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:
@ -254,7 +254,15 @@
|
|||||||
19. **Phase 62-OWNERSHIP-P3-ROUTE-DESIGN(次のフォーカス候補)**: P3 本番ルートへ OwnershipPlan を渡す設計
|
19. **Phase 62-OWNERSHIP-P3-ROUTE-DESIGN(次のフォーカス候補)**: P3 本番ルートへ OwnershipPlan を渡す設計
|
||||||
- MIR→JoinIR の `pattern3_with_if_phi.rs` は OwnershipPlan を受け取らないため、AST-based ownership 解析の接続点を設計する。
|
- MIR→JoinIR の `pattern3_with_if_phi.rs` は OwnershipPlan を受け取らないため、AST-based ownership 解析の接続点を設計する。
|
||||||
- dev-only で段階接続し、legacy と stdout/exit 一致の比較で回帰を固定(既定挙動は不変)。
|
- 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 ビルドで検証しているので、
|
- すでに PHI/ValueId 契約は debug ビルドで検証しているので、
|
||||||
必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。
|
必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。
|
||||||
|
|
||||||
|
|||||||
@ -53,4 +53,5 @@ Phase 61 は「Break(P2) に P3 固有ロジックを混ぜない」を達成し
|
|||||||
## Next Candidates
|
## Next Candidates
|
||||||
|
|
||||||
- Phase 62: P3(if-sum) の「本番 MIR→JoinIR ルート」へ OwnershipPlan を渡す設計(AST-based ownership 解析の接続点を設計 → dev-only で段階接続)。
|
- 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 と不変条件を明文化)。
|
- Phase 63+: multi-hop / merge relay の意味論設計(Fail-Fast を解除する前に SSOT と不変条件を明文化)。
|
||||||
|
|||||||
28
docs/development/current/main/PHASE_63_SUMMARY.md
Normal file
28
docs/development/current/main/PHASE_63_SUMMARY.md
Normal file
@ -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<OwnershipPlan>`)
|
||||||
|
- 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 化を開始する。
|
||||||
@ -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<OwnershipPlan>` を生成
|
||||||
|
- 実装サマリ: `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`
|
||||||
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;
|
mod analyzer;
|
||||||
#[cfg(feature = "normalized_dev")]
|
#[cfg(feature = "normalized_dev")]
|
||||||
mod plan_to_lowering;
|
mod plan_to_lowering;
|
||||||
|
#[cfg(feature = "normalized_dev")]
|
||||||
|
mod ast_analyzer;
|
||||||
|
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub use analyzer::*;
|
pub use analyzer::*;
|
||||||
#[cfg(feature = "normalized_dev")]
|
#[cfg(feature = "normalized_dev")]
|
||||||
pub use plan_to_lowering::*;
|
pub use plan_to_lowering::*;
|
||||||
|
#[cfg(feature = "normalized_dev")]
|
||||||
|
pub use ast_analyzer::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user