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:
Michael Schimmel
2026-02-18 00:09:46 +01:00
parent 83f6cfb8e1
commit 4d9adccab5
8 changed files with 208 additions and 9 deletions
+90
View File
@@ -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(_) => {}
}
}
}