joinir(ownership): shadowing-aware AST ownership analyzer

This commit is contained in:
nyash-codex
2025-12-13 02:04:04 +09:00
parent 0913ee8bbc
commit 795d68ecf1

View File

@ -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<ScopeId>,
defined: BTreeSet<String>,
reads: BTreeSet<String>,
writes: BTreeSet<String>,
condition_reads: BTreeSet<String>,
declared: BTreeMap<String, BindingId>,
reads: BTreeSet<BindingId>,
writes: BTreeSet<BindingId>,
condition_reads: BTreeSet<BindingId>,
}
/// 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<ScopeId, ScopeInfo>,
bindings: BTreeMap<BindingId, BindingInfo>,
next_scope_id: u32,
next_binding_id: u32,
env_stack: Vec<BTreeMap<String, BindingId>>,
}
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<Vec<OwnershipPlan>, 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<ScopeId>) -> Result<ScopeId, String> {
fn analyze_function_decl(
&mut self,
node: &ASTNode,
parent: Option<ScopeId>,
) -> Result<ScopeId, String> {
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(&current_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(&current_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(&current_scope)
.unwrap()
.reads
.insert(name.to_string());
if is_condition {
self.scopes
.get_mut(&current_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(&current_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[&current];
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<ScopeId>)> {
fn relay_path_to_owner(
&self,
from_scope: ScopeId,
owner_scope: ScopeId,
) -> Result<Vec<ScopeId>, String> {
let mut current = from_scope;
let mut path = Vec::new();
loop {
let scope = &self.scopes[&current];
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(&current)
.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<BindingId> {
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<BindingId, String> {
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");
}
}