From 0ff96612cf3f65ba5529e20123f03d0a58befe99 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Fri, 12 Dec 2025 22:51:21 +0900 Subject: [PATCH] docs(joinir): Phase 63 - AST ownership analyzer (dev-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ASTNode → OwnershipPlan の解析器を追加(analysis-only, normalized_dev)。 Key changes: - New ast_analyzer.rs (~370 lines): - AstOwnershipAnalyzer: AST → Vec - 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 --- CURRENT_TASK.md | 10 +- .../current/main/PHASE_61_SUMMARY.md | 1 + .../current/main/PHASE_63_SUMMARY.md | 28 + .../main/phase62-ownership-p3-route-design.md | 105 +++ src/mir/join_ir/ownership/ast_analyzer.rs | 804 ++++++++++++++++++ src/mir/join_ir/ownership/mod.rs | 4 + 6 files changed, 951 insertions(+), 1 deletion(-) create mode 100644 docs/development/current/main/PHASE_63_SUMMARY.md create mode 100644 docs/development/current/main/phase62-ownership-p3-route-design.md create mode 100644 src/mir/join_ir/ownership/ast_analyzer.rs diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index f9f93d57..982ab0e9 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -254,7 +254,15 @@ 19. **Phase 62-OWNERSHIP-P3-ROUTE-DESIGN(次のフォーカス候補)**: P3 本番ルートへ OwnershipPlan を渡す設計 - MIR→JoinIR の `pattern3_with_if_phi.rs` は OwnershipPlan を受け取らないため、AST-based ownership 解析の接続点を設計する。 - dev-only で段階接続し、legacy と stdout/exit 一致の比較で回帰を固定(既定挙動は不変)。 -20. JoinIR Verify / 最適化まわり + - 設計詳細: [phase62-ownership-p3-route-design.md](docs/development/current/main/phase62-ownership-p3-route-design.md) +20. **Phase 63-OWNERSHIP-AST-ANALYZER(完了✅ 2025-12-12)**: 本番 AST から OwnershipPlan を生成(dev-only) + - `AstOwnershipAnalyzer` を追加し、ASTNode から owned/relay/capture を plan 化(analysis-only)。 + - JSON v0 の “Local=rebind” ハックを排除(fixture 専用のまま)。 + - 詳細: [PHASE_63_SUMMARY.md](docs/development/current/main/PHASE_63_SUMMARY.md) +21. **Phase 64-OWNERSHIP-P3-PROD-PLUMB(次のフォーカス候補)**: 本番 P3(if-sum) ルートへ段階接続(dev-only) + - `pattern3_with_if_phi.rs` で OwnershipPlan を導入し、carrier set/inputs を SSOT 化する(order は exit_meta と整合チェックで段階移行)。 + - Fail-Fast: multi-hop relay / carrier set 不一致 / owner 不在 write を拒否。 +22. JoinIR Verify / 最適化まわり - すでに PHI/ValueId 契約は debug ビルドで検証しているので、 必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。 diff --git a/docs/development/current/main/PHASE_61_SUMMARY.md b/docs/development/current/main/PHASE_61_SUMMARY.md index f787eff9..d58a8cda 100644 --- a/docs/development/current/main/PHASE_61_SUMMARY.md +++ b/docs/development/current/main/PHASE_61_SUMMARY.md @@ -53,4 +53,5 @@ Phase 61 は「Break(P2) に P3 固有ロジックを混ぜない」を達成し ## Next Candidates - Phase 62: P3(if-sum) の「本番 MIR→JoinIR ルート」へ OwnershipPlan を渡す設計(AST-based ownership 解析の接続点を設計 → dev-only で段階接続)。 +- Phase 63: ASTNode → OwnershipPlan の analyzer を追加(analysis-only, dev-only)。 - Phase 63+: multi-hop / merge relay の意味論設計(Fail-Fast を解除する前に SSOT と不変条件を明文化)。 diff --git a/docs/development/current/main/PHASE_63_SUMMARY.md b/docs/development/current/main/PHASE_63_SUMMARY.md new file mode 100644 index 00000000..aee43f43 --- /dev/null +++ b/docs/development/current/main/PHASE_63_SUMMARY.md @@ -0,0 +1,28 @@ +# Phase 63 Summary: Ownership AST Analyzer (dev-only) + +## Goal + +本番ルート(MIR builder)の入力である `crate::ast::ASTNode` から、Ownership-Relay の解析結果(`OwnershipPlan`)を生成できるようにする。 + +- JSON v0 専用の「Local=rebind」扱いは使わない +- `normalized_dev` feature 下でのみ有効(analysis-only) + +## Changes + +- 追加: `src/mir/join_ir/ownership/ast_analyzer.rs` + - `AstOwnershipAnalyzer` を実装(AST → `Vec`) + - ScopeKind: Function / Loop / If / Block + - 不変条件: `owned_vars / relay_writes / captures / condition_captures` を `OwnershipPlan` として出力 +- 導線: `src/mir/join_ir/ownership/mod.rs` + - `#[cfg(feature = "normalized_dev")] pub use ast_analyzer::*;` を追加 + +## Tests + +- `src/mir/join_ir/ownership/ast_analyzer.rs` 内にユニットテストを追加: + - loop-local owned + write(carrier 相当) + - condition capture(condition_captures ⊆ captures) + - relay write(外側 owned への更新) + +## Notes + +- Phase 64 で、本番 P3(if-sum) ルート(`pattern3_with_if_phi.rs`)へ dev-only で接続し、carrier order / boundary inputs の SSOT 化を開始する。 diff --git a/docs/development/current/main/phase62-ownership-p3-route-design.md b/docs/development/current/main/phase62-ownership-p3-route-design.md new file mode 100644 index 00000000..fb21741f --- /dev/null +++ b/docs/development/current/main/phase62-ownership-p3-route-design.md @@ -0,0 +1,105 @@ +# Phase 62: Ownership → P3 (if-sum) Production Route Design (dev-only) + +## Goal + +本番の MIR→JoinIR ルート(`src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs`)に対して、 +Ownership-Relay の契約(owned / captures / relay_writes)を SSOT として差し込み、 +P3(if-sum) の boundary inputs(host_inputs / join_inputs)と carrier set を「推測」から「解析結果」へ移行する。 + +前提: 既定挙動は不変。`normalized_dev` feature + dev 実行ガード下で段階接続する。 + +## Context + +- Phase 56–60: OwnershipPlan / relay / plan_to_lowering を整備し、fixtures ルートで段階的に接続済み。 +- Phase 61: Break(P2) への by-name 混入を撤去し、if-sum+break は別箱で構造的に導入した。 +- Phase 63: 本番 AST(`ASTNode`)から OwnershipPlan を生成できる `AstOwnershipAnalyzer` を追加した(dev-only, analysis-only)。 + +本番の P3(if-sum) lowering は、OwnershipPlan を受け取らず、carrier set/inputs が解析 SSOT で固定されていない。 + +## Scope (This Phase) + +このフェーズは **設計のみ**(SSOT 文書)。コード変更は Phase 64 で行う。 + +### In scope + +- 本番 P3(if-sum) ルートで OwnershipPlan を導入する「接続点」の決定 +- OwnershipPlan と既存コンポーネント(ConditionEnv / ExitMeta / CarrierInfo)の責務境界を明文化 +- dev-only の段階接続計画(Fail-Fast 条件/回帰テスト方針)を決める + +### Out of scope + +- multi-hop relay / merge relay(別フェーズで意味論設計が必要) +- 新しい言語仕様・パターン拡張 +- 既定挙動変更(canonical への昇格など) + +## Current Production Route (P3 if-sum) + +入口: `src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs` + +現状の流れ(簡略): + +1. `build_pattern_context(...)` で `PatternPipelineContext` を構築 +2. `ctx.is_if_sum_pattern()` のとき `lower_pattern3_if_sum(...)` を実行 +3. `ConditionEnvBuilder` で `ConditionEnv + condition_bindings` を構築 +4. `lower_if_sum_pattern(...)` が JoinIR を生成し、`fragment_meta.exit_meta` を返す +5. `ExitMetaCollector::collect(..., Some(&ctx.carrier_info), ...)` で exit_bindings を作る +6. `JoinInlineBoundaryBuilder` に `with_inputs(...)` / `with_condition_bindings(...)` / `with_exit_bindings(...)` を渡して conversion pipeline を実行 + +課題: +- carrier set が OwnershipPlan(owned/relay/capture)と一致する保証がない +- 解析 SSOT が無い状態で境界 inputs が構築され、混線を早期に検出しづらい + +## Target Architecture (Ownership as SSOT) + +### SSOT + +- **OwnershipPlan** を SSOT とし、P3(if-sum) の「管理(carrier)」と「参照(capture)」を固定する。 +- `carriers = writes ∩ owned`(Ownership-Relay の不変条件)を P3 本番ルートでも維持する。 + +### Key Idea + +P3(if-sum) で必要なのは次の 2 点: + +1. **carrier set**: boundary inputs / exit bindings の対象集合を固定する +2. **capture set**: 条件式が読むだけの外部値を condition_bindings に限定し、carrier と混ぜない + +(carrier order は JoinIR 側の exit_meta / 既存 carrier_info と整合チェックで段階的に移行する) + +## Integration Point (Where to Compute OwnershipPlan) + +候補は 1 点に絞る: + +- `cf_loop_pattern3_with_if_phi` 内で `build_pattern_context(...)` の直後 + - `PatternPipelineContext` は loop_var(name/id)と body(AST)を持ち、P3 判定も済んでいる + - ここで OwnershipPlan を生成し、以後の boundary 構築の整合チェックに使う + +## Required Interface (Already available) + +- `AstOwnershipAnalyzer`(Phase 63): `ASTNode` から `Vec` を生成 + - 実装サマリ: `docs/development/current/main/PHASE_63_SUMMARY.md` + +## Fail-Fast Contracts (Production Route, dev-only) + +OwnershipPlan を導入する際、次を Fail-Fast で固定する: + +1. **Single-hop relay only**: `relay_path.len() > 1` は Err(段階移行の安全策) +2. **Carrier set alignment**: + - OwnershipPlan から得た carriers 集合が、`ctx.carrier_info` および `exit_meta.exit_values` の集合と一致しない場合は Err +3. **No by-name switching**: + - 関数名/Box名で意味論を変える分岐は禁止 + - ルートの切替は既存の pattern 判定(`ctx.is_if_sum_pattern()`)と構造チェックのみ + +## Migration Plan (Next Phase) + +### Phase 64: P3 本番ルートへ dev-only 接続 + +- `pattern3_with_if_phi.rs` に dev-only で OwnershipPlan を導入 +- boundary inputs / exit bindings に対して carrier set の整合チェックを追加し、混線を Fail-Fast で検出可能にする +- carrier order は既存の exit_meta / carrier_info と一致することを前提にし、順序の SSOT 化は後続フェーズで行う + +## References + +- Ownership SSOT: `docs/development/current/main/phase56-ownership-relay-design.md` +- Phase 61 summary: `docs/development/current/main/PHASE_61_SUMMARY.md` +- Phase 63 summary: `docs/development/current/main/PHASE_63_SUMMARY.md` +- Production P3 route: `src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs` diff --git a/src/mir/join_ir/ownership/ast_analyzer.rs b/src/mir/join_ir/ownership/ast_analyzer.rs new file mode 100644 index 00000000..6f954e73 --- /dev/null +++ b/src/mir/join_ir/ownership/ast_analyzer.rs @@ -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, + defined: BTreeSet, + reads: BTreeSet, + writes: BTreeSet, + condition_reads: BTreeSet, +} + +/// 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, + 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, 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) -> 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 { + 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) -> Result { + 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, 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)> { + 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); + } +} diff --git a/src/mir/join_ir/ownership/mod.rs b/src/mir/join_ir/ownership/mod.rs index 508d6135..9e16ee0d 100644 --- a/src/mir/join_ir/ownership/mod.rs +++ b/src/mir/join_ir/ownership/mod.rs @@ -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::*;