From 795d68ecf15bd5b8b2b75380084551e67fbdfb16 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sat, 13 Dec 2025 02:04:04 +0900 Subject: [PATCH] joinir(ownership): shadowing-aware AST ownership analyzer --- src/mir/join_ir/ownership/ast_analyzer.rs | 703 ++++++++++++++-------- 1 file changed, 442 insertions(+), 261 deletions(-) diff --git a/src/mir/join_ir/ownership/ast_analyzer.rs b/src/mir/join_ir/ownership/ast_analyzer.rs index cd09515b..84a95e66 100644 --- a/src/mir/join_ir/ownership/ast_analyzer.rs +++ b/src/mir/join_ir/ownership/ast_analyzer.rs @@ -6,15 +6,24 @@ use super::*; use crate::ast::ASTNode; use std::collections::{BTreeMap, BTreeSet}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct BindingId(u32); + +#[derive(Debug, Clone)] +struct BindingInfo { + name: String, + owner_scope: ScopeId, +} + #[derive(Debug)] struct ScopeInfo { id: ScopeId, kind: ScopeKind, parent: Option, - defined: BTreeSet, - reads: BTreeSet, - writes: BTreeSet, - condition_reads: BTreeSet, + declared: BTreeMap, + reads: BTreeSet, + writes: BTreeSet, + condition_reads: BTreeSet, } /// Analyzes real AST and produces `OwnershipPlan`. @@ -25,20 +34,29 @@ struct ScopeInfo { /// - Treats `Loop/While/ForRange` and `If` conditions as `condition_reads` pub struct AstOwnershipAnalyzer { scopes: BTreeMap, + bindings: BTreeMap, next_scope_id: u32, + next_binding_id: u32, + env_stack: Vec>, } impl AstOwnershipAnalyzer { pub fn new() -> Self { Self { scopes: BTreeMap::new(), + bindings: BTreeMap::new(), next_scope_id: 0, + next_binding_id: 0, + env_stack: Vec::new(), } } pub fn analyze_ast(&mut self, ast: &ASTNode) -> Result, String> { self.scopes.clear(); + self.bindings.clear(); self.next_scope_id = 0; + self.next_binding_id = 0; + self.env_stack.clear(); match ast { ASTNode::Program { statements, .. } => { @@ -50,7 +68,9 @@ impl AstOwnershipAnalyzer { self.analyze_function_decl(ast, None)?; } _ => { - return Err("AstOwnershipAnalyzer: expected Program or FunctionDeclaration".to_string()); + return Err( + "AstOwnershipAnalyzer: expected Program or FunctionDeclaration".to_string(), + ); } } @@ -88,7 +108,7 @@ impl AstOwnershipAnalyzer { id, kind, parent, - defined: BTreeSet::new(), + declared: BTreeMap::new(), reads: BTreeSet::new(), writes: BTreeSet::new(), condition_reads: BTreeSet::new(), @@ -97,36 +117,47 @@ impl AstOwnershipAnalyzer { id } - fn analyze_function_decl(&mut self, node: &ASTNode, parent: Option) -> Result { + fn analyze_function_decl( + &mut self, + node: &ASTNode, + parent: Option, + ) -> Result { let ASTNode::FunctionDeclaration { params, body, .. } = node else { return Err("AstOwnershipAnalyzer: expected FunctionDeclaration".to_string()); }; let scope_id = self.alloc_scope(ScopeKind::Function, parent); + self.push_env(); for name in params { - self.scopes - .get_mut(&scope_id) - .unwrap() - .defined - .insert(name.to_string()); + self.declare_binding(scope_id, name)?; } - let block_scope = self.alloc_scope(ScopeKind::Block, Some(scope_id)); - for stmt in body { - self.analyze_node(stmt, block_scope, false)?; - } - self.propagate_to_parent(block_scope); + let result: Result<(), String> = body + .iter() + .try_for_each(|stmt| self.analyze_node(stmt, scope_id, false)); + self.pop_env(); + result?; Ok(scope_id) } - fn analyze_node(&mut self, node: &ASTNode, current_scope: ScopeId, is_condition: bool) -> Result<(), String> { + fn analyze_node( + &mut self, + node: &ASTNode, + current_scope: ScopeId, + is_condition: bool, + ) -> Result<(), String> { match node { ASTNode::Program { statements, .. } => { - for s in statements { - self.analyze_node(s, current_scope, false)?; - } + let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); + self.push_env(); + let result: Result<(), String> = statements + .iter() + .try_for_each(|s| self.analyze_node(s, block_scope, false)); + self.pop_env(); + result?; + self.propagate_to_parent(block_scope); } ASTNode::FunctionDeclaration { .. } => { @@ -151,18 +182,11 @@ impl AstOwnershipAnalyzer { initial_values, .. } => { - let owner_scope = self.find_enclosing_loop_or_function(current_scope); - for name in variables { - self.scopes - .get_mut(&owner_scope) - .unwrap() - .defined - .insert(name.to_string()); - } - for init in initial_values { + for (name, init) in variables.iter().zip(initial_values.iter()) { if let Some(expr) = init.as_ref() { self.analyze_node(expr, current_scope, false)?; } + self.declare_binding(current_scope, name)?; } } @@ -171,18 +195,11 @@ impl AstOwnershipAnalyzer { initial_values, .. } => { - let owner_scope = self.find_enclosing_loop_or_function(current_scope); - for name in variables { - self.scopes - .get_mut(&owner_scope) - .unwrap() - .defined - .insert(name.to_string()); - } - for init in initial_values { + for (name, init) in variables.iter().zip(initial_values.iter()) { if let Some(expr) = init.as_ref() { self.analyze_node(expr, current_scope, false)?; } + self.declare_binding(current_scope, name)?; } } @@ -192,11 +209,10 @@ impl AstOwnershipAnalyzer { } ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => { - self.scopes - .get_mut(¤t_scope) - .unwrap() - .writes - .insert(lhs.to_string()); + let binding = self.resolve_binding(lhs).ok_or_else(|| { + format!("AstOwnershipAnalyzer: write to undefined var '{}'", lhs) + })?; + self.record_write(binding, current_scope); self.analyze_node(rhs, current_scope, is_condition)?; } @@ -205,11 +221,13 @@ impl AstOwnershipAnalyzer { expression, .. } => { - self.scopes - .get_mut(¤t_scope) - .unwrap() - .writes - .insert(variable.to_string()); + let binding = self.resolve_binding(variable).ok_or_else(|| { + format!( + "AstOwnershipAnalyzer: write to undefined var '{}'", + variable + ) + })?; + self.record_write(binding, current_scope); self.analyze_node(expression, current_scope, false)?; } @@ -235,32 +253,45 @@ impl AstOwnershipAnalyzer { self.analyze_node(condition, if_scope, true)?; let then_scope = self.alloc_scope(ScopeKind::Block, Some(if_scope)); - for s in then_body { - self.analyze_node(s, then_scope, false)?; - } + self.push_env(); + let result: Result<(), String> = then_body + .iter() + .try_for_each(|s| self.analyze_node(s, then_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(then_scope); if let Some(else_body) = else_body { let else_scope = self.alloc_scope(ScopeKind::Block, Some(if_scope)); - for s in else_body { - self.analyze_node(s, else_scope, false)?; - } + self.push_env(); + let result: Result<(), String> = else_body + .iter() + .try_for_each(|s| self.analyze_node(s, else_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(else_scope); } self.propagate_to_parent(if_scope); } - ASTNode::Loop { condition, body, .. } | ASTNode::While { condition, body, .. } => { + ASTNode::Loop { + condition, body, .. + } + | ASTNode::While { + condition, body, .. + } => { let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope)); - self.analyze_node(condition, loop_scope, true)?; - - let body_scope = self.alloc_scope(ScopeKind::Block, Some(loop_scope)); - for s in body { - self.analyze_node(s, body_scope, false)?; - } - self.propagate_to_parent(body_scope); - + self.push_env(); + let result: Result<(), String> = (|| { + for s in body { + self.analyze_node(s, loop_scope, false)?; + } + self.analyze_node(condition, loop_scope, true)?; + Ok(()) + })(); + self.pop_env(); + result?; self.propagate_to_parent(loop_scope); } @@ -272,28 +303,29 @@ impl AstOwnershipAnalyzer { .. } => { let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope)); - self.scopes - .get_mut(&loop_scope) - .unwrap() - .defined - .insert(var_name.to_string()); - self.analyze_node(start, loop_scope, true)?; - self.analyze_node(end, loop_scope, true)?; - - let body_scope = self.alloc_scope(ScopeKind::Block, Some(loop_scope)); - for s in body { - self.analyze_node(s, body_scope, false)?; - } - self.propagate_to_parent(body_scope); - + self.push_env(); + let result: Result<(), String> = (|| { + self.declare_binding(loop_scope, var_name)?; + self.analyze_node(start, loop_scope, true)?; + self.analyze_node(end, loop_scope, true)?; + for s in body { + self.analyze_node(s, loop_scope, false)?; + } + Ok(()) + })(); + self.pop_env(); + result?; self.propagate_to_parent(loop_scope); } ASTNode::ScopeBox { body, .. } => { let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); - for s in body { - self.analyze_node(s, block_scope, false)?; - } + self.push_env(); + let result: Result<(), String> = body + .iter() + .try_for_each(|s| self.analyze_node(s, block_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(block_scope); } @@ -304,31 +336,37 @@ impl AstOwnershipAnalyzer { .. } => { let try_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); - for s in try_body { - self.analyze_node(s, try_scope, false)?; - } + self.push_env(); + let result: Result<(), String> = try_body + .iter() + .try_for_each(|s| self.analyze_node(s, try_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(try_scope); for clause in catch_clauses { let catch_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); + self.push_env(); if let Some(var) = clause.variable_name.as_ref() { - self.scopes - .get_mut(&catch_scope) - .unwrap() - .defined - .insert(var.to_string()); - } - for s in &clause.body { - self.analyze_node(s, catch_scope, false)?; + self.declare_binding(catch_scope, var)?; } + let result: Result<(), String> = clause + .body + .iter() + .try_for_each(|s| self.analyze_node(s, catch_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(catch_scope); } if let Some(finally_body) = finally_body { let finally_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); - for s in finally_body { - self.analyze_node(s, finally_scope, false)?; - } + self.push_env(); + let result: Result<(), String> = finally_body + .iter() + .try_for_each(|s| self.analyze_node(s, finally_scope, false)); + self.pop_env(); + result?; self.propagate_to_parent(finally_scope); } } @@ -350,17 +388,8 @@ impl AstOwnershipAnalyzer { | ASTNode::MeField { .. } => {} ASTNode::Variable { name, .. } => { - self.scopes - .get_mut(¤t_scope) - .unwrap() - .reads - .insert(name.to_string()); - if is_condition { - self.scopes - .get_mut(¤t_scope) - .unwrap() - .condition_reads - .insert(name.to_string()); + if let Some(binding) = self.resolve_binding(name) { + self.record_read(binding, current_scope, is_condition); } } @@ -374,9 +403,7 @@ impl AstOwnershipAnalyzer { } ASTNode::MethodCall { - object, - arguments, - .. + object, arguments, .. } => { self.analyze_node(object, current_scope, is_condition)?; for a in arguments { @@ -411,7 +438,9 @@ impl AstOwnershipAnalyzer { } } - ASTNode::Call { callee, arguments, .. } => { + ASTNode::Call { + callee, arguments, .. + } => { self.analyze_node(callee, current_scope, is_condition)?; for a in arguments { self.analyze_node(a, current_scope, is_condition)?; @@ -446,15 +475,14 @@ impl AstOwnershipAnalyzer { ASTNode::Lambda { .. } => {} ASTNode::Arrow { - sender, - receiver, - .. + sender, receiver, .. } => { self.analyze_node(sender, current_scope, is_condition)?; self.analyze_node(receiver, current_scope, is_condition)?; } - ASTNode::AwaitExpression { expression, .. } | ASTNode::QMarkPropagate { expression, .. } => { + ASTNode::AwaitExpression { expression, .. } + | ASTNode::QMarkPropagate { expression, .. } => { self.analyze_node(expression, current_scope, is_condition)?; } } @@ -462,14 +490,17 @@ impl AstOwnershipAnalyzer { Ok(()) } - fn record_assignment_target(&mut self, target: &ASTNode, current_scope: ScopeId) -> Result<(), String> { + fn record_assignment_target( + &mut self, + target: &ASTNode, + current_scope: ScopeId, + ) -> Result<(), String> { match target { ASTNode::Variable { name, .. } => { - self.scopes - .get_mut(¤t_scope) - .unwrap() - .writes - .insert(name.to_string()); + let binding = self.resolve_binding(name).ok_or_else(|| { + format!("AstOwnershipAnalyzer: write to undefined var '{}'", name) + })?; + self.record_write(binding, current_scope); } _ => { // For complex lvalues (field/index), conservatively treat subexpressions as reads. @@ -479,24 +510,11 @@ impl AstOwnershipAnalyzer { Ok(()) } - fn find_enclosing_loop_or_function(&self, scope_id: ScopeId) -> ScopeId { - let mut current = scope_id; - loop { - let scope = &self.scopes[¤t]; - if scope.kind == ScopeKind::Loop || scope.kind == ScopeKind::Function { - return current; - } - current = scope.parent.expect("Scope chain must have a Function root"); - } - } - fn propagate_to_parent(&mut self, child_id: ScopeId) { - let (parent_id, child_kind, child_defined, reads, writes, cond_reads) = { + let (parent_id, reads, writes, cond_reads) = { let child = &self.scopes[&child_id]; ( child.parent, - child.kind, - child.defined.clone(), child.reads.clone(), child.writes.clone(), child.condition_reads.clone(), @@ -505,17 +523,29 @@ impl AstOwnershipAnalyzer { if let Some(parent_id) = parent_id { let parent = self.scopes.get_mut(&parent_id).unwrap(); - parent.reads.extend(reads); - parent.condition_reads.extend(cond_reads); - - if child_kind == ScopeKind::Loop || child_kind == ScopeKind::Function { - for w in writes { - if !child_defined.contains(&w) { - parent.writes.insert(w); + for b in reads { + if let Some(info) = self.bindings.get(&b) { + if info.owner_scope == child_id { + continue; } } - } else { - parent.writes.extend(writes); + parent.reads.insert(b); + } + for b in writes { + if let Some(info) = self.bindings.get(&b) { + if info.owner_scope == child_id { + continue; + } + } + parent.writes.insert(b); + } + for b in cond_reads { + if let Some(info) = self.bindings.get(&b) { + if info.owner_scope == child_id { + continue; + } + } + parent.condition_reads.insert(b); } } } @@ -527,83 +557,103 @@ impl AstOwnershipAnalyzer { if scope.kind != ScopeKind::Loop && scope.kind != ScopeKind::Function { continue; } - - let mut plan = OwnershipPlan::new(scope.id); - - for name in &scope.defined { - let is_written = scope.writes.contains(name); - let is_condition_only = is_written && scope.condition_reads.contains(name); - plan.owned_vars.push(ScopeOwnedVar { - name: name.clone(), - is_written, - is_condition_only, - }); - } - - for name in &scope.writes { - if scope.defined.contains(name) { - continue; - } - if let Some((owner_scope, relay_path)) = self.find_owner(scope.id, name) { - plan.relay_writes.push(RelayVar { - name: name.clone(), - owner_scope, - relay_path, - }); - } else { - return Err(format!( - "AstOwnershipAnalyzer: relay write '{}' in scope {:?} has no owner", - name, scope.id - )); - } - } - - for name in &scope.reads { - if scope.defined.contains(name) || scope.writes.contains(name) { - continue; - } - if let Some((owner_scope, _)) = self.find_owner(scope.id, name) { - plan.captures.push(CapturedVar { - name: name.clone(), - owner_scope, - }); - } - } - - for cap in &plan.captures { - if scope.condition_reads.contains(&cap.name) { - plan.condition_captures.push(cap.clone()); - } - } - - #[cfg(debug_assertions)] - plan.verify_invariants()?; - - plans.push(plan); + plans.push(self.build_plan_for_scope(scope.id)?); } Ok(plans) } - fn find_owner(&self, from_scope: ScopeId, name: &str) -> Option<(ScopeId, Vec)> { + fn relay_path_to_owner( + &self, + from_scope: ScopeId, + owner_scope: ScopeId, + ) -> Result, String> { let mut current = from_scope; let mut path = Vec::new(); - loop { - let scope = &self.scopes[¤t]; - if scope.defined.contains(name) { - return Some((current, path)); - } - - if let Some(parent) = scope.parent { - if scope.kind == ScopeKind::Loop { - path.push(current); - } - current = parent; - } else { - return None; + while current != owner_scope { + let scope = self + .scopes + .get(¤t) + .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", current))?; + if scope.kind == ScopeKind::Loop { + path.push(current); } + current = scope.parent.ok_or_else(|| { + format!( + "AstOwnershipAnalyzer: no parent while searching relay path: from={:?} owner={:?}", + from_scope, owner_scope + ) + })?; } + + Ok(path) + } + + fn resolve_binding(&self, name: &str) -> Option { + self.env_stack + .iter() + .rev() + .find_map(|frame| frame.get(name).copied()) + } + + fn push_env(&mut self) { + self.env_stack.push(BTreeMap::new()); + } + + fn pop_env(&mut self) { + self.env_stack.pop(); + } + + fn declare_binding(&mut self, scope_id: ScopeId, name: &str) -> Result { + let Some(frame) = self.env_stack.last_mut() else { + return Err( + "AstOwnershipAnalyzer: internal error: declare_binding with empty env_stack" + .to_string(), + ); + }; + + if let Some(existing) = frame.get(name).copied() { + self.scopes + .get_mut(&scope_id) + .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", scope_id))? + .declared + .entry(name.to_string()) + .or_insert(existing); + return Ok(existing); + } + + let binding = BindingId(self.next_binding_id); + self.next_binding_id += 1; + + frame.insert(name.to_string(), binding); + self.bindings.insert( + binding, + BindingInfo { + name: name.to_string(), + owner_scope: scope_id, + }, + ); + self.scopes + .get_mut(&scope_id) + .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", scope_id))? + .declared + .insert(name.to_string(), binding); + + Ok(binding) + } + + fn record_read(&mut self, binding: BindingId, scope_id: ScopeId, is_condition: bool) { + let scope = self.scopes.get_mut(&scope_id).expect("scope must exist"); + scope.reads.insert(binding); + if is_condition { + scope.condition_reads.insert(binding); + } + } + + fn record_write(&mut self, binding: BindingId, scope_id: ScopeId) { + let scope = self.scopes.get_mut(&scope_id).expect("scope must exist"); + scope.writes.insert(binding); } } @@ -648,31 +698,31 @@ pub fn analyze_loop( // Create temporary function scope for parent context let parent_scope = analyzer.alloc_scope(ScopeKind::Function, None); + analyzer.push_env(); for var in parent_defined { - analyzer - .scopes - .get_mut(&parent_scope) - .unwrap() - .defined - .insert(var.clone()); + analyzer.declare_binding(parent_scope, var)?; } // Create loop scope let loop_scope = analyzer.alloc_scope(ScopeKind::Loop, Some(parent_scope)); - - // Analyze condition (with is_condition=true flag) - analyzer.analyze_node(condition, loop_scope, true)?; + analyzer.push_env(); // Analyze body statements for stmt in body { analyzer.analyze_node(stmt, loop_scope, false)?; } - // Propagate to parent + // Analyze condition (with is_condition=true flag) + analyzer.analyze_node(condition, loop_scope, true)?; + + analyzer.pop_env(); // loop scope + // Propagate to parent analyzer.propagate_to_parent(loop_scope); // Build plan for loop only - analyzer.build_plan_for_scope(loop_scope) + let plan = analyzer.build_plan_for_scope(loop_scope)?; + analyzer.pop_env(); // parent scope + Ok(plan) } impl AstOwnershipAnalyzer { @@ -690,9 +740,9 @@ impl AstOwnershipAnalyzer { let mut plan = OwnershipPlan::new(scope_id); // Collect owned vars - for name in &scope.defined { - let is_written = scope.writes.contains(name); - let is_condition_only = is_written && scope.condition_reads.contains(name); + for (name, binding) in &scope.declared { + let is_written = scope.writes.contains(binding); + let is_condition_only = is_written && scope.condition_reads.contains(binding); plan.owned_vars.push(ScopeOwnedVar { name: name.clone(), is_written, @@ -701,42 +751,42 @@ impl AstOwnershipAnalyzer { } // Collect relay writes - for name in &scope.writes { - if scope.defined.contains(name) { + for binding in &scope.writes { + let info = self + .bindings + .get(binding) + .ok_or_else(|| format!("AstOwnershipAnalyzer: unknown binding {:?}", binding))?; + if info.owner_scope == scope_id { continue; } - if let Some((owner_scope, relay_path)) = self.find_owner(scope_id, name) { - plan.relay_writes.push(RelayVar { - name: name.clone(), - owner_scope, - relay_path, - }); - } else { - return Err(format!( - "AstOwnershipAnalyzer: relay write '{}' in scope {:?} has no owner", - name, scope_id - )); - } + let relay_path = self.relay_path_to_owner(scope_id, info.owner_scope)?; + plan.relay_writes.push(RelayVar { + name: info.name.clone(), + owner_scope: info.owner_scope, + relay_path, + }); } // Collect captures - for name in &scope.reads { - if scope.defined.contains(name) || scope.writes.contains(name) { + for binding in &scope.reads { + if scope.writes.contains(binding) { continue; } - if let Some((owner_scope, _)) = self.find_owner(scope_id, name) { - plan.captures.push(CapturedVar { - name: name.clone(), - owner_scope, - }); + let info = self + .bindings + .get(binding) + .ok_or_else(|| format!("AstOwnershipAnalyzer: unknown binding {:?}", binding))?; + if info.owner_scope == scope_id { + continue; } - } - - // Collect condition captures - for cap in &plan.captures { - if scope.condition_reads.contains(&cap.name) { - plan.condition_captures.push(cap.clone()); + let captured = CapturedVar { + name: info.name.clone(), + owner_scope: info.owner_scope, + }; + if scope.condition_reads.contains(binding) { + plan.condition_captures.push(captured.clone()); } + plan.captures.push(captured); } #[cfg(debug_assertions)] @@ -772,6 +822,128 @@ mod tests { } } + #[test] + fn shadowing_inner_local_does_not_create_relay_write() { + // local sum=0 (parent) + // loop(true) { { local sum=1; sum=sum+1 } break } + // Expected: loop does NOT relay-write parent sum (inner sum shadows it). + let condition = lit_true(); + let body = vec![ + ASTNode::Program { + statements: vec![ + ASTNode::Local { + variables: vec!["sum".to_string()], + initial_values: vec![Some(Box::new(lit_i(1)))], + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("sum")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("sum")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ]; + + let plan = analyze_loop(&condition, &body, &["sum".to_string()]).unwrap(); + assert!( + !plan.relay_writes.iter().any(|r| r.name == "sum"), + "inner shadowed sum must not produce relay_writes: {:?}", + plan.relay_writes + ); + } + + #[test] + fn nested_block_local_is_not_a_loop_carrier() { + // loop(true) { { local x=0; x=x+1 } break } + // Expected: nested-block local 'x' does not appear in loop owned_vars. + let condition = lit_true(); + let body = vec![ + ASTNode::Program { + statements: vec![ + ASTNode::Local { + variables: vec!["x".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("x")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("x")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ]; + + let plan = analyze_loop(&condition, &body, &[]).unwrap(); + assert!( + !plan.owned_vars.iter().any(|v| v.name == "x"), + "nested-block local must not be owned by loop: {:?}", + plan.owned_vars + ); + assert!( + !plan.relay_writes.iter().any(|r| r.name == "x"), + "nested-block local must not be relayed by loop: {:?}", + plan.relay_writes + ); + assert!( + !plan.captures.iter().any(|c| c.name == "x"), + "nested-block local must not be captured by loop: {:?}", + plan.captures + ); + } + + #[test] + fn outer_update_is_relay_write_when_not_shadowed() { + // local sum=0 (parent) + // loop(true) { sum=sum+1; break } + // Expected: loop relays write to parent-owned sum. + let condition = lit_true(); + let body = vec![ + ASTNode::Assignment { + target: Box::new(var("sum")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("sum")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ]; + + let plan = analyze_loop(&condition, &body, &["sum".to_string()]).unwrap(); + let relay = plan + .relay_writes + .iter() + .find(|r| r.name == "sum") + .expect("expected relay write for sum"); + assert_ne!(relay.owner_scope, plan.scope_id); + assert_eq!(relay.relay_path.len(), 1); + assert_eq!(relay.relay_path[0], plan.scope_id); + } + #[test] fn loop_local_carrier_is_owned_and_written() { // function main() { @@ -798,7 +970,9 @@ mod tests { }), span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }], @@ -856,7 +1030,9 @@ mod tests { }), span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }, @@ -907,7 +1083,9 @@ mod tests { }), span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }, @@ -985,15 +1163,21 @@ mod tests { }), span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }, - ASTNode::Break { span: Span::unknown() }, + ASTNode::Break { + span: Span::unknown(), + }, ], span: Span::unknown(), }, @@ -1048,9 +1232,6 @@ mod tests { ); // Verify: owner is L1 - assert_eq!( - relay.owner_scope, l1_scope_id, - "owner_scope must be L1" - ); + assert_eq!(relay.owner_scope, l1_scope_id, "owner_scope must be L1"); } }