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::cell::RefCell;
|
||||
use crate::ast::nodes::{Node, UntypedKind};
|
||||
@@ -64,18 +64,31 @@ pub struct Binder {
|
||||
functions: Vec<FunctionCompiler>,
|
||||
// Globals mapping: Name -> (Index, Type)
|
||||
globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
|
||||
// Identities of definitions that need boxing (pre-calculated)
|
||||
boxed_declarations: HashSet<Identity>,
|
||||
}
|
||||
|
||||
impl Binder {
|
||||
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 {
|
||||
functions: Vec::new(),
|
||||
globals,
|
||||
boxed_declarations: boxed,
|
||||
};
|
||||
binder.functions.push(FunctionCompiler::new());
|
||||
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> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user