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

@ -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 ビルドで検証しているので、
必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。 必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。

View File

@ -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 と不変条件を明文化)。

View 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 + writecarrier 相当)
- condition capturecondition_captures ⊆ captures
- relay write外側 owned への更新)
## Notes
- Phase 64 で、本番 P3(if-sum) ルート(`pattern3_with_if_phi.rs`)へ dev-only で接続し、carrier order / boundary inputs の SSOT 化を開始する。

View File

@ -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 inputshost_inputs / join_inputsと carrier set を「推測」から「解析結果」へ移行する。
前提: 既定挙動は不変。`normalized_dev` feature + dev 実行ガード下で段階接続する。
## Context
- Phase 5660: 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 が OwnershipPlanowned/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_varname/idと bodyASTを持ち、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`

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