diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 104857fc..3581ba0a 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -216,10 +216,21 @@ - Phase 53 で実戦ループ追加済みのため、追加投入より先に構造判定精度の測定に集中。 - 構造シグネチャ軸を 5+ に拡張(Compare op 分布など)し、P2/P3 の偽陽性観測テストを追加。 - 結果: selfhost 群の構造判定だけでは分離が不十分(偽陽性率 ~50%)。dev-only name ガードは当面必須と判断。 -12. **Phase 55-SELFHOST-SHAPE-AXIS-EXPAND(次のフォーカス候補・dev-only)**: 構造軸 8+ へ拡張し誤判定を下げる足場 - - 条件複雑度(ネスト/論理結合の形)、算術更新パターン、分岐ファンアウトなどの新軸を追加。 - - selfhost/canonical の feature ベクトル比較と観測テストを拡充し、name ガード縮小の根拠を作る(撤去は後続)。 -13. JoinIR Verify / 最適化まわり +12. **Phase 55-SELFHOST-SHAPE-AXIS-EXPAND(dev-only / 保留)**: 構造軸を可変 feature として拡張し誤判定を下げる足場 + - Phase 56–61 の Ownership-Relay ライン優先のため、selfhost shape 軸拡張は一旦保留。 + - OwnershipAnalyzer 導入後に、scope 署名(owned/carriers/captures/relay)を新しい構造軸として合流させる。 +13. **Phase 56-OWNERSHIP-RELAY-DESIGN(完了✅ 2025-12-12)**: Ownership-Relay アーキテクチャ設計 + インターフェース skeleton + - 設計詳細: [phase56-ownership-relay-design.md](docs/development/current/main/phase56-ownership-relay-design.md) + - コア定義: owned / carriers / captures / relay の 4 分類を明確化 + - 不変条件: Ownership Uniqueness / Carrier Locality / Relay Propagation / Capture Read-Only + - Module 作成: `src/mir/join_ir/ownership/` - 責務は「解析のみ」 + - 型定義: `ScopeId`, `ScopeOwnedVar`, `RelayVar`, `CapturedVar`, `OwnershipPlan` + - テスト: 3 つのユニットテスト追加(empty plan / carriers filter / invariant verification) + - 次: Phase 57 で OwnershipAnalyzer 実装(dev-only) +14. **Phase 57-OWNERSHIP-ANALYZER-DEV(次のフォーカス候補・dev-only)**: OwnershipPlan を生成する解析箱の実装 + - `OwnershipAnalyzer` を追加し、ネスト含む reads/writes/owned を集計→ carriers/relay/captures を plan 化。 + - 既存 fixtures(pattern2/3, jsonparser, selfhost)で plan の回帰テストを追加。 +15. JoinIR Verify / 最適化まわり - すでに PHI/ValueId 契約は debug ビルドで検証しているので、 必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。 diff --git a/docs/development/current/main/phase56-ownership-relay-design.md b/docs/development/current/main/phase56-ownership-relay-design.md new file mode 100644 index 00000000..3797a620 --- /dev/null +++ b/docs/development/current/main/phase56-ownership-relay-design.md @@ -0,0 +1,224 @@ +# Phase 56: Ownership-Relay Design + +## Overview +「読むのは自由、管理は直下 owned だけ」アーキテクチャの設計文書。 + +Phase 56 は **インターフェース設計のみ**。実装は Phase 57 以降。 + +## Core Definitions + +### owned (所有) +- 変数を定義したスコープが唯一の owner +- Loop直下の `local x` → そのループが owned +- body-local(if/block内の local)→ 最も内側の enclosing loop が owned + +**例**: +```rust +fn outer() { + local a = 0 // owned by outer + loop { + local b = 0 // owned by this loop + if cond { + local c = 0 // owned by enclosing loop (not if!) + } + } +} +``` + +### carriers (管理対象) +- `carriers = writes ∩ owned` +- そのスコープが定義 AND 更新する変数のみ +- loop_step の引数として管理 + +**重要**: Carrier は「所有 AND 更新」のみ。読み取り専用の owned 変数は carrier ではない。 + +### captures (読み取り参照) +- `captures = reads \ owned` (かつ carriers ではない) +- 祖先スコープの変数への read-only アクセス +- CapturedEnv / ConditionEnv 経由 + +**例**: +```rust +local limit = 100 +loop { + local sum = 0 + if sum < limit { // limit は capture (read-only) + sum++ // sum は carrier (owned + written) + } +} +``` + +### relay (中継) +- 内側スコープが祖先 owned を更新する場合 +- 更新責務を owner へ昇格(relay up) +- 中間ループは引数として素通し + +**例**: +```rust +loop outer { + local total = 0 // owned by outer + loop inner { + total++ // relay to outer (inner doesn't own total) + } +} +// outer の exit PHI で total を merge +``` + +## Invariants (不変条件) + +1. **Ownership Uniqueness**: 各変数は唯一の owner を持つ +2. **Carrier Locality**: carriers = writes ∩ owned (借用なし) +3. **Relay Propagation**: writes \ owned → owner に昇格 +4. **Capture Read-Only**: captures は read-only (PHI 不要) + +## Shadowing Rules + +```nyash +local x = 0 // outer owned +loop { + local x = 1 // inner owned (shadows outer) + // 外の x は capture 可能だが、inner の x が優先 + print(x) // inner の x (1) +} +print(x) // outer の x (0) +``` + +- Shadowing = 新しい ownership 発生 +- 名前解決は最内スコープ優先 +- 外の x は capture として参照可能だが、内の x が存在する限り内が優先 + +## Multi-Writer Merge + +```nyash +loop outer { + local total = 0 + if a { loop inner1 { total++ } } // relay to outer + if b { loop inner2 { total-- } } // relay to outer +} +// outer の exit PHI で merge +``` + +- Relay は「更新意図の伝達」 +- 実際の PHI merge は owner 側で実行 +- 複数の内側ループが同じ変数を relay → owner の exit PHI で統合 + +## JoinIR Mapping + +### Current System → New System + +| Current | New | +|---------|-----| +| CarrierInfo.carriers | OwnershipPlan.owned_carriers | +| promoted_loopbodylocals | (absorbed into owned analysis) | +| CapturedEnv | OwnershipPlan.captures | +| ConditionEnv | OwnershipPlan.condition_captures | +| (implicit) | OwnershipPlan.relay_writes | + +### OwnershipPlan Structure + +```rust +pub struct OwnershipPlan { + pub scope_id: ScopeId, + pub owned_carriers: Vec, + pub relay_writes: Vec, + pub captures: Vec, + pub condition_captures: Vec, +} +``` + +**設計意図**: +- `owned_carriers`: このスコープが所有 AND 更新する変数 +- `relay_writes`: 祖先の変数への書き込み(owner へ昇格) +- `captures`: 祖先の変数への読み取り専用参照 +- `condition_captures`: captures のうち、条件式で使われるもの + +## Implementation Phases + +- **Phase 56**: Design + interface skeleton (this phase) ✅ +- **Phase 57**: OwnershipAnalyzer implementation (dev-only) +- **Phase 58**: P2 plumbing (dev-only) +- **Phase 59**: P3 plumbing (dev-only) +- **Phase 60**: Cleanup dev heuristics +- **Phase 61**: Canonical promotion decision + +## Module Boundary + +`src/mir/join_ir/ownership/` - 責務は「解析のみ」 + +**This module does**: +- ✅ Collect reads/writes from AST/ProgramJSON +- ✅ Determine variable ownership (owned/relay/capture) +- ✅ Produce OwnershipPlan for downstream lowering + +**This module does NOT**: +- ❌ Generate MIR instructions +- ❌ Modify JoinIR structures +- ❌ Perform lowering transformations + +Lowering/MIR生成は既存モジュールが担当。 + +## Example Ownership Plans + +### Example 1: Simple Loop + +```nyash +local sum = 0 +loop { + sum++ +} +``` + +**OwnershipPlan (loop scope)**: +- `owned_carriers`: [`sum` (written)] +- `relay_writes`: [] +- `captures`: [] + +### Example 2: Nested Loop with Relay + +```nyash +local total = 0 +loop outer { + loop inner { + total++ + } +} +``` + +**OwnershipPlan (inner loop)**: +- `owned_carriers`: [] +- `relay_writes`: [`total` → relay to outer] +- `captures`: [] + +**OwnershipPlan (outer loop)**: +- `owned_carriers`: [`total` (written via relay)] +- `relay_writes`: [] +- `captures`: [] + +### Example 3: Capture + Carrier + +```nyash +local limit = 100 +loop { + local sum = 0 + if sum < limit { + sum++ + } +} +``` + +**OwnershipPlan (loop scope)**: +- `owned_carriers`: [`sum` (written)] +- `relay_writes`: [] +- `captures`: [`limit` (read-only)] +- `condition_captures`: [`limit`] + +## References +- **Phase 53-54**: Structural axis expansion +- **Phase 43/245B**: Normalized JoinIR completion +- **ChatGPT discussion**: 「読むのは自由、管理は直下だけ」設計 +- **JoinIR Architecture**: [joinir-architecture-overview.md](joinir-architecture-overview.md) + +## Status + +- ✅ Phase 56 (this): Design + interface skeleton completed +- ⏳ Phase 57+: Implementation pending diff --git a/src/mir/join_ir/mod.rs b/src/mir/join_ir/mod.rs index 2ddb3d56..43fb9449 100644 --- a/src/mir/join_ir/mod.rs +++ b/src/mir/join_ir/mod.rs @@ -41,6 +41,9 @@ pub mod normalized; // Phase 34-1: Frontend (AST→JoinIR) — skeleton only pub mod frontend; +// Phase 56: Ownership analysis (reads/writes → owned/relay/capture) +pub mod ownership; + // Re-export lowering functions for backward compatibility pub use lowering::{ lower_funcscanner_trim_to_joinir, lower_min_loop_to_joinir, lower_skip_ws_to_joinir, diff --git a/src/mir/join_ir/ownership/README.md b/src/mir/join_ir/ownership/README.md new file mode 100644 index 00000000..fd609e4f --- /dev/null +++ b/src/mir/join_ir/ownership/README.md @@ -0,0 +1,79 @@ +# Ownership Analysis Module + +## Responsibility Boundary + +This module is responsible for **analysis only**: +- ✅ Collecting reads/writes from AST/ProgramJSON +- ✅ Determining variable ownership (owned/relay/capture) +- ✅ Producing OwnershipPlan for downstream lowering + +This module does NOT: +- ❌ Generate MIR instructions +- ❌ Modify JoinIR structures +- ❌ Perform lowering transformations + +## Core Types + +| Type | Purpose | +|------|---------| +| `ScopeId` | Unique scope identifier | +| `ScopeOwnedVar` | Variable defined in this scope | +| `RelayVar` | Write to ancestor-owned variable | +| `CapturedVar` | Read-only reference to ancestor | +| `OwnershipPlan` | Complete analysis result | + +## Invariants + +1. `carriers = owned_vars.filter(is_written)` +2. No variable in both owned and relay +3. No variable in both owned and captures +4. Relay implies ancestor ownership exists + +## Design Philosophy + +**「読むのは自由、管理は直下 owned だけ」** + +- **Owned**: Variable defined in this scope (unique owner) +- **Carrier**: Owned AND written (managed as loop_step argument) +- **Capture**: Read-only reference to ancestor (via CapturedEnv) +- **Relay**: Write to ancestor → relay up to owner (exit PHI at owner) + +## Phase Status + +- Phase 56: ✅ Interface skeleton +- Phase 57: ⏳ OwnershipAnalyzer implementation +- Phase 58: ⏳ P2 plumbing +- Phase 59: ⏳ P3 plumbing +- Phase 60: ⏳ Cleanup dev heuristics +- Phase 61: ⏳ Canonical promotion decision + +## Usage (Future) + +```rust +let plan = OwnershipAnalyzer::analyze(&ast_node, parent_scope); +plan.verify_invariants()?; +let carriers: Vec<_> = plan.carriers().collect(); +``` + +## Example + +```nyash +local limit = 100 // owned by outer +loop { + local sum = 0 // owned by loop + if sum < limit { // limit = capture (read-only) + sum++ // sum = carrier (owned + written) + } +} +``` + +**OwnershipPlan (loop scope)**: +- `owned_vars`: [`sum` (written), `limit` (read-only)] +- `relay_writes`: [] +- `captures`: [`limit`] +- `condition_captures`: [`limit`] + +## References + +- Design Doc: [phase56-ownership-relay-design.md](../../../../docs/development/current/main/phase56-ownership-relay-design.md) +- JoinIR Architecture: [joinir-architecture-overview.md](../../../../docs/development/current/main/joinir-architecture-overview.md) diff --git a/src/mir/join_ir/ownership/mod.rs b/src/mir/join_ir/ownership/mod.rs new file mode 100644 index 00000000..0c65ec81 --- /dev/null +++ b/src/mir/join_ir/ownership/mod.rs @@ -0,0 +1,28 @@ +//! Ownership Analysis for JoinIR +//! +//! # Responsibility Boundary +//! +//! This module is responsible for **analysis only**: +//! - Collecting reads/writes from AST/ProgramJSON +//! - Determining variable ownership (owned/relay/capture) +//! - Producing OwnershipPlan for downstream lowering +//! +//! This module does NOT: +//! - Generate MIR instructions +//! - Modify JoinIR structures +//! - Perform lowering transformations +//! +//! # Core Invariants +//! +//! 1. **Ownership Uniqueness**: Each variable has exactly one owner scope +//! 2. **Carrier Locality**: carriers = writes ∩ owned +//! 3. **Relay Propagation**: writes to ancestor-owned → relay up +//! 4. **Capture Read-Only**: captures have no PHI at this scope +//! +//! # Phase 56 Status +//! +//! Interface skeleton only. Implementation in Phase 57+. + +mod types; + +pub use types::*; diff --git a/src/mir/join_ir/ownership/types.rs b/src/mir/join_ir/ownership/types.rs new file mode 100644 index 00000000..adfed160 --- /dev/null +++ b/src/mir/join_ir/ownership/types.rs @@ -0,0 +1,168 @@ +//! Core types for ownership analysis. +//! +//! Phase 56: Interface definitions only (not yet used). + +#[cfg(any(debug_assertions, test))] +use std::collections::BTreeSet; + +/// Unique identifier for a scope (loop, function, block). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ScopeId(pub u32); + +/// A variable owned by the current scope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScopeOwnedVar { + /// Variable name + pub name: String, + /// Whether this variable is written within the scope + pub is_written: bool, + /// Whether this variable is used in loop conditions + pub is_condition_only: bool, +} + +/// A variable whose updates should be relayed to an ancestor owner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RelayVar { + /// Variable name + pub name: String, + /// Scope that owns this variable + pub owner_scope: ScopeId, + /// Intermediate scopes that need to forward this update + pub relay_path: Vec, +} + +/// A variable captured (read-only) from an ancestor scope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CapturedVar { + /// Variable name + pub name: String, + /// Scope that owns this variable + pub owner_scope: ScopeId, +} + +/// Complete ownership analysis result for a scope. +#[derive(Debug, Clone, Default)] +pub struct OwnershipPlan { + /// ID of this scope + pub scope_id: ScopeId, + + /// Variables owned by this scope (defined here) + /// Invariant: carriers = owned_vars where is_written = true + pub owned_vars: Vec, + + /// Variables written but owned by ancestor (need relay) + pub relay_writes: Vec, + + /// Variables read but not owned (read-only capture) + pub captures: Vec, + + /// Subset of captures used in conditions + pub condition_captures: Vec, +} + +impl Default for ScopeId { + fn default() -> Self { + ScopeId(0) + } +} + +impl OwnershipPlan { + /// Create empty plan for a scope. + pub fn new(scope_id: ScopeId) -> Self { + Self { + scope_id, + ..Default::default() + } + } + + /// Get carriers (owned AND written). + pub fn carriers(&self) -> impl Iterator { + self.owned_vars.iter().filter(|v| v.is_written) + } + + /// Get condition-only carriers (owned, written, condition-only). + pub fn condition_only_carriers(&self) -> impl Iterator { + self.owned_vars.iter().filter(|v| v.is_written && v.is_condition_only) + } + + /// Check invariant: no variable appears in multiple categories. + #[cfg(any(debug_assertions, test))] + pub fn verify_invariants(&self) -> Result<(), String> { + let mut all_names: BTreeSet<&str> = BTreeSet::new(); + + for v in &self.owned_vars { + if !all_names.insert(&v.name) { + return Err(format!("Duplicate owned var: {}", v.name)); + } + } + + for v in &self.relay_writes { + if self.owned_vars.iter().any(|o| o.name == v.name) { + return Err(format!("Relay var '{}' conflicts with owned", v.name)); + } + } + + for v in &self.captures { + if self.owned_vars.iter().any(|o| o.name == v.name) { + return Err(format!("Captured var '{}' conflicts with owned", v.name)); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_plan() { + let plan = OwnershipPlan::new(ScopeId(1)); + assert_eq!(plan.scope_id.0, 1); + assert!(plan.owned_vars.is_empty()); + assert_eq!(plan.carriers().count(), 0); + } + + #[test] + fn test_carriers_filter() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.owned_vars.push(ScopeOwnedVar { + name: "sum".to_string(), + is_written: true, + is_condition_only: false, + }); + plan.owned_vars.push(ScopeOwnedVar { + name: "limit".to_string(), + is_written: false, // read-only owned + is_condition_only: false, + }); + + let carriers: Vec<_> = plan.carriers().collect(); + assert_eq!(carriers.len(), 1); + assert_eq!(carriers[0].name, "sum"); + } + + #[test] + fn test_invariant_verification() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.owned_vars.push(ScopeOwnedVar { + name: "x".to_string(), + is_written: true, + is_condition_only: false, + }); + + // Valid plan + assert!(plan.verify_invariants().is_ok()); + + // Add conflicting relay + plan.relay_writes.push(RelayVar { + name: "x".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![], + }); + + // Now invalid + assert!(plan.verify_invariants().is_err()); + } +}