Introduce boxing for captured variables
The binder now uses an `UpvalueAnalyzer` to identify local variables that are captured by nested functions. These identified variables are marked with `is_boxed: true` during `DefLocal` node creation. The VM then uses this flag to wrap such variables in a `Value::Cell` (using `Rc<RefCell<_>>`) to ensure they can be mutated across function calls. feat: Introduce boxing for captured variables Add UpvalueAnalyzer to identify variables captured by nested lambdas. Modify Binder to use the analyzer and mark captured local variables for boxing. Update BoundKind::DefLocal to include an `is_boxed` flag. Update VM to box captured variables when they are defined. Add tests for upvalue capture detection.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{HashMap, BTreeMap};
|
use std::collections::{HashMap, BTreeMap, HashSet};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use crate::ast::nodes::{Node, UntypedKind};
|
use crate::ast::nodes::{Node, UntypedKind};
|
||||||
@@ -64,18 +64,31 @@ pub struct Binder {
|
|||||||
functions: Vec<FunctionCompiler>,
|
functions: Vec<FunctionCompiler>,
|
||||||
// Globals mapping: Name -> (Index, Type)
|
// Globals mapping: Name -> (Index, Type)
|
||||||
globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
|
globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
|
||||||
|
// Identities of definitions that need boxing (pre-calculated)
|
||||||
|
boxed_declarations: HashSet<Identity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binder {
|
impl Binder {
|
||||||
pub fn new(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>) -> Self {
|
pub fn new(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>) -> Self {
|
||||||
|
Self::with_boxed(globals, HashSet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_boxed(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, boxed: HashSet<Identity>) -> Self {
|
||||||
let mut binder = Self {
|
let mut binder = Self {
|
||||||
functions: Vec::new(),
|
functions: Vec::new(),
|
||||||
globals,
|
globals,
|
||||||
|
boxed_declarations: boxed,
|
||||||
};
|
};
|
||||||
binder.functions.push(FunctionCompiler::new());
|
binder.functions.push(FunctionCompiler::new());
|
||||||
binder
|
binder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bind_root(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, 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<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
|
pub fn bind(&mut self, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop, StaticType::Void)),
|
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop, StaticType::Void)),
|
||||||
@@ -157,9 +170,11 @@ impl Binder {
|
|||||||
info.ty = ty.clone();
|
info.ty = ty.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(self.make_node(node.identity.clone(), BoundKind::Set {
|
let is_boxed = self.boxed_declarations.contains(&node.identity);
|
||||||
addr: Address::Local(slot_or_idx),
|
Ok(self.make_node(node.identity.clone(), BoundKind::DefLocal {
|
||||||
value: Box::new(val_node)
|
slot: slot_or_idx,
|
||||||
|
value: Box::new(val_node) ,
|
||||||
|
is_boxed
|
||||||
}, ty))
|
}, ty))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -326,3 +341,61 @@ impl Binder {
|
|||||||
Node { identity, kind, ty }
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,12 +17,19 @@ pub enum BoundKind {
|
|||||||
// Variable Access (Resolved)
|
// Variable Access (Resolved)
|
||||||
Get(Address),
|
Get(Address),
|
||||||
|
|
||||||
// Variable Update (Resolved)
|
// Variable Update (Assignment)
|
||||||
Set {
|
Set {
|
||||||
addr: Address,
|
addr: Address,
|
||||||
value: Box<Node<BoundKind, StaticType>>,
|
value: Box<Node<BoundKind, StaticType>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Variable Declaration (Local)
|
||||||
|
DefLocal {
|
||||||
|
slot: u32,
|
||||||
|
value: Box<Node<BoundKind, StaticType>>,
|
||||||
|
is_boxed: bool,
|
||||||
|
},
|
||||||
|
|
||||||
If {
|
If {
|
||||||
cond: Box<Node<BoundKind, StaticType>>,
|
cond: Box<Node<BoundKind, StaticType>>,
|
||||||
then_br: Box<Node<BoundKind, StaticType>>,
|
then_br: Box<Node<BoundKind, StaticType>>,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
pub mod binder;
|
pub mod binder;
|
||||||
pub mod bound_nodes;
|
pub mod bound_nodes;
|
||||||
pub mod tco;
|
pub mod tco;
|
||||||
|
pub mod upvalues;
|
||||||
|
|
||||||
pub use binder::*;
|
pub use binder::*;
|
||||||
pub use bound_nodes::*;
|
pub use bound_nodes::*;
|
||||||
pub use tco::*;
|
pub use tco::*;
|
||||||
|
pub use upvalues::*;
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ impl TCO {
|
|||||||
..node
|
..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 } => {
|
BoundKind::DefGlobal { global_index, value } => {
|
||||||
Node {
|
Node {
|
||||||
kind: BoundKind::DefGlobal { global_index, value: Box::new(Self::transform(*value, false)) },
|
kind: BoundKind::DefGlobal { global_index, value: Box::new(Self::transform(*value, false)) },
|
||||||
|
|||||||
@@ -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<UntypedKind>) -> HashSet<Identity> {
|
||||||
|
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<UntypedKind>, scopes: &mut Vec<HashMap<Rc<str>, Identity>>, boxed: &mut HashSet<Identity>) {
|
||||||
|
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(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,11 +116,10 @@ impl Environment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Bind & Type Check
|
// 3. Bind & Type Check
|
||||||
let mut binder = Binder::new(self.global_names.clone());
|
let bound_ast = Binder::bind_root(self.global_names.clone(), &untyped_ast)?;
|
||||||
let mut bound_ast = binder.bind(&untyped_ast)?;
|
|
||||||
|
|
||||||
// 4. Optimize
|
// 4. Optimize
|
||||||
bound_ast = TCO::optimize(bound_ast);
|
let bound_ast = TCO::optimize(bound_ast);
|
||||||
|
|
||||||
// 5. Execute
|
// 5. Execute
|
||||||
let mut vm = VM::new(self.global_values.clone());
|
let mut vm = VM::new(self.global_values.clone());
|
||||||
|
|||||||
@@ -82,6 +82,28 @@ impl VM {
|
|||||||
Ok(val)
|
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 } => {
|
BoundKind::If { cond, then_br, else_br } => {
|
||||||
let c = self.eval(cond)?;
|
let c = self.eval(cond)?;
|
||||||
if c.is_truthy() {
|
if c.is_truthy() {
|
||||||
|
|||||||
+1
-1
@@ -220,7 +220,7 @@ impl eframe::App for CompilerApp {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.add_enabled_ui(false, |ui| {
|
ui.add_enabled_ui(false, |ui| {
|
||||||
ui.button("Save (None loaded)");
|
let _ = ui.button("Save (None loaded)");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user