Move variable resolution to middleend

- Introduce AST-based visitor and resolver in middleend
- Update guard field and parser to use AstNode<Expr>
- Remove backend variable_resolution and adjust exports
- Expose middleend in the library
This commit is contained in:
Giulio Agostini
2026-06-30 09:47:22 +02:00
parent 29c86d278d
commit ef8abda048
10 changed files with 361 additions and 326 deletions
-115
View File
@@ -1,115 +0,0 @@
use crate::common::{
ast::{AstNode, Expr, Stmt},
lox_result::LoxResult,
};
// Enum per tutti i possibili target di operazioni
enum OperationTarget<'a> {
Stmt(&'a mut Stmt),
Expr(&'a mut Expr),
AstStmt(&'a mut AstNode<Stmt>),
AstExpr(&'a mut AstNode<Expr>),
}
// Trait per le operazioni
trait Operation: Send + Sync {
fn execute(&self, target: OperationTarget) -> LoxResult<()>;
}
// Operazione concreta per set_label
struct SetLabelOperation {
new_label: String,
}
impl SetLabelOperation {
fn new(label: String) -> Self {
Self { new_label: label }
}
}
impl Operation for SetLabelOperation {
fn execute(&self, target: OperationTarget) -> LoxResult<()> {
match target {
OperationTarget::Stmt(stmt) => {
if let Stmt::Block { label, .. } = stmt {
*label = self.new_label.clone();
}
}
OperationTarget::AstStmt(node) => {
if let Stmt::Block { label, .. } = &mut node.node {
*label = self.new_label.clone();
}
}
_ => {} // Expr non hanno label
}
Ok(())
}
}
// Trait per convertire tipi in OperationTarget
trait AsOperationTarget {
fn as_target(&mut self) -> OperationTarget;
}
impl AsOperationTarget for Stmt {
fn as_target(&mut self) -> OperationTarget {
OperationTarget::Stmt(self)
}
}
impl AsOperationTarget for Expr {
fn as_target(&mut self) -> OperationTarget {
OperationTarget::Expr(self)
}
}
impl AsOperationTarget for AstNode<Stmt> {
fn as_target(&mut self) -> OperationTarget {
OperationTarget::AstStmt(self)
}
}
impl AsOperationTarget for AstNode<Expr> {
fn as_target(&mut self) -> OperationTarget {
OperationTarget::AstExpr(self)
}
}
// Crawler generico con Command Pattern
struct Crawler<T: AsOperationTarget> {
node: T,
operations: Vec<Box<dyn Operation>>,
}
impl<T: AsOperationTarget> Crawler<T> {
fn new(node: T) -> Self {
Self {
node,
operations: vec![],
}
}
fn add_operation(&mut self, op: Box<dyn Operation>) {
self.operations.push(op);
}
fn execute_operations(&mut self) -> LoxResult<()> {
for op in &self.operations {
op.execute(self.node.as_target())?;
}
Ok(())
}
fn with_operation(mut self, op: Box<dyn Operation>) -> Self {
self.operations.push(op);
self
}
fn get_node(&self) -> &T {
&self.node
}
fn get_node_mut(&mut self) -> &mut T {
&mut self.node
}
}
+2 -1
View File
@@ -1 +1,2 @@
pub mod crawler;
pub mod variable_resolution;
pub mod visit_ast;
+99
View File
@@ -0,0 +1,99 @@
use std::collections::HashMap;
use crate::{
backend::environment::EnvironmentStack,
common::{
ast::{AstNode, Expr, Stmt},
lox_result::{LoxError, LoxResult},
},
frontend::source_registry::SourceSlice,
middleend::visit_ast::{walk_expr, walk_stmt, Visitor},
};
struct Resolver {
scopes: EnvironmentStack<bool>,
locals: HashMap<SourceSlice, usize>,
}
impl Resolver {
pub fn new() -> Self {
Resolver {
scopes: EnvironmentStack::new(),
locals: HashMap::new(),
}
}
fn declare(&mut self, name: &String) {
if self.scopes.is_empty() {
return;
}
let _ = self.scopes.set(name.clone(), false);
}
fn define(&mut self, name: &String) {
if self.scopes.is_empty() {
return;
}
let _ = self.scopes.set(name.clone(), true);
}
fn resolve_local(&mut self, name: &String) {
let depth = self.scopes.depth();
for i in (0..depth).rev() {
if self.scopes.scope_contains(i, name) {
self.locals.insert(, i);
}
}
}
}
impl Visitor for Resolver {
fn visit_stmt(&mut self, stmt: &AstNode<Stmt>) -> LoxResult<()> {
match &stmt.node {
Stmt::VarDeclaration { name, .. } => {
self.declare(name);
// `walk_stmt` resolves the initializer (if present).
walk_stmt(self, stmt)?;
self.define(name);
Ok(())
}
Stmt::VarAssigment { name, .. } => {
match self.scopes.get(name) {
Ok(true) => (),
Ok(false) => {
return Err(LoxError::RuntimeError {
source_slice: SourceSlice::default(),
message: "Cant read local variable in it own lintilizer".to_string(),
})
}
Err(err) => return Err(err),
}
// `walk_stmt` resolves the assigned value.
walk_stmt(self, stmt)
}
Stmt::Block { .. } => {
self.scopes.push_new_scope();
walk_stmt(self, stmt)?;
self.scopes.pop_scope();
Ok(())
}
// Expression, Print, Return, If, While, For: default traversal.
_ => walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &AstNode<Expr>) -> LoxResult<()> {
match &expr.node {
Expr::Identifier { name, .. } => {
if !self.scopes.is_empty() && self.scopes.get(name).is_ok() {
return Err(LoxError::ParseError {
source_slice: SourceSlice::default(),
message: "Cant read local varialbe in it own initializer".to_string(),
});
}
Ok(())
}
_ => walk_expr(self, expr),
}
}
}
+248
View File
@@ -0,0 +1,248 @@
//! A reusable, read-only AST visitor with default traversal.
//!
//! The traversal is split into two layers so that many different static
//! analyses can share a single definition of "how to walk the tree":
//!
//! * The `walk_*` free functions contain the canonical recursion. For each
//! node they call back into the visitor on every child. This is the only
//! place that needs to know the shape of the AST.
//! * The [`Visitor`] trait's `visit_*` methods are the overridable hooks. Each
//! one defaults to calling the matching `walk_*` function, so a visitor that
//! overrides nothing still performs a complete traversal.
//!
//! To implement an analysis, implement [`Visitor`] and override only the hooks
//! you care about. Inside an override, call the corresponding `walk_*` function
//! whenever you want the default "descend into children" behaviour to happen.
//! Accumulate results in your own struct fields (errors, scope tables, type
//! information, ...) rather than through the return value, which is only used
//! to short-circuit on error.
//!
//! # Example
//!
//! ```ignore
//! struct IdentifierCounter { count: usize }
//!
//! impl Visitor for IdentifierCounter {
//! fn visit_expr(&mut self, expr: &AstNode<Expr>) -> LoxResult<()> {
//! if let Expr::Identifier { .. } = &expr.node {
//! self.count += 1;
//! }
//! walk_expr(self, expr) // keep descending into children
//! }
//! }
//! ```
use crate::common::{
ast::{AstNode, Expr, Stmt},
base_value::{BaseValue, LoxFunction},
lox_result::LoxResult,
};
/// A read-only visitor over the AST.
///
/// Every hook has a default implementation that performs the standard
/// recursive traversal, so implementors only override the cases they need.
pub trait Visitor: Sized {
/// Visit a statement node. Defaults to [`walk_stmt`].
fn visit_stmt(&mut self, stmt: &AstNode<Stmt>) -> LoxResult<()> {
walk_stmt(self, stmt)
}
/// Visit an expression node. Defaults to [`walk_expr`].
fn visit_expr(&mut self, expr: &AstNode<Expr>) -> LoxResult<()> {
walk_expr(self, expr)
}
/// Visit a function literal (parameters, optional guard and body).
///
/// Defaults to [`walk_function`], which walks the guard (if any) and the
/// body. Override this to manage a parameter scope before descending.
fn visit_function(&mut self, function: &LoxFunction) -> LoxResult<()> {
walk_function(self, function)
}
}
/// Recurse into the children of `stmt`, calling back into `visitor`.
pub fn walk_stmt<V: Visitor>(visitor: &mut V, stmt: &AstNode<Stmt>) -> LoxResult<()> {
match &stmt.node {
Stmt::Expression { expression, .. } => visitor.visit_expr(expression),
Stmt::Print { expression, .. } => visitor.visit_expr(expression),
Stmt::VarDeclaration { initializer, .. } => {
if let Some(initializer) = initializer {
visitor.visit_expr(initializer)?;
}
Ok(())
}
Stmt::VarAssigment { value, .. } => visitor.visit_expr(value),
Stmt::Return { expression, .. } => visitor.visit_expr(expression),
Stmt::Block { statements, .. } => {
for statement in statements.iter() {
visitor.visit_stmt(statement)?;
}
Ok(())
}
Stmt::If {
condition,
then_branch,
elif_branch,
else_branch,
..
} => {
visitor.visit_expr(condition)?;
visitor.visit_stmt(then_branch)?;
for (elif_condition, elif_body) in elif_branch.iter() {
visitor.visit_expr(elif_condition)?;
visitor.visit_stmt(elif_body)?;
}
if let Some(else_body) = else_branch {
visitor.visit_stmt(else_body)?;
}
Ok(())
}
Stmt::While {
condition, body, ..
} => {
visitor.visit_expr(condition)?;
visitor.visit_stmt(body)
}
Stmt::For {
variable,
condition,
increment,
body,
..
} => {
visitor.visit_stmt(variable)?;
visitor.visit_expr(condition)?;
visitor.visit_stmt(increment)?;
visitor.visit_stmt(body)
}
}
}
/// Recurse into the children of `expr`, calling back into `visitor`.
pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &AstNode<Expr>) -> LoxResult<()> {
match &expr.node {
// A function literal carries an entire sub-tree (its body), so it is
// not a leaf: hand it to the dedicated function hook.
Expr::Literal { value } => {
if let BaseValue::Function(function) = value {
visitor.visit_function(function)?;
}
Ok(())
}
Expr::Identifier { .. } => Ok(()),
Expr::Binary { left, right, .. } => {
visitor.visit_expr(left)?;
visitor.visit_expr(right)
}
Expr::Unary { operand, .. } => visitor.visit_expr(operand),
Expr::Grouping { expression } => visitor.visit_expr(expression),
Expr::Call {
callee, arguments, ..
} => {
visitor.visit_expr(callee)?;
for argument in arguments.iter() {
visitor.visit_expr(argument)?;
}
Ok(())
}
}
}
/// Walk the guard (if present) and body of a function literal.
pub fn walk_function<V: Visitor>(visitor: &mut V, function: &LoxFunction) -> LoxResult<()> {
if let Some(guard) = &function.guard {
visitor.visit_expr(guard)?;
}
visitor.visit_stmt(&function.body)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::lox_result::runtime_error;
use crate::frontend::lexer::Lexer;
use crate::frontend::parser::Parser;
fn parse(src: &str) -> Vec<AstNode<Stmt>> {
let tokens = Lexer::new(src.to_string(), 0)
.scans_tokens()
.expect("source should lex");
Parser::new(tokens).parse().expect("source should parse")
}
/// A visitor that relies entirely on the default traversal and just counts
/// how many statement and expression nodes it sees.
#[derive(Default)]
struct Counter {
stmts: usize,
exprs: usize,
}
impl Visitor for Counter {
fn visit_stmt(&mut self, stmt: &AstNode<Stmt>) -> LoxResult<()> {
self.stmts += 1;
walk_stmt(self, stmt)
}
fn visit_expr(&mut self, expr: &AstNode<Expr>) -> LoxResult<()> {
self.exprs += 1;
walk_expr(self, expr)
}
}
fn count(src: &str) -> Counter {
let mut counter = Counter::default();
for stmt in parse(src).iter() {
counter.visit_stmt(stmt).unwrap();
}
counter
}
#[test]
fn counts_every_node_via_default_traversal() {
// 1 + 2 * 3 => Binary(+){ Literal, Binary(*){ Literal, Literal } }
let counter = count("1 + 2 * 3;");
assert_eq!(counter.stmts, 1);
assert_eq!(counter.exprs, 5);
}
#[test]
fn descends_into_function_bodies() {
// The function body must be traversed through `visit_function`, so the
// `return a;` inside it should contribute to the counts.
let counter = count("f :: fn (a) do return a; end");
// VarDeclaration + Block + Return
assert_eq!(counter.stmts, 3);
// Function literal + identifier `a`
assert_eq!(counter.exprs, 2);
}
/// A visitor that aborts as soon as it sees an identifier, used to check
/// that errors short-circuit the traversal.
struct FailOnIdentifier;
impl Visitor for FailOnIdentifier {
fn visit_expr(&mut self, expr: &AstNode<Expr>) -> LoxResult<()> {
if let Expr::Identifier { .. } = &expr.node {
return runtime_error(expr.source_slice.clone(), "found an identifier");
}
walk_expr(self, expr)
}
}
#[test]
fn errors_propagate_through_traversal() {
let stmts = parse("var x: Int = 1; x;");
let mut visitor = FailOnIdentifier;
let mut result = Ok(());
for stmt in stmts.iter() {
result = visitor.visit_stmt(stmt);
if result.is_err() {
break;
}
}
assert!(result.is_err());
}
}