joinir(ownership): shadowing-aware AST ownership analyzer
This commit is contained in:
@ -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(¤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<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[¤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<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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user