ownership: split ast_analyzer into modules

This commit is contained in:
2025-12-27 16:24:51 +09:00
parent e6aeccb793
commit ad469ec2ff
6 changed files with 970 additions and 1237 deletions

File diff suppressed because it is too large Load Diff

View 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(&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);
}
}
impl Default for AstOwnershipAnalyzer {
fn default() -> Self {
Self::new()
}
}

View 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)
}
}

View 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;

View 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(())
}
}

View 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
);
}