MIR: fix free-vars lexical scoping

This commit is contained in:
nyash-codex
2025-12-13 01:35:39 +09:00
parent 1fae4f1648
commit 0913ee8bbc

View File

@ -1,157 +1,293 @@
use crate::ast::ASTNode; use crate::ast::ASTNode;
use std::collections::HashSet; use std::collections::HashSet;
/// Collect free variables used in `node` into `used`, excluding names present in `locals`.
/// `locals` is updated as new local declarations are encountered.
#[allow(dead_code)] #[allow(dead_code)]
pub(in crate::mir::builder) fn collect_free_vars( pub(in crate::mir::builder) fn collect_free_vars(
node: &ASTNode, node: &ASTNode,
used: &mut HashSet<String>, used: &mut HashSet<String>,
locals: &mut HashSet<String>, locals: &mut HashSet<String>,
) { ) {
match node { let mut collector = FreeVarCollector::new(used, locals);
ASTNode::Variable { name, .. } => { collector.walk(node, true);
if name != "me" && name != "this" && !locals.contains(name) { *locals = collector.take_root_locals();
used.insert(name.clone()); }
}
struct FreeVarCollector<'a> {
used: &'a mut HashSet<String>,
root_locals: HashSet<String>,
scope_stack: Vec<HashSet<String>>,
}
impl<'a> FreeVarCollector<'a> {
fn new(used: &'a mut HashSet<String>, root_locals: &HashSet<String>) -> Self {
Self {
used,
root_locals: root_locals.clone(),
scope_stack: vec![root_locals.clone()],
} }
ASTNode::Local { variables, .. } => { }
for v in variables {
locals.insert(v.clone()); fn take_root_locals(self) -> HashSet<String> {
} self.root_locals
}
fn is_declared(&self, name: &str) -> bool {
self.scope_stack.iter().rev().any(|s| s.contains(name))
}
fn declare_in_current_scope(&mut self, name: &str) {
if let Some(scope) = self.scope_stack.last_mut() {
scope.insert(name.to_string());
} }
ASTNode::Assignment { target, value, .. } => { if self.scope_stack.len() == 1 {
collect_free_vars(target, used, locals); self.root_locals.insert(name.to_string());
collect_free_vars(value, used, locals);
} }
// Phase 152-A: Grouped assignment expression }
ASTNode::GroupedAssignmentExpr { rhs, .. } => {
collect_free_vars(rhs, used, locals); fn with_child_scope(&mut self, f: impl FnOnce(&mut Self)) {
self.scope_stack.push(HashSet::new());
f(self);
self.scope_stack.pop();
}
fn walk_block(&mut self, statements: &[ASTNode]) {
for st in statements {
self.walk(st, false);
} }
ASTNode::BinaryOp { left, right, .. } => { }
collect_free_vars(left, used, locals);
collect_free_vars(right, used, locals); fn walk(&mut self, node: &ASTNode, is_root: bool) {
} match node {
ASTNode::UnaryOp { operand, .. } => { ASTNode::Variable { name, .. } => {
collect_free_vars(operand, used, locals); if name != "me" && name != "this" && !self.is_declared(name) {
} self.used.insert(name.clone());
ASTNode::MethodCall {
object, arguments, ..
} => {
collect_free_vars(object, used, locals);
for a in arguments {
collect_free_vars(a, used, locals);
}
}
ASTNode::FunctionCall { arguments, .. } => {
for a in arguments {
collect_free_vars(a, used, locals);
}
}
ASTNode::Call {
callee, arguments, ..
} => {
collect_free_vars(callee, used, locals);
for a in arguments {
collect_free_vars(a, used, locals);
}
}
ASTNode::FieldAccess { object, .. } => {
collect_free_vars(object, used, locals);
}
ASTNode::Index { target, index, .. } => {
collect_free_vars(target, used, locals);
collect_free_vars(index, used, locals);
}
ASTNode::New { arguments, .. } => {
for a in arguments {
collect_free_vars(a, used, locals);
}
}
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
collect_free_vars(condition, used, locals);
for st in then_body {
collect_free_vars(st, used, locals);
}
if let Some(eb) = else_body {
for st in eb {
collect_free_vars(st, used, locals);
} }
} }
} ASTNode::Local {
ASTNode::Loop { variables,
condition, body, .. initial_values,
} => { ..
collect_free_vars(condition, used, locals); } => {
for st in body { for init in initial_values {
collect_free_vars(st, used, locals); if let Some(expr) = init {
} self.walk(expr, false);
} }
ASTNode::TryCatch { }
try_body, for v in variables {
catch_clauses, self.declare_in_current_scope(v);
finally_body,
..
} => {
for st in try_body {
collect_free_vars(st, used, locals);
}
for c in catch_clauses {
for st in &c.body {
collect_free_vars(st, used, locals);
} }
} }
if let Some(fb) = finally_body { ASTNode::Outbox {
for st in fb { variables,
collect_free_vars(st, used, locals); initial_values,
..
} => {
for init in initial_values {
if let Some(expr) = init {
self.walk(expr, false);
}
}
for v in variables {
self.declare_in_current_scope(v);
} }
} }
} ASTNode::Assignment { target, value, .. } => {
ASTNode::Throw { expression, .. } => { self.walk(target, false);
collect_free_vars(expression, used, locals); self.walk(value, false);
}
ASTNode::Print { expression, .. } => {
collect_free_vars(expression, used, locals);
}
ASTNode::Return { value, .. } => {
if let Some(v) = value {
collect_free_vars(v, used, locals);
} }
} ASTNode::GroupedAssignmentExpr { rhs, .. } => {
ASTNode::AwaitExpression { expression, .. } => { self.walk(rhs, false);
collect_free_vars(expression, used, locals);
}
ASTNode::MatchExpr {
scrutinee,
arms,
else_expr,
..
} => {
collect_free_vars(scrutinee, used, locals);
for (_, e) in arms {
collect_free_vars(e, used, locals);
} }
collect_free_vars(else_expr, used, locals); ASTNode::BinaryOp { left, right, .. } => {
} self.walk(left, false);
ASTNode::Program { statements, .. } => { self.walk(right, false);
for st in statements {
collect_free_vars(st, used, locals);
} }
} ASTNode::UnaryOp { operand, .. } => {
ASTNode::FunctionDeclaration { params, body, .. } => { self.walk(operand, false);
let mut inner = locals.clone();
for p in params {
inner.insert(p.clone());
} }
for st in body { ASTNode::MethodCall {
collect_free_vars(st, used, &mut inner); object, arguments, ..
} => {
self.walk(object, false);
for a in arguments {
self.walk(a, false);
}
} }
ASTNode::FunctionCall { arguments, .. } => {
for a in arguments {
self.walk(a, false);
}
}
ASTNode::Call {
callee, arguments, ..
} => {
self.walk(callee, false);
for a in arguments {
self.walk(a, false);
}
}
ASTNode::FieldAccess { object, .. } => {
self.walk(object, false);
}
ASTNode::Index { target, index, .. } => {
self.walk(target, false);
self.walk(index, false);
}
ASTNode::New { arguments, .. } => {
for a in arguments {
self.walk(a, false);
}
}
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
self.walk(condition, false);
self.with_child_scope(|this| this.walk_block(then_body));
if let Some(eb) = else_body {
self.with_child_scope(|this| this.walk_block(eb));
}
}
ASTNode::Loop {
condition, body, ..
} => {
self.walk(condition, false);
self.with_child_scope(|this| this.walk_block(body));
}
ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
..
} => {
self.with_child_scope(|this| this.walk_block(try_body));
for c in catch_clauses {
self.with_child_scope(|this| this.walk_block(&c.body));
}
if let Some(fb) = finally_body {
self.with_child_scope(|this| this.walk_block(fb));
}
}
ASTNode::Throw { expression, .. } => {
self.walk(expression, false);
}
ASTNode::Print { expression, .. } => {
self.walk(expression, false);
}
ASTNode::Return { value, .. } => {
if let Some(v) = value {
self.walk(v, false);
}
}
ASTNode::AwaitExpression { expression, .. } => {
self.walk(expression, false);
}
ASTNode::MatchExpr {
scrutinee,
arms,
else_expr,
..
} => {
self.walk(scrutinee, false);
for (_, e) in arms {
self.walk(e, false);
}
self.walk(else_expr, false);
}
ASTNode::Program { statements, .. } => {
if is_root {
self.walk_block(statements);
} else {
self.with_child_scope(|this| this.walk_block(statements));
}
}
ASTNode::ScopeBox { body, .. } => {
if is_root {
self.walk_block(body);
} else {
self.with_child_scope(|this| this.walk_block(body));
}
}
ASTNode::FunctionDeclaration { params, body, .. } => {
self.with_child_scope(|this| {
for p in params {
this.declare_in_current_scope(p);
}
this.walk_block(body);
});
}
_ => {}
} }
_ => {} }
}
#[cfg(test)]
mod tests {
use super::collect_free_vars;
use crate::ast::ASTNode;
use crate::ast::Span;
use std::collections::HashSet;
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: Span::unknown(),
}
}
fn local(name: &str) -> ASTNode {
ASTNode::Local {
variables: vec![name.to_string()],
initial_values: vec![None],
span: Span::unknown(),
}
}
fn block(statements: Vec<ASTNode>) -> ASTNode {
ASTNode::Program {
statements,
span: Span::unknown(),
}
}
fn scopebox(statements: Vec<ASTNode>) -> ASTNode {
ASTNode::ScopeBox {
body: statements,
span: Span::unknown(),
}
}
#[test]
fn block_local_does_not_leak_to_outer_scope() {
let node = block(vec![block(vec![local("y")]), var("y")]);
let mut used = HashSet::new();
let mut locals = HashSet::new();
collect_free_vars(&node, &mut used, &mut locals);
assert!(used.contains("y"));
}
#[test]
fn scopebox_local_does_not_leak_to_outer_scope() {
let node = block(vec![scopebox(vec![local("y")]), var("y")]);
let mut used = HashSet::new();
let mut locals = HashSet::new();
collect_free_vars(&node, &mut used, &mut locals);
assert!(used.contains("y"));
}
#[test]
fn local_initializer_is_counted_as_read_before_declare() {
let node = block(vec![ASTNode::Local {
variables: vec!["x".to_string()],
initial_values: vec![Some(Box::new(var("y")))],
span: Span::unknown(),
}]);
let mut used = HashSet::new();
let mut locals = HashSet::new();
collect_free_vars(&node, &mut used, &mut locals);
assert!(used.contains("y"));
assert!(locals.contains("x"));
} }
} }