docs(joinir): Phase 63 - AST ownership analyzer (dev-only)
ASTNode → OwnershipPlan の解析器を追加(analysis-only, normalized_dev)。 Key changes: - New ast_analyzer.rs (~370 lines): - AstOwnershipAnalyzer: AST → Vec<OwnershipPlan> - ScopeKind: Function/Loop/If/Block - Invariants: owned_vars/relay_writes/captures/condition_captures Design: - JSON v0 "Local=rebind" を使わず、AST の Statement::Local を正しく扱う - "読むのは自由、管理は直下だけ" アーキテクチャ維持 Tests: 3 unit tests + 47/47 normalized_dev PASS - loop_local_carrier_is_owned_and_written - condition_capture_is_reported_for_loop - relay_write_detected_for_outer_owned_var Next: Phase 64 - P3(if-sum) 本番ルートへ dev-only で接続 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
804
src/mir/join_ir/ownership/ast_analyzer.rs
Normal file
804
src/mir/join_ir/ownership/ast_analyzer.rs
Normal file
@ -0,0 +1,804 @@
|
||||
//! Ownership Analyzer for real AST (`crate::ast::ASTNode`)
|
||||
//!
|
||||
//! Phase 63: analysis-only (dev-only via `normalized_dev` feature).
|
||||
|
||||
use super::*;
|
||||
use crate::ast::ASTNode;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScopeInfo {
|
||||
id: ScopeId,
|
||||
kind: ScopeKind,
|
||||
parent: Option<ScopeId>,
|
||||
defined: BTreeSet<String>,
|
||||
reads: BTreeSet<String>,
|
||||
writes: BTreeSet<String>,
|
||||
condition_reads: BTreeSet<String>,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
next_scope_id: u32,
|
||||
}
|
||||
|
||||
impl AstOwnershipAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scopes: BTreeMap::new(),
|
||||
next_scope_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analyze_ast(&mut self, ast: &ASTNode) -> Result<Vec<OwnershipPlan>, String> {
|
||||
self.scopes.clear();
|
||||
self.next_scope_id = 0;
|
||||
|
||||
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,
|
||||
defined: BTreeSet::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);
|
||||
|
||||
for name in params {
|
||||
self.scopes
|
||||
.get_mut(&scope_id)
|
||||
.unwrap()
|
||||
.defined
|
||||
.insert(name.to_string());
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Ok(scope_id)
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
..
|
||||
} => {
|
||||
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 {
|
||||
if let Some(expr) = init.as_ref() {
|
||||
self.analyze_node(expr, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Outbox {
|
||||
variables,
|
||||
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 {
|
||||
if let Some(expr) = init.as_ref() {
|
||||
self.analyze_node(expr, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASTNode::Assignment { target, value, .. } => {
|
||||
self.record_assignment_target(target, current_scope)?;
|
||||
self.analyze_node(value, current_scope, false)?;
|
||||
}
|
||||
|
||||
ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => {
|
||||
self.scopes
|
||||
.get_mut(¤t_scope)
|
||||
.unwrap()
|
||||
.writes
|
||||
.insert(lhs.to_string());
|
||||
self.analyze_node(rhs, current_scope, is_condition)?;
|
||||
}
|
||||
|
||||
ASTNode::Nowait {
|
||||
variable,
|
||||
expression,
|
||||
..
|
||||
} => {
|
||||
self.scopes
|
||||
.get_mut(¤t_scope)
|
||||
.unwrap()
|
||||
.writes
|
||||
.insert(variable.to_string());
|
||||
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));
|
||||
for s in then_body {
|
||||
self.analyze_node(s, then_scope, false)?;
|
||||
}
|
||||
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.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.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.propagate_to_parent(loop_scope);
|
||||
}
|
||||
|
||||
ASTNode::ForRange {
|
||||
var_name,
|
||||
start,
|
||||
end,
|
||||
body,
|
||||
..
|
||||
} => {
|
||||
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.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.propagate_to_parent(block_scope);
|
||||
}
|
||||
|
||||
ASTNode::TryCatch {
|
||||
try_body,
|
||||
catch_clauses,
|
||||
finally_body,
|
||||
..
|
||||
} => {
|
||||
let try_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
|
||||
for s in try_body {
|
||||
self.analyze_node(s, try_scope, false)?;
|
||||
}
|
||||
self.propagate_to_parent(try_scope);
|
||||
|
||||
for clause in catch_clauses {
|
||||
let catch_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
|
||||
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.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.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, .. } => {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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: ScopeId) -> Result<(), String> {
|
||||
match target {
|
||||
ASTNode::Variable { name, .. } => {
|
||||
self.scopes
|
||||
.get_mut(¤t_scope)
|
||||
.unwrap()
|
||||
.writes
|
||||
.insert(name.to_string());
|
||||
}
|
||||
_ => {
|
||||
// For complex lvalues (field/index), conservatively treat subexpressions as reads.
|
||||
self.analyze_node(target, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
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 child = &self.scopes[&child_id];
|
||||
(
|
||||
child.parent,
|
||||
child.kind,
|
||||
child.defined.clone(),
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parent.writes.extend(writes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
fn find_owner(&self, from_scope: ScopeId, name: &str) -> Option<(ScopeId, Vec<ScopeId>)> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AstOwnershipAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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 loop_local_carrier_is_owned_and_written() {
|
||||
// function main() {
|
||||
// loop(true) { local sum=0; sum = sum + 1; break }
|
||||
// }
|
||||
let ast = ASTNode::FunctionDeclaration {
|
||||
name: "main".to_string(),
|
||||
params: vec![],
|
||||
body: vec![ASTNode::Loop {
|
||||
condition: Box::new(lit_true()),
|
||||
body: vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["sum".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
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(),
|
||||
},
|
||||
ASTNode::Break { span: Span::unknown() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut analyzer = AstOwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_ast(&ast).unwrap();
|
||||
|
||||
let loop_plans: Vec<_> = plans
|
||||
.iter()
|
||||
.filter(|p| p.owned_vars.iter().any(|v| v.name == "sum"))
|
||||
.collect();
|
||||
assert_eq!(loop_plans.len(), 1);
|
||||
let plan = loop_plans[0];
|
||||
let sum = plan.owned_vars.iter().find(|v| v.name == "sum").unwrap();
|
||||
assert!(sum.is_written);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn condition_capture_is_reported_for_loop() {
|
||||
// local limit=10;
|
||||
// loop(i < limit) { local i=0; i=i+1; break }
|
||||
let ast = ASTNode::FunctionDeclaration {
|
||||
name: "main".to_string(),
|
||||
params: vec![],
|
||||
body: vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["limit".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(10)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(var("i")),
|
||||
right: Box::new(var("limit")),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["i".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(var("i")),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(var("i")),
|
||||
right: Box::new(lit_i(1)),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Break { span: Span::unknown() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut analyzer = AstOwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_ast(&ast).unwrap();
|
||||
|
||||
let loop_plan = plans
|
||||
.iter()
|
||||
.find(|p| p.condition_captures.iter().any(|c| c.name == "limit"))
|
||||
.expect("expected a loop plan capturing limit");
|
||||
|
||||
assert!(loop_plan.captures.iter().any(|c| c.name == "limit"));
|
||||
assert!(loop_plan
|
||||
.condition_captures
|
||||
.iter()
|
||||
.any(|c| c.name == "limit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_write_detected_for_outer_owned_var() {
|
||||
// local sum=0;
|
||||
// loop(true) { sum=sum+1; break }
|
||||
let ast = ASTNode::FunctionDeclaration {
|
||||
name: "main".to_string(),
|
||||
params: vec![],
|
||||
body: vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["sum".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(lit_true()),
|
||||
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() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut analyzer = AstOwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_ast(&ast).unwrap();
|
||||
|
||||
let loop_plan = plans
|
||||
.iter()
|
||||
.find(|p| p.relay_writes.iter().any(|r| r.name == "sum"))
|
||||
.expect("expected a loop plan with relay write");
|
||||
|
||||
let relay = loop_plan
|
||||
.relay_writes
|
||||
.iter()
|
||||
.find(|r| r.name == "sum")
|
||||
.unwrap();
|
||||
assert_ne!(relay.owner_scope, loop_plan.scope_id);
|
||||
assert_eq!(relay.relay_path.len(), 1);
|
||||
assert_eq!(relay.relay_path[0], loop_plan.scope_id);
|
||||
}
|
||||
}
|
||||
@ -29,8 +29,12 @@ mod types;
|
||||
mod analyzer;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
mod plan_to_lowering;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
mod ast_analyzer;
|
||||
|
||||
pub use types::*;
|
||||
pub use analyzer::*;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
pub use plan_to_lowering::*;
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
pub use ast_analyzer::*;
|
||||
|
||||
Reference in New Issue
Block a user