diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index a07a7f0..88c8af7 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, BTreeMap}; +use std::collections::{HashMap, BTreeMap, HashSet}; use std::rc::Rc; use std::cell::RefCell; use crate::ast::nodes::{Node, UntypedKind}; @@ -64,18 +64,31 @@ pub struct Binder { functions: Vec, // Globals mapping: Name -> (Index, Type) globals: Rc>>, + // Identities of definitions that need boxing (pre-calculated) + boxed_declarations: HashSet, } impl Binder { pub fn new(globals: Rc>>) -> Self { + Self::with_boxed(globals, HashSet::new()) + } + + fn with_boxed(globals: Rc>>, boxed: HashSet) -> Self { let mut binder = Self { functions: Vec::new(), globals, + boxed_declarations: boxed, }; binder.functions.push(FunctionCompiler::new()); binder } + pub fn bind_root(globals: Rc>>, node: &Node) -> Result, String> { + let boxed = crate::ast::compiler::upvalues::UpvalueAnalyzer::analyze(node); + let mut binder = Self::with_boxed(globals, boxed); + binder.bind(node) + } + pub fn bind(&mut self, node: &Node) -> Result, String> { match &node.kind { UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop, StaticType::Void)), @@ -157,9 +170,11 @@ impl Binder { info.ty = ty.clone(); } } - Ok(self.make_node(node.identity.clone(), BoundKind::Set { - addr: Address::Local(slot_or_idx), - value: Box::new(val_node) + let is_boxed = self.boxed_declarations.contains(&node.identity); + Ok(self.make_node(node.identity.clone(), BoundKind::DefLocal { + slot: slot_or_idx, + value: Box::new(val_node) , + is_boxed }, ty)) } }, @@ -326,3 +341,61 @@ impl Binder { Node { identity, kind, ty } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::parser::Parser; + + #[test] + fn test_upvalue_capture_sets_is_boxed() { + // Wrap in a lambda to ensure 'x' is a local variable, not a global + let source = "(fn [] (do (def x 10) (def f (fn [] x)) x))"; + let mut parser = Parser::new(source).unwrap(); + let untyped = parser.parse_expression().unwrap(); + + let globals = Rc::new(RefCell::new(HashMap::new())); + let bound = Binder::bind_root(globals, &untyped).unwrap(); + + // Structure: Lambda -> Block -> [ DefLocal(x), DefLocal(f), Get(x) ] + if let BoundKind::Lambda { body, .. } = &bound.kind { + if let BoundKind::Block { exprs } = &body.kind { + let x_decl = &exprs[0]; + if let BoundKind::DefLocal { is_boxed, .. } = &x_decl.kind { + assert!(is_boxed, "Variable 'x' should be marked as boxed because it is captured by lambda 'f'"); + } else { + panic!("First expression in block should be DefLocal, got {:?}", x_decl.kind); + } + } else { + panic!("Lambda body should be a Block, got {:?}", body.kind); + } + } else { + panic!("Root should be a Lambda, got {:?}", bound.kind); + } + } + + #[test] + fn test_no_capture_not_boxed() { + let source = "(fn [] (do (def x 10) x))"; + let mut parser = Parser::new(source).unwrap(); + let untyped = parser.parse_expression().unwrap(); + + let globals = Rc::new(RefCell::new(HashMap::new())); + let bound = Binder::bind_root(globals, &untyped).unwrap(); + + if let BoundKind::Lambda { body, .. } = &bound.kind { + if let BoundKind::Block { exprs } = &body.kind { + let x_decl = &exprs[0]; + if let BoundKind::DefLocal { is_boxed, .. } = &x_decl.kind { + assert!(!is_boxed, "Variable 'x' should NOT be boxed as it is not captured"); + } else { + panic!("First expression should be DefLocal"); + } + } else { + panic!("Lambda body should be a Block"); + } + } else { + panic!("Root should be a Lambda"); + } + } +} diff --git a/src/ast/compiler/bound_nodes.rs b/src/ast/compiler/bound_nodes.rs index f66dd75..2c03055 100644 --- a/src/ast/compiler/bound_nodes.rs +++ b/src/ast/compiler/bound_nodes.rs @@ -17,12 +17,19 @@ pub enum BoundKind { // Variable Access (Resolved) Get(Address), - // Variable Update (Resolved) + // Variable Update (Assignment) Set { addr: Address, value: Box>, }, + // Variable Declaration (Local) + DefLocal { + slot: u32, + value: Box>, + is_boxed: bool, + }, + If { cond: Box>, then_br: Box>, diff --git a/src/ast/compiler/mod.rs b/src/ast/compiler/mod.rs index 6df5bb3..962bcb6 100644 --- a/src/ast/compiler/mod.rs +++ b/src/ast/compiler/mod.rs @@ -1,7 +1,9 @@ pub mod binder; pub mod bound_nodes; pub mod tco; +pub mod upvalues; pub use binder::*; pub use bound_nodes::*; pub use tco::*; +pub use upvalues::*; diff --git a/src/ast/compiler/tco.rs b/src/ast/compiler/tco.rs index 9c60723..f3b80a1 100644 --- a/src/ast/compiler/tco.rs +++ b/src/ast/compiler/tco.rs @@ -98,6 +98,12 @@ impl TCO { ..node } }, + BoundKind::DefLocal { slot, value, is_boxed } => { + Node { + kind: BoundKind::DefLocal { slot, value: Box::new(Self::transform(*value, false)), is_boxed }, + ..node + } + }, BoundKind::DefGlobal { global_index, value } => { Node { kind: BoundKind::DefGlobal { global_index, value: Box::new(Self::transform(*value, false)) }, diff --git a/src/ast/compiler/upvalues.rs b/src/ast/compiler/upvalues.rs new file mode 100644 index 0000000..c8f5736 --- /dev/null +++ b/src/ast/compiler/upvalues.rs @@ -0,0 +1,90 @@ +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; +use crate::ast::nodes::{Node, UntypedKind}; +use crate::ast::types::Identity; + +/// Analyzes the AST to find all variable declarations that are captured by nested lambdas. +/// Returns a set of Identities corresponding to the 'Def' nodes that need boxing. +pub struct UpvalueAnalyzer; + +impl UpvalueAnalyzer { + pub fn analyze(root: &Node) -> HashSet { + let mut boxed = HashSet::new(); + let mut scopes = vec![HashMap::new()]; // Root scope + Self::visit(root, &mut scopes, &mut boxed); + boxed + } + + fn visit(node: &Node, scopes: &mut Vec, Identity>>, boxed: &mut HashSet) { + match &node.kind { + UntypedKind::Identifier(name) => { + // Resolve name in scope stack (from inner to outer) + for (depth, scope) in scopes.iter().rev().enumerate() { + if let Some(id) = scope.get(name) { + if depth > 0 { + // Captured from an outer scope! + boxed.insert(id.clone()); + } + break; + } + } + } + UntypedKind::Def { name, value } => { + // 1. Visit initializer first (it executes in current scope) + Self::visit(value, scopes, boxed); + + // 2. Define the variable in current scope + if let Some(current) = scopes.last_mut() { + current.insert(name.clone(), node.identity.clone()); + } + } + UntypedKind::Lambda { params, body } => { + // Enter new lambda scope + let mut new_scope = HashMap::new(); + for p in params { + // Parameters are defined in the new scope, masking outer ones. + // We use the lambda's identity as a placeholder for parameter origin. + new_scope.insert(p.clone(), node.identity.clone()); + } + + scopes.push(new_scope); + Self::visit(body, scopes, boxed); + scopes.pop(); + } + UntypedKind::If { cond, then_br, else_br } => { + Self::visit(cond, scopes, boxed); + Self::visit(then_br, scopes, boxed); + if let Some(e) = else_br { + Self::visit(e, scopes, boxed); + } + } + UntypedKind::Assign { target, value } => { + Self::visit(target, scopes, boxed); + Self::visit(value, scopes, boxed); + } + UntypedKind::Call { callee, args } => { + Self::visit(callee, scopes, boxed); + for arg in args { + Self::visit(arg, scopes, boxed); + } + } + UntypedKind::Block { exprs } => { + for expr in exprs { + Self::visit(expr, scopes, boxed); + } + } + UntypedKind::Tuple { elements } => { + for el in elements { + Self::visit(el, scopes, boxed); + } + } + UntypedKind::Map { entries } => { + for (k, v) in entries { + Self::visit(k, scopes, boxed); + Self::visit(v, scopes, boxed); + } + } + UntypedKind::Nop | UntypedKind::Constant(_) | UntypedKind::Extension(_) => {} + } + } +} diff --git a/src/ast/environment.rs b/src/ast/environment.rs index a74f623..af4e0e1 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -116,11 +116,10 @@ impl Environment { } // 3. Bind & Type Check - let mut binder = Binder::new(self.global_names.clone()); - let mut bound_ast = binder.bind(&untyped_ast)?; + let bound_ast = Binder::bind_root(self.global_names.clone(), &untyped_ast)?; // 4. Optimize - bound_ast = TCO::optimize(bound_ast); + let bound_ast = TCO::optimize(bound_ast); // 5. Execute let mut vm = VM::new(self.global_values.clone()); diff --git a/src/ast/vm.rs b/src/ast/vm.rs index ce2224b..58e40c7 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -82,6 +82,28 @@ impl VM { Ok(val) }, + BoundKind::DefLocal { slot, value, is_boxed } => { + let val = self.eval(value)?; + let final_val = if *is_boxed { + Value::Cell(Rc::new(RefCell::new(val))) + } else { + val + }; + + let frame = self.frames.last().ok_or("No call frame")?; + let abs_index = frame.stack_base + (*slot as usize); + + // If it's the exact top of stack, push it. Otherwise, set it. + if abs_index == self.stack.len() { + self.stack.push(final_val.clone()); + } else if abs_index < self.stack.len() { + self.stack[abs_index] = final_val.clone(); + } else { + return Err(format!("Stack gap at local {}", slot)); + } + Ok(final_val) + }, + BoundKind::If { cond, then_br, else_br } => { let c = self.eval(cond)?; if c.is_truthy() { diff --git a/src/main.rs b/src/main.rs index 8285471..b288ea6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -220,7 +220,7 @@ impl eframe::App for CompilerApp { } } else { ui.add_enabled_ui(false, |ui| { - ui.button("Save (None loaded)"); + let _ = ui.button("Save (None loaded)"); }); }