Add AST dumper and improve upvalue analysis

The `UpvalueAnalyzer` now correctly identifies which lambdas capture
which variable declarations, rather than just marking declarations that
need boxing. This information is stored in a `capture_map`.

The `Binder` has been updated to use this new `capture_map` instead of a
`HashSet` of boxed declarations. The `DefLocal` node in `bound_nodes.rs`
now stores a `captured_by` field, which is a list of lambda identities
that capture the local variable.

A new `dumper` module has been added to provide a human-readable
representation of the bound AST, including information about captured
variables.

The `VM` has been updated to use the `captured_by` field to determine if
a local variable needs to be stored in a `Cell`, rather than relying on
a boolean `is_boxed` flag.

An example script `extreme_capture.myc` has been added to test deep
nesting and variable capture.

A new "Dump AST" button has been added to the UI, which uses the new
`Dumper` to display the bound AST.
This commit is contained in:
Michael Schimmel
2026-02-18 00:35:08 +01:00
parent 4d9adccab5
commit 25e3bc1d01
10 changed files with 269 additions and 49 deletions
+14 -14
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, BTreeMap, HashSet};
use std::collections::{HashMap, BTreeMap};
use std::rc::Rc;
use std::cell::RefCell;
use crate::ast::nodes::{Node, UntypedKind};
@@ -64,28 +64,28 @@ 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>,
// Map of Declaration Identity -> List of Lambda Identities that capture it
capture_map: HashMap<Identity, Vec<Identity>>,
}
impl Binder {
pub fn new(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>) -> Self {
Self::with_boxed(globals, HashSet::new())
Self::with_boxed(globals, HashMap::new())
}
fn with_boxed(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, boxed: HashSet<Identity>) -> Self {
fn with_boxed(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, captures: HashMap<Identity, Vec<Identity>>) -> Self {
let mut binder = Self {
functions: Vec::new(),
globals,
boxed_declarations: boxed,
capture_map: captures,
};
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);
let captures = crate::ast::compiler::upvalues::UpvalueAnalyzer::analyze(node);
let mut binder = Self::with_boxed(globals, captures);
binder.bind(node)
}
@@ -170,11 +170,11 @@ impl Binder {
info.ty = ty.clone();
}
}
let is_boxed = self.boxed_declarations.contains(&node.identity);
let captured_by = self.capture_map.get(&node.identity).cloned().unwrap_or_default();
Ok(self.make_node(node.identity.clone(), BoundKind::DefLocal {
slot: slot_or_idx,
value: Box::new(val_node) ,
is_boxed
captured_by
}, ty))
}
},
@@ -361,8 +361,8 @@ mod tests {
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'");
if let BoundKind::DefLocal { captured_by, .. } = &x_decl.kind {
assert!(!captured_by.is_empty(), "Variable 'x' should have capturers because it is used in lambda 'f'");
} else {
panic!("First expression in block should be DefLocal, got {:?}", x_decl.kind);
}
@@ -386,8 +386,8 @@ mod tests {
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");
if let BoundKind::DefLocal { captured_by, .. } = &x_decl.kind {
assert!(captured_by.is_empty(), "Variable 'x' should NOT have any capturers");
} else {
panic!("First expression should be DefLocal");
}