ownership: split ast_analyzer into modules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
292
src/mir/join_ir/ownership/ast_analyzer/core.rs
Normal file
292
src/mir/join_ir/ownership/ast_analyzer/core.rs
Normal file
@ -0,0 +1,292 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::ownership::{OwnershipPlan, ScopeId, ScopeKind};
|
||||
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>,
|
||||
declared: BTreeMap<String, BindingId>,
|
||||
reads: BTreeSet<BindingId>,
|
||||
writes: BTreeSet<BindingId>,
|
||||
condition_reads: BTreeSet<BindingId>,
|
||||
}
|
||||
|
||||
/// Analyzes real AST and produces `OwnershipPlan`.
|
||||
///
|
||||
/// This analyzer:
|
||||
/// - Treats `ASTNode::Local` as "definition" (no JSON v0 rebind hack)
|
||||
/// - Records writes via `ASTNode::Assignment` / `ASTNode::GroupedAssignmentExpr`
|
||||
/// - 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, .. } => {
|
||||
for stmt in statements {
|
||||
self.analyze_toplevel(stmt, None)?;
|
||||
}
|
||||
}
|
||||
ASTNode::FunctionDeclaration { .. } => {
|
||||
self.analyze_function_decl(ast, None)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
"AstOwnershipAnalyzer: expected Program or FunctionDeclaration".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.build_plans()
|
||||
}
|
||||
|
||||
fn analyze_toplevel(&mut self, node: &ASTNode, parent: Option<ScopeId>) -> Result<(), String> {
|
||||
match node {
|
||||
ASTNode::FunctionDeclaration { .. } => {
|
||||
self.analyze_function_decl(node, parent)?;
|
||||
}
|
||||
ASTNode::BoxDeclaration {
|
||||
methods,
|
||||
constructors,
|
||||
..
|
||||
} => {
|
||||
for (_, f) in methods {
|
||||
self.analyze_function_decl(f, parent)?;
|
||||
}
|
||||
for (_, f) in constructors {
|
||||
self.analyze_function_decl(f, parent)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn alloc_scope(&mut self, kind: ScopeKind, parent: Option<ScopeId>) -> ScopeId {
|
||||
let id = ScopeId(self.next_scope_id);
|
||||
self.next_scope_id += 1;
|
||||
self.scopes.insert(
|
||||
id,
|
||||
ScopeInfo {
|
||||
id,
|
||||
kind,
|
||||
parent,
|
||||
declared: BTreeMap::new(),
|
||||
reads: BTreeSet::new(),
|
||||
writes: BTreeSet::new(),
|
||||
condition_reads: BTreeSet::new(),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
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.declare_binding(scope_id, name)?;
|
||||
}
|
||||
|
||||
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 propagate_to_parent(&mut self, child_id: ScopeId) {
|
||||
let (parent_id, reads, writes, cond_reads) = {
|
||||
let child = &self.scopes[&child_id];
|
||||
(
|
||||
child.parent,
|
||||
child.reads.clone(),
|
||||
child.writes.clone(),
|
||||
child.condition_reads.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(parent_id) = parent_id {
|
||||
let parent = self.scopes.get_mut(&parent_id).unwrap();
|
||||
for b in reads {
|
||||
if let Some(info) = self.bindings.get(&b) {
|
||||
if info.owner_scope == child_id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_plans(&self) -> Result<Vec<OwnershipPlan>, String> {
|
||||
let mut plans = Vec::new();
|
||||
|
||||
for (_, scope) in &self.scopes {
|
||||
if scope.kind != ScopeKind::Loop && scope.kind != ScopeKind::Function {
|
||||
continue;
|
||||
}
|
||||
plans.push(self.build_plan_for_scope(scope.id)?);
|
||||
}
|
||||
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AstOwnershipAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
139
src/mir/join_ir/ownership/ast_analyzer/loop_helper.rs
Normal file
139
src/mir/join_ir/ownership/ast_analyzer/loop_helper.rs
Normal file
@ -0,0 +1,139 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::ownership::{
|
||||
CapturedVar, OwnershipPlan, RelayVar, ScopeId, ScopeKind, ScopeOwnedVar,
|
||||
};
|
||||
|
||||
use super::AstOwnershipAnalyzer;
|
||||
|
||||
/// Phase 64: Analyze a single loop (condition + body) with parent context.
|
||||
///
|
||||
/// This helper is designed for P3 production integration. It creates a temporary
|
||||
/// function scope for parent-defined variables, then analyzes the loop scope.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `condition` - Loop condition AST node
|
||||
/// * `body` - Loop body statements
|
||||
/// * `parent_defined` - Variables defined in parent scope (function params/locals)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// OwnershipPlan for the loop scope only (not the temporary function scope).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // loop(i < 10) { local sum=0; sum=sum+1; i=i+1; }
|
||||
/// let condition = /* i < 10 */;
|
||||
/// let body = vec![/* local sum=0; sum=sum+1; i=i+1; */];
|
||||
/// let parent_defined = vec![];
|
||||
/// let plan = analyze_loop(&condition, &body, &parent_defined)?;
|
||||
/// // plan.owned_vars contains: sum (is_written=true), i (is_written=true)
|
||||
/// ```
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
pub fn analyze_loop(
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
parent_defined: &[String],
|
||||
) -> Result<OwnershipPlan, String> {
|
||||
let mut analyzer = AstOwnershipAnalyzer::new();
|
||||
|
||||
// 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.declare_binding(parent_scope, var)?;
|
||||
}
|
||||
|
||||
// Create loop scope
|
||||
let loop_scope = analyzer.alloc_scope(ScopeKind::Loop, Some(parent_scope));
|
||||
analyzer.push_env();
|
||||
|
||||
// Analyze body statements
|
||||
for stmt in body {
|
||||
analyzer.analyze_node(stmt, loop_scope, false)?;
|
||||
}
|
||||
|
||||
// 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
|
||||
let plan = analyzer.build_plan_for_scope(loop_scope)?;
|
||||
analyzer.pop_env(); // parent scope
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
impl AstOwnershipAnalyzer {
|
||||
/// Phase 64: Build OwnershipPlan for a specific scope (helper for analyze_loop).
|
||||
///
|
||||
/// This is a private helper used by `analyze_loop()` to extract a single
|
||||
/// scope's OwnershipPlan without building plans for all scopes.
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn build_plan_for_scope(&self, scope_id: ScopeId) -> Result<OwnershipPlan, String> {
|
||||
let scope = self
|
||||
.scopes
|
||||
.get(&scope_id)
|
||||
.ok_or_else(|| format!("Scope {:?} not found", scope_id))?;
|
||||
|
||||
let mut plan = OwnershipPlan::new(scope_id);
|
||||
|
||||
// Collect owned vars
|
||||
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,
|
||||
is_condition_only,
|
||||
});
|
||||
}
|
||||
|
||||
// Collect relay writes
|
||||
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;
|
||||
}
|
||||
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 binding in &scope.reads {
|
||||
if scope.writes.contains(binding) {
|
||||
continue;
|
||||
}
|
||||
let info = self
|
||||
.bindings
|
||||
.get(binding)
|
||||
.ok_or_else(|| format!("AstOwnershipAnalyzer: unknown binding {:?}", binding))?;
|
||||
if info.owner_scope == scope_id {
|
||||
continue;
|
||||
}
|
||||
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)]
|
||||
plan.verify_invariants()?;
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
14
src/mir/join_ir/ownership/ast_analyzer/mod.rs
Normal file
14
src/mir/join_ir/ownership/ast_analyzer/mod.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//! Ownership Analyzer for real AST (`crate::ast::ASTNode`)
|
||||
//!
|
||||
//! Phase 63: analysis-only (dev-only via `normalized_dev` feature).
|
||||
|
||||
mod core;
|
||||
mod node_analysis;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
mod loop_helper;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use core::AstOwnershipAnalyzer;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
pub use loop_helper::analyze_loop;
|
||||
371
src/mir/join_ir/ownership/ast_analyzer/node_analysis.rs
Normal file
371
src/mir/join_ir/ownership/ast_analyzer/node_analysis.rs
Normal file
@ -0,0 +1,371 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::ownership::ScopeKind;
|
||||
|
||||
use super::AstOwnershipAnalyzer;
|
||||
|
||||
impl AstOwnershipAnalyzer {
|
||||
fn analyze_node(
|
||||
&mut self,
|
||||
node: &ASTNode,
|
||||
current_scope: crate::mir::join_ir::ownership::ScopeId,
|
||||
is_condition: bool,
|
||||
) -> Result<(), String> {
|
||||
match node {
|
||||
ASTNode::Program { statements, .. } => {
|
||||
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 { .. } => {
|
||||
self.analyze_function_decl(node, Some(current_scope))?;
|
||||
}
|
||||
|
||||
ASTNode::BoxDeclaration {
|
||||
methods,
|
||||
constructors,
|
||||
..
|
||||
} => {
|
||||
for (_, f) in methods {
|
||||
self.analyze_function_decl(f, Some(current_scope))?;
|
||||
}
|
||||
for (_, f) in constructors {
|
||||
self.analyze_function_decl(f, Some(current_scope))?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Local {
|
||||
variables,
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Outbox {
|
||||
variables,
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Assignment { target, value, .. } => {
|
||||
self.record_assignment_target(target, current_scope)?;
|
||||
self.analyze_node(value, current_scope, false)?;
|
||||
}
|
||||
|
||||
ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => {
|
||||
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)?;
|
||||
}
|
||||
|
||||
ASTNode::Nowait {
|
||||
variable,
|
||||
expression,
|
||||
..
|
||||
} => {
|
||||
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)?;
|
||||
}
|
||||
|
||||
ASTNode::Print { expression, .. } => {
|
||||
self.analyze_node(expression, current_scope, false)?;
|
||||
}
|
||||
|
||||
ASTNode::Return { value, .. } => {
|
||||
if let Some(v) = value.as_ref() {
|
||||
self.analyze_node(v, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Break { .. } | ASTNode::Continue { .. } => {}
|
||||
|
||||
ASTNode::If {
|
||||
condition,
|
||||
then_body,
|
||||
else_body,
|
||||
..
|
||||
} => {
|
||||
let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope));
|
||||
self.analyze_node(condition, if_scope, true)?;
|
||||
|
||||
let then_scope = self.alloc_scope(ScopeKind::Block, Some(if_scope));
|
||||
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));
|
||||
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, ..
|
||||
} => {
|
||||
let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_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);
|
||||
}
|
||||
|
||||
ASTNode::ForRange {
|
||||
var_name,
|
||||
start,
|
||||
end,
|
||||
body,
|
||||
..
|
||||
} => {
|
||||
let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_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));
|
||||
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);
|
||||
}
|
||||
|
||||
ASTNode::TryCatch {
|
||||
try_body,
|
||||
catch_clauses,
|
||||
finally_body,
|
||||
..
|
||||
} => {
|
||||
let try_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
|
||||
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.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));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Throw { expression, .. } => {
|
||||
self.analyze_node(expression, current_scope, false)?;
|
||||
}
|
||||
|
||||
ASTNode::UsingStatement { .. } | ASTNode::ImportStatement { .. } => {}
|
||||
|
||||
ASTNode::GlobalVar { value, .. } => {
|
||||
self.analyze_node(value, current_scope, false)?;
|
||||
}
|
||||
|
||||
ASTNode::Literal { .. }
|
||||
| ASTNode::This { .. }
|
||||
| ASTNode::Me { .. }
|
||||
| ASTNode::ThisField { .. }
|
||||
| ASTNode::MeField { .. } => {}
|
||||
|
||||
ASTNode::Variable { name, .. } => {
|
||||
if let Some(binding) = self.resolve_binding(name) {
|
||||
self.record_read(binding, current_scope, is_condition);
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::UnaryOp { operand, .. } => {
|
||||
self.analyze_node(operand, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::BinaryOp { left, right, .. } => {
|
||||
self.analyze_node(left, current_scope, is_condition)?;
|
||||
self.analyze_node(right, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::MethodCall {
|
||||
object, arguments, ..
|
||||
} => {
|
||||
self.analyze_node(object, current_scope, is_condition)?;
|
||||
for a in arguments {
|
||||
self.analyze_node(a, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::FieldAccess { object, .. } => {
|
||||
self.analyze_node(object, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::Index { target, index, .. } => {
|
||||
self.analyze_node(target, current_scope, is_condition)?;
|
||||
self.analyze_node(index, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::New { arguments, .. } => {
|
||||
for a in arguments {
|
||||
self.analyze_node(a, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::FromCall { arguments, .. } => {
|
||||
for a in arguments {
|
||||
self.analyze_node(a, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::FunctionCall { arguments, .. } => {
|
||||
for a in arguments {
|
||||
self.analyze_node(a, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Call {
|
||||
callee, arguments, ..
|
||||
} => {
|
||||
self.analyze_node(callee, current_scope, is_condition)?;
|
||||
for a in arguments {
|
||||
self.analyze_node(a, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::ArrayLiteral { elements, .. } => {
|
||||
for e in elements {
|
||||
self.analyze_node(e, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::MapLiteral { entries, .. } => {
|
||||
for (_, v) in entries {
|
||||
self.analyze_node(v, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::MatchExpr {
|
||||
scrutinee,
|
||||
arms,
|
||||
else_expr,
|
||||
..
|
||||
} => {
|
||||
self.analyze_node(scrutinee, current_scope, is_condition)?;
|
||||
for (_, e) in arms {
|
||||
self.analyze_node(e, current_scope, is_condition)?;
|
||||
}
|
||||
self.analyze_node(else_expr, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::Lambda { .. } => {}
|
||||
|
||||
ASTNode::Arrow {
|
||||
sender, receiver, ..
|
||||
} => {
|
||||
self.analyze_node(sender, current_scope, is_condition)?;
|
||||
self.analyze_node(receiver, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::AwaitExpression { expression, .. }
|
||||
| ASTNode::QMarkPropagate { expression, .. } => {
|
||||
self.analyze_node(expression, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_assignment_target(
|
||||
&mut self,
|
||||
target: &ASTNode,
|
||||
current_scope: crate::mir::join_ir::ownership::ScopeId,
|
||||
) -> Result<(), String> {
|
||||
match target {
|
||||
ASTNode::Variable { name, .. } => {
|
||||
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.
|
||||
self.analyze_node(target, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
154
src/mir/join_ir/ownership/ast_analyzer/tests.rs
Normal file
154
src/mir/join_ir/ownership/ast_analyzer/tests.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use super::*;
|
||||
use crate::ast::{BinaryOperator, LiteralValue, Span};
|
||||
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(i),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lit_true() -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Bool(true),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
#[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 writes_to_parent_are_relayed() {
|
||||
// local sum=0 (parent)
|
||||
// loop(true) { sum = sum + 1; break }
|
||||
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();
|
||||
assert!(
|
||||
plan.relay_writes.iter().any(|r| r.name == "sum"),
|
||||
"expected relay_writes to contain parent sum: {:?}",
|
||||
plan.relay_writes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn condition_reads_are_marked() {
|
||||
// loop(i < n) { break }
|
||||
let condition = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(var("i")),
|
||||
right: Box::new(var("n")),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let body = vec![ASTNode::Break {
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let plan = analyze_loop(
|
||||
&condition,
|
||||
&body,
|
||||
&["i".to_string(), "n".to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let names: Vec<_> = plan
|
||||
.condition_captures
|
||||
.iter()
|
||||
.map(|c| c.name.as_str())
|
||||
.collect();
|
||||
assert!(names.contains(&"i"));
|
||||
assert!(names.contains(&"n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_local_written_is_owned_var() {
|
||||
// loop(true) { local tmp = 0; tmp = tmp + 1; break }
|
||||
let condition = lit_true();
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["tmp".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(var("tmp")),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(var("tmp")),
|
||||
right: Box::new(lit_i(1)),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Break {
|
||||
span: Span::unknown(),
|
||||
},
|
||||
];
|
||||
|
||||
let plan = analyze_loop(&condition, &body, &[]).unwrap();
|
||||
assert!(
|
||||
plan.owned_vars.iter().any(|o| o.name == "tmp"),
|
||||
"tmp should be owned in loop scope: {:?}",
|
||||
plan.owned_vars
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user