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:
@@ -3,85 +3,94 @@ 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.
|
||||
/// Analyzes the AST to find which lambdas capture which variable declarations.
|
||||
/// Returns a map: Declaration Identity -> List of Lambda Identities that capture it.
|
||||
pub struct UpvalueAnalyzer;
|
||||
|
||||
impl UpvalueAnalyzer {
|
||||
pub fn analyze(root: &Node<UntypedKind>) -> HashSet<Identity> {
|
||||
let mut boxed = HashSet::new();
|
||||
pub fn analyze(root: &Node<UntypedKind>) -> HashMap<Identity, Vec<Identity>> {
|
||||
let mut capture_map: HashMap<Identity, HashSet<Identity>> = HashMap::new();
|
||||
let mut scopes = vec![HashMap::new()]; // Root scope
|
||||
Self::visit(root, &mut scopes, &mut boxed);
|
||||
boxed
|
||||
|
||||
Self::visit(root, &mut scopes, &mut capture_map, None);
|
||||
|
||||
// Convert HashSet back to sorted Vec for the final result
|
||||
capture_map.into_iter()
|
||||
.map(|(decl, lambdas)| (decl, lambdas.into_iter().collect()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn visit(node: &Node<UntypedKind>, scopes: &mut Vec<HashMap<Rc<str>, Identity>>, boxed: &mut HashSet<Identity>) {
|
||||
fn visit(
|
||||
node: &Node<UntypedKind>,
|
||||
scopes: &mut Vec<HashMap<Rc<str>, Identity>>,
|
||||
capture_map: &mut HashMap<Identity, HashSet<Identity>>,
|
||||
current_lambda: Option<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 let Some(decl_id) = scope.get(name) {
|
||||
if depth > 0 {
|
||||
// Captured from an outer scope!
|
||||
boxed.insert(id.clone());
|
||||
if let Some(lambda_id) = ¤t_lambda {
|
||||
capture_map.entry(decl_id.clone())
|
||||
.or_insert_with(HashSet::new)
|
||||
.insert(lambda_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
|
||||
Self::visit(value, scopes, capture_map, current_lambda.clone());
|
||||
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);
|
||||
// The current node is the lambda causing captures in its body
|
||||
Self::visit(body, scopes, capture_map, Some(node.identity.clone()));
|
||||
scopes.pop();
|
||||
}
|
||||
UntypedKind::If { cond, then_br, else_br } => {
|
||||
Self::visit(cond, scopes, boxed);
|
||||
Self::visit(then_br, scopes, boxed);
|
||||
Self::visit(cond, scopes, capture_map, current_lambda.clone());
|
||||
Self::visit(then_br, scopes, capture_map, current_lambda.clone());
|
||||
if let Some(e) = else_br {
|
||||
Self::visit(e, scopes, boxed);
|
||||
Self::visit(e, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
}
|
||||
UntypedKind::Assign { target, value } => {
|
||||
Self::visit(target, scopes, boxed);
|
||||
Self::visit(value, scopes, boxed);
|
||||
Self::visit(target, scopes, capture_map, current_lambda.clone());
|
||||
Self::visit(value, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
UntypedKind::Call { callee, args } => {
|
||||
Self::visit(callee, scopes, boxed);
|
||||
Self::visit(callee, scopes, capture_map, current_lambda.clone());
|
||||
for arg in args {
|
||||
Self::visit(arg, scopes, boxed);
|
||||
Self::visit(arg, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
}
|
||||
UntypedKind::Block { exprs } => {
|
||||
for expr in exprs {
|
||||
Self::visit(expr, scopes, boxed);
|
||||
Self::visit(expr, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
}
|
||||
UntypedKind::Tuple { elements } => {
|
||||
for el in elements {
|
||||
Self::visit(el, scopes, boxed);
|
||||
Self::visit(el, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
}
|
||||
UntypedKind::Map { entries } => {
|
||||
for (k, v) in entries {
|
||||
Self::visit(k, scopes, boxed);
|
||||
Self::visit(v, scopes, boxed);
|
||||
Self::visit(k, scopes, capture_map, current_lambda.clone());
|
||||
Self::visit(v, scopes, capture_map, current_lambda.clone());
|
||||
}
|
||||
}
|
||||
UntypedKind::Nop | UntypedKind::Constant(_) | UntypedKind::Extension(_) => {}
|
||||
|
||||
Reference in New Issue
Block a user