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:
nyash-codex
2025-12-12 22:51:21 +09:00
parent acb6720d9b
commit 0ff96612cf
6 changed files with 951 additions and 1 deletions

View 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(&current_scope)
.unwrap()
.writes
.insert(lhs.to_string());
self.analyze_node(rhs, current_scope, is_condition)?;
}
ASTNode::Nowait {
variable,
expression,
..
} => {
self.scopes
.get_mut(&current_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(&current_scope)
.unwrap()
.reads
.insert(name.to_string());
if is_condition {
self.scopes
.get_mut(&current_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(&current_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[&current];
if scope.kind == ScopeKind::Loop || scope.kind == ScopeKind::Function {
return current;
}
current = scope.parent.expect("Scope chain must have a Function root");
}
}
fn propagate_to_parent(&mut self, child_id: ScopeId) {
let (parent_id, child_kind, child_defined, reads, writes, cond_reads) = {
let 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[&current];
if scope.defined.contains(name) {
return Some((current, path));
}
if let Some(parent) = scope.parent {
if scope.kind == ScopeKind::Loop {
path.push(current);
}
current = parent;
} else {
return None;
}
}
}
}
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);
}
}

View File

@ -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::*;