From b21a02f12ffb6d562b072e2aed92878144d07c2d Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Fri, 4 Oct 2024 20:26:18 -0700 Subject: [PATCH] Move utility visitors out of compiler.rs We are converting a 1200 line file into an 800 and 400 line files. It's actually a lot easier to read now, those visitors rarely ever change and they get in the way of me reading the file (with my eyes, not with a program). Signed-off-by: Alek Ratzloff --- src/compiler.rs | 408 +-------------------------------------- src/compiler/visitors.rs | 408 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+), 404 deletions(-) create mode 100644 src/compiler/visitors.rs diff --git a/src/compiler.rs b/src/compiler.rs index 1e85734..2d3542d 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -1,3 +1,5 @@ +mod visitors; + use std::collections::{HashMap, HashSet}; use std::fmt::{self, Display}; use std::fs::File; @@ -11,417 +13,15 @@ use common_macros::hash_map; use thiserror::Error; use crate::ast::*; +use crate::compiler::visitors::*; use crate::obj::prelude::*; -use crate::obj::Ptr; -use crate::obj::BUILTINS; +use crate::obj::{Ptr, BUILTINS}; use crate::parser::Parser; use crate::token::TokenKind; use crate::vm::*; pub type Result = std::result::Result>; -//////////////////////////////////////////////////////////////////////////////// -// LineNumber visitor -//////////////////////////////////////////////////////////////////////////////// - -#[derive(Default)] -struct LineNumber { - lock_start: bool, - start: usize, - end: usize, -} - -impl LineNumber { - fn update_start(&mut self, start: usize) { - if !self.lock_start { - self.start = start; - self.lock_start = true; - } - } - - fn update_end(&mut self, end: usize) { - self.end = end; - } -} - -impl StmtVisitor for LineNumber { - fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { - self.update_start(stmt.import_kw.line); - self.update_end(stmt.module.line); - Ok(()) - } - - fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { - stmt.expr.accept(self).unwrap(); - Ok(()) - } - - fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { - self.update_start(stmt.lhs.line); - stmt.rhs.accept(self).unwrap(); - Ok(()) - } - - fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { - stmt.expr.accept(self).unwrap(); - stmt.rhs.accept(self).unwrap(); - Ok(()) - } - - fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { - self.update_start(stmt.lbrace.line); - self.update_end(stmt.rbrace.line); - Ok(()) - } - fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { - self.update_start(stmt.return_kw.line); - self.update_end(stmt.return_kw.line); - if let Some(expr) = stmt.expr.as_ref() { - expr.accept(self).unwrap(); - } - Ok(()) - } - fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { - self.update_start(stmt.if_kw.line); - stmt.condition.accept(self).unwrap(); - stmt.then_branch.accept(self).unwrap(); - for stmt in &stmt.else_branch { - stmt.accept(self).unwrap(); - } - Ok(()) - } -} - -impl ExprVisitor for LineNumber { - fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { - expr.lhs.accept(self).unwrap(); - expr.rhs.accept(self).unwrap(); - Ok(()) - } - - fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { - self.update_start(expr.op.line); - expr.expr.accept(self).unwrap(); - Ok(()) - } - - fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { - expr.expr.accept(self).unwrap(); - self.update_end(expr.rparen.line); - Ok(()) - } - - fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { - expr.expr.accept(self).unwrap(); - self.update_end(expr.name.line); - Ok(()) - } - - fn visit_index_expr(&mut self, expr: &IndexExpr) -> Result<()> { - expr.expr.accept(self).unwrap(); - self.update_end(expr.rbracket.line); - Ok(()) - } - - fn visit_primary_expr(&mut self, expr: &PrimaryExpr) -> Result<()> { - self.update_start(expr.token.line); - self.update_end(expr.token.line); - Ok(()) - } - - fn visit_function_expr(&mut self, expr: &FunctionExpr) -> Result<()> { - self.update_start(expr.lparen.line); - self.update_end(expr.rbrace.line); - Ok(()) - } - - fn visit_list_expr(&mut self, expr: &ListExpr) -> Result<()> { - self.update_start(expr.lbracket.line); - self.update_end(expr.rbracket.line); - Ok(()) - } -} - -fn expr_line_number(expr: &dyn Expr) -> LineRange { - let mut line_number = LineNumber::default(); - expr.accept(&mut line_number).unwrap(); - (line_number.start, line_number.end) -} - -fn stmt_line_number(stmt: &dyn Stmt) -> LineRange { - let mut line_number = LineNumber::default(); - stmt.accept(&mut line_number).unwrap(); - (line_number.start, line_number.end) -} - -//////////////////////////////////////////////////////////////////////////////// -// LocalAssignCollector and LocalNameCollector -//////////////////////////////////////////////////////////////////////////////// - -// TODO - reduce copy/paste stuff here? - -#[derive(Default)] -struct LocalAssignCollector { - names: HashSet, -} - -impl LocalAssignCollector { - fn collect(body: &Vec) -> HashSet { - let mut collector = Self::default(); - for stmt in body { - stmt.accept(&mut collector).unwrap(); - } - collector.names - } -} - -impl StmtVisitor for LocalAssignCollector { - fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { - if stmt.what.is_empty() { - // `import foo` - if stmt.module.kind == TokenKind::Name { - // do not add `import "my_file.ext"` - self.names.insert(stmt.module.text.to_string()); - } - } else { - // `import foo, bar from baz` - for what in &stmt.what { - if what.kind == TokenKind::Name { - // do not add `import * from foo` - self.names.insert(what.text.to_string()); - } - } - } - Ok(()) - } - - fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { - stmt.expr.accept(self)?; - Ok(()) - } - - fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { - self.names.insert(stmt.lhs.text.to_string()); - Ok(()) - } - - fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { - stmt.expr.accept(self)?; - stmt.rhs.accept(self)?; - Ok(()) - } - - fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { - // we visit the block statement because even though it goes below the current "local" - // scope, we're ultimately trying to get a list of ALL local names that are assigned to in - // this scope. - // TODO FIXME BUG this does create some weirdness, for example take this: - // outer_function = () { - // some_value = 1234 - // inner_function = () { - // { - // # this is a local value because we're assigning to it - // some_value = 5678 - // } - // # our local named "some_value" has gone out of scope, so hypothetically we - // # should be using the "some_value" that was defined in the scope above us. - // # however, since we're collecting local assignments in all blocks, this should - // # error out as "unknown local 'some_value'" - // println(some_value) - // } - // return inner_function - // } - // - // Ideally, we would be checking nonlocals with every new scope layer, and every new block. - // This is a pretty tough bug to solve with how things are set up right now. not sure how - // we'll go about solving this one. - for stmt in &stmt.stmts { - stmt.accept(self)?; - } - Ok(()) - } - - fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { - if let Some(expr) = stmt.expr.as_ref() { - expr.accept(self)?; - } - Ok(()) - } - - fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { - stmt.condition.accept(self)?; - stmt.then_branch.accept(self)?; - for stmt in &stmt.else_branch { - stmt.accept(self)?; - } - Ok(()) - } -} - -impl ExprVisitor for LocalAssignCollector { - fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { - expr.lhs.accept(self)?; - expr.rhs.accept(self)?; - Ok(()) - } - - fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_index_expr(&mut self, _expr: &IndexExpr) -> Result<()> { - Ok(()) - } - - fn visit_primary_expr(&mut self, _expr: &PrimaryExpr) -> Result<()> { - Ok(()) - } - - fn visit_function_expr(&mut self, _expr: &FunctionExpr) -> Result<()> { - // don't visit function expr, we're only collecting local assigns - Ok(()) - } - - fn visit_list_expr(&mut self, _expr: &ListExpr) -> Result<()> { - // there shouldn't be any local assignments inside of a list expression - Ok(()) - } -} - -#[derive(Default)] -struct LocalNameCollector { - names: HashSet, -} - -impl LocalNameCollector { - fn collect(body: &Vec) -> HashSet { - let mut collector = Self::default(); - for stmt in body { - stmt.accept(&mut collector).unwrap(); - } - collector.names - } -} - -impl StmtVisitor for LocalNameCollector { - fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { - if stmt.what.is_empty() { - // `import foo` - if stmt.module.kind == TokenKind::Name { - // do not add `import "my_file.ext"` - self.names.insert(stmt.module.text.to_string()); - } - } else { - // `import foo, bar from baz` - for what in &stmt.what { - if what.kind == TokenKind::Name { - // do not add `import * from foo` - self.names.insert(what.text.to_string()); - } - } - } - Ok(()) - } - - fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { - stmt.expr.accept(self)?; - Ok(()) - } - - fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { - stmt.rhs.accept(self)?; - Ok(()) - } - - fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { - stmt.expr.accept(self)?; - stmt.rhs.accept(self)?; - Ok(()) - } - - fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { - for stmt in &stmt.stmts { - stmt.accept(self)?; - } - Ok(()) - } - - fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { - if let Some(expr) = stmt.expr.as_ref() { - expr.accept(self)?; - } - Ok(()) - } - - fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { - stmt.condition.accept(self)?; - stmt.then_branch.accept(self)?; - for stmt in &stmt.else_branch { - stmt.accept(self)?; - } - Ok(()) - } -} - -impl ExprVisitor for LocalNameCollector { - fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { - expr.lhs.accept(self)?; - expr.rhs.accept(self)?; - Ok(()) - } - - fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { - expr.expr.accept(self)?; - Ok(()) - } - - fn visit_index_expr(&mut self, expr: &IndexExpr) -> Result<()> { - expr.expr.accept(self)?; - expr.index.accept(self)?; - Ok(()) - } - - fn visit_primary_expr(&mut self, expr: &PrimaryExpr) -> Result<()> { - if expr.token.kind == TokenKind::Name { - self.names.insert(expr.token.text.to_string()); - } - Ok(()) - } - - fn visit_function_expr(&mut self, _expr: &FunctionExpr) -> Result<()> { - // don't visit function expr, we're only collecting local assigns - Ok(()) - } - - fn visit_list_expr(&mut self, expr: &ListExpr) -> Result<()> { - for expr in &expr.exprs { - expr.accept(self)?; - } - Ok(()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Misc //////////////////////////////////////////////////////////////////////////////// diff --git a/src/compiler/visitors.rs b/src/compiler/visitors.rs new file mode 100644 index 0000000..b21fa77 --- /dev/null +++ b/src/compiler/visitors.rs @@ -0,0 +1,408 @@ +use std::collections::HashSet; + +use crate::ast::*; +use crate::compiler::Result; +use crate::token::TokenKind; +use crate::vm::*; + +//////////////////////////////////////////////////////////////////////////////// +// LineNumber visitor +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Default)] +pub(super) struct LineNumber { + lock_start: bool, + start: usize, + end: usize, +} + +impl LineNumber { + fn update_start(&mut self, start: usize) { + if !self.lock_start { + self.start = start; + self.lock_start = true; + } + } + + fn update_end(&mut self, end: usize) { + self.end = end; + } +} + +impl StmtVisitor for LineNumber { + fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { + self.update_start(stmt.import_kw.line); + self.update_end(stmt.module.line); + Ok(()) + } + + fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { + stmt.expr.accept(self).unwrap(); + Ok(()) + } + + fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { + self.update_start(stmt.lhs.line); + stmt.rhs.accept(self).unwrap(); + Ok(()) + } + + fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { + stmt.expr.accept(self).unwrap(); + stmt.rhs.accept(self).unwrap(); + Ok(()) + } + + fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { + self.update_start(stmt.lbrace.line); + self.update_end(stmt.rbrace.line); + Ok(()) + } + fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { + self.update_start(stmt.return_kw.line); + self.update_end(stmt.return_kw.line); + if let Some(expr) = stmt.expr.as_ref() { + expr.accept(self).unwrap(); + } + Ok(()) + } + fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { + self.update_start(stmt.if_kw.line); + stmt.condition.accept(self).unwrap(); + stmt.then_branch.accept(self).unwrap(); + for stmt in &stmt.else_branch { + stmt.accept(self).unwrap(); + } + Ok(()) + } +} + +impl ExprVisitor for LineNumber { + fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { + expr.lhs.accept(self).unwrap(); + expr.rhs.accept(self).unwrap(); + Ok(()) + } + + fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { + self.update_start(expr.op.line); + expr.expr.accept(self).unwrap(); + Ok(()) + } + + fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { + expr.expr.accept(self).unwrap(); + self.update_end(expr.rparen.line); + Ok(()) + } + + fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { + expr.expr.accept(self).unwrap(); + self.update_end(expr.name.line); + Ok(()) + } + + fn visit_index_expr(&mut self, expr: &IndexExpr) -> Result<()> { + expr.expr.accept(self).unwrap(); + self.update_end(expr.rbracket.line); + Ok(()) + } + + fn visit_primary_expr(&mut self, expr: &PrimaryExpr) -> Result<()> { + self.update_start(expr.token.line); + self.update_end(expr.token.line); + Ok(()) + } + + fn visit_function_expr(&mut self, expr: &FunctionExpr) -> Result<()> { + self.update_start(expr.lparen.line); + self.update_end(expr.rbrace.line); + Ok(()) + } + + fn visit_list_expr(&mut self, expr: &ListExpr) -> Result<()> { + self.update_start(expr.lbracket.line); + self.update_end(expr.rbracket.line); + Ok(()) + } +} + +pub(super) fn expr_line_number(expr: &dyn Expr) -> LineRange { + let mut line_number = LineNumber::default(); + expr.accept(&mut line_number).unwrap(); + (line_number.start, line_number.end) +} + +pub(super) fn stmt_line_number(stmt: &dyn Stmt) -> LineRange { + let mut line_number = LineNumber::default(); + stmt.accept(&mut line_number).unwrap(); + (line_number.start, line_number.end) +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAssignCollector and LocalNameCollector +//////////////////////////////////////////////////////////////////////////////// + +// TODO - reduce copy/paste stuff here? + +#[derive(Default)] +pub(super) struct LocalAssignCollector { + names: HashSet, +} + +impl LocalAssignCollector { + pub fn collect(body: &Vec) -> HashSet { + let mut collector = Self::default(); + for stmt in body { + stmt.accept(&mut collector).unwrap(); + } + collector.names + } +} + +impl StmtVisitor for LocalAssignCollector { + fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { + if stmt.what.is_empty() { + // `import foo` + if stmt.module.kind == TokenKind::Name { + // do not add `import "my_file.ext"` + self.names.insert(stmt.module.text.to_string()); + } + } else { + // `import foo, bar from baz` + for what in &stmt.what { + if what.kind == TokenKind::Name { + // do not add `import * from foo` + self.names.insert(what.text.to_string()); + } + } + } + Ok(()) + } + + fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { + stmt.expr.accept(self)?; + Ok(()) + } + + fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { + self.names.insert(stmt.lhs.text.to_string()); + Ok(()) + } + + fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { + stmt.expr.accept(self)?; + stmt.rhs.accept(self)?; + Ok(()) + } + + fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { + // we visit the block statement because even though it goes below the current "local" + // scope, we're ultimately trying to get a list of ALL local names that are assigned to in + // this scope. + // TODO FIXME BUG this does create some weirdness, for example take this: + // outer_function = () { + // some_value = 1234 + // inner_function = () { + // { + // # this is a local value because we're assigning to it + // some_value = 5678 + // } + // # our local named "some_value" has gone out of scope, so hypothetically we + // # should be using the "some_value" that was defined in the scope above us. + // # however, since we're collecting local assignments in all blocks, this should + // # error out as "unknown local 'some_value'" + // println(some_value) + // } + // return inner_function + // } + // + // Ideally, we would be checking nonlocals with every new scope layer, and every new block. + // This is a pretty tough bug to solve with how things are set up right now. not sure how + // we'll go about solving this one. + for stmt in &stmt.stmts { + stmt.accept(self)?; + } + Ok(()) + } + + fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { + if let Some(expr) = stmt.expr.as_ref() { + expr.accept(self)?; + } + Ok(()) + } + + fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { + stmt.condition.accept(self)?; + stmt.then_branch.accept(self)?; + for stmt in &stmt.else_branch { + stmt.accept(self)?; + } + Ok(()) + } +} + +impl ExprVisitor for LocalAssignCollector { + fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { + expr.lhs.accept(self)?; + expr.rhs.accept(self)?; + Ok(()) + } + + fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_index_expr(&mut self, _expr: &IndexExpr) -> Result<()> { + Ok(()) + } + + fn visit_primary_expr(&mut self, _expr: &PrimaryExpr) -> Result<()> { + Ok(()) + } + + fn visit_function_expr(&mut self, _expr: &FunctionExpr) -> Result<()> { + // don't visit function expr, we're only collecting local assigns + Ok(()) + } + + fn visit_list_expr(&mut self, _expr: &ListExpr) -> Result<()> { + // there shouldn't be any local assignments inside of a list expression + Ok(()) + } +} + +#[derive(Default)] +pub(super) struct LocalNameCollector { + names: HashSet, +} + +impl LocalNameCollector { + pub fn collect(body: &Vec) -> HashSet { + let mut collector = Self::default(); + for stmt in body { + stmt.accept(&mut collector).unwrap(); + } + collector.names + } +} + +impl StmtVisitor for LocalNameCollector { + fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> { + if stmt.what.is_empty() { + // `import foo` + if stmt.module.kind == TokenKind::Name { + // do not add `import "my_file.ext"` + self.names.insert(stmt.module.text.to_string()); + } + } else { + // `import foo, bar from baz` + for what in &stmt.what { + if what.kind == TokenKind::Name { + // do not add `import * from foo` + self.names.insert(what.text.to_string()); + } + } + } + Ok(()) + } + + fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> { + stmt.expr.accept(self)?; + Ok(()) + } + + fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> { + stmt.rhs.accept(self)?; + Ok(()) + } + + fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> { + stmt.expr.accept(self)?; + stmt.rhs.accept(self)?; + Ok(()) + } + + fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> { + for stmt in &stmt.stmts { + stmt.accept(self)?; + } + Ok(()) + } + + fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> { + if let Some(expr) = stmt.expr.as_ref() { + expr.accept(self)?; + } + Ok(()) + } + + fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> { + stmt.condition.accept(self)?; + stmt.then_branch.accept(self)?; + for stmt in &stmt.else_branch { + stmt.accept(self)?; + } + Ok(()) + } +} + +impl ExprVisitor for LocalNameCollector { + fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> { + expr.lhs.accept(self)?; + expr.rhs.accept(self)?; + Ok(()) + } + + fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> { + expr.expr.accept(self)?; + Ok(()) + } + + fn visit_index_expr(&mut self, expr: &IndexExpr) -> Result<()> { + expr.expr.accept(self)?; + expr.index.accept(self)?; + Ok(()) + } + + fn visit_primary_expr(&mut self, expr: &PrimaryExpr) -> Result<()> { + if expr.token.kind == TokenKind::Name { + self.names.insert(expr.token.text.to_string()); + } + Ok(()) + } + + fn visit_function_expr(&mut self, _expr: &FunctionExpr) -> Result<()> { + // don't visit function expr, we're only collecting local assigns + Ok(()) + } + + fn visit_list_expr(&mut self, expr: &ListExpr) -> Result<()> { + for expr in &expr.exprs { + expr.accept(self)?; + } + Ok(()) + } +}