docs(joinir): Phase 56 - Ownership-Relay Design + interface skeleton

「読むのは自由、管理は直下 owned だけ」アーキテクチャの設計文書と型定義。

Key changes:
- Design doc: phase56-ownership-relay-design.md
  - Core definitions: owned/carriers/captures/relay
  - Invariants: Ownership Uniqueness, Carrier Locality, Relay Propagation
  - Shadowing rules, multi-writer merge semantics
  - JoinIR mapping from current system to new system
  - Implementation phases roadmap (56-61)

- New module: src/mir/join_ir/ownership/
  - types.rs: ScopeId, ScopeOwnedVar, RelayVar, CapturedVar, OwnershipPlan
  - mod.rs: Module documentation with responsibility boundaries
  - README.md: Usage guide and examples

- API methods:
  - OwnershipPlan::carriers() - owned AND written variables
  - OwnershipPlan::condition_only_carriers() - condition-only carriers
  - OwnershipPlan::verify_invariants() - invariant checking

Tests: 942/942 PASS (+3 unit tests)
Zero behavioral change - analysis module skeleton only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-12 17:39:59 +09:00
parent 80e952b83a
commit 2d10c5ce3f
6 changed files with 517 additions and 4 deletions

View File

@ -216,10 +216,21 @@
- Phase 53 で実戦ループ追加済みのため、追加投入より先に構造判定精度の測定に集中。 - Phase 53 で実戦ループ追加済みのため、追加投入より先に構造判定精度の測定に集中。
- 構造シグネチャ軸を 5+ に拡張Compare op 分布などし、P2/P3 の偽陽性観測テストを追加。 - 構造シグネチャ軸を 5+ に拡張Compare op 分布などし、P2/P3 の偽陽性観測テストを追加。
- 結果: selfhost 群の構造判定だけでは分離が不十分(偽陽性率 ~50%。dev-only name ガードは当面必須と判断。 - 結果: selfhost 群の構造判定だけでは分離が不十分(偽陽性率 ~50%。dev-only name ガードは当面必須と判断。
12. **Phase 55-SELFHOST-SHAPE-AXIS-EXPAND次のフォーカス候補・dev-only**: 構造軸 8+ へ拡張し誤判定を下げる足場 12. **Phase 55-SELFHOST-SHAPE-AXIS-EXPANDdev-only / 保留**: 構造軸を可変 feature として拡張し誤判定を下げる足場
- 条件複雑度(ネスト/論理結合の形)、算術更新パターン、分岐ファンアウトなどの新軸を追加 - Phase 5661 の Ownership-Relay ライン優先のため、selfhost shape 軸拡張は一旦保留
- selfhost/canonical の feature ベクトル比較と観測テストを拡充し、name ガード縮小の根拠を作る(撤去は後続) - OwnershipAnalyzer 導入後に、scope 署名owned/carriers/captures/relayを新しい構造軸として合流させる
13. JoinIR Verify / 最適化まわり 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 化。
- 既存 fixturespattern2/3, jsonparser, selfhostで plan の回帰テストを追加。
15. JoinIR Verify / 最適化まわり
- すでに PHI/ValueId 契約は debug ビルドで検証しているので、 - すでに PHI/ValueId 契約は debug ビルドで検証しているので、
必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。 必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。

View File

@ -0,0 +1,224 @@
# Phase 56: Ownership-Relay Design
## Overview
「読むのは自由、管理は直下 owned だけ」アーキテクチャの設計文書。
Phase 56 は **インターフェース設計のみ**。実装は Phase 57 以降。
## Core Definitions
### owned (所有)
- 変数を定義したスコープが唯一の owner
- Loop直下の `local x` → そのループが owned
- body-localif/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<ScopeOwnedVar>,
pub relay_writes: Vec<RelayVar>,
pub captures: Vec<CapturedVar>,
pub condition_captures: Vec<CapturedVar>,
}
```
**設計意図**:
- `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

View File

@ -41,6 +41,9 @@ pub mod normalized;
// Phase 34-1: Frontend (AST→JoinIR) — skeleton only // Phase 34-1: Frontend (AST→JoinIR) — skeleton only
pub mod frontend; pub mod frontend;
// Phase 56: Ownership analysis (reads/writes → owned/relay/capture)
pub mod ownership;
// Re-export lowering functions for backward compatibility // Re-export lowering functions for backward compatibility
pub use lowering::{ pub use lowering::{
lower_funcscanner_trim_to_joinir, lower_min_loop_to_joinir, lower_skip_ws_to_joinir, lower_funcscanner_trim_to_joinir, lower_min_loop_to_joinir, lower_skip_ws_to_joinir,

View File

@ -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)

View File

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

View File

@ -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<ScopeId>,
}
/// 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<ScopeOwnedVar>,
/// Variables written but owned by ancestor (need relay)
pub relay_writes: Vec<RelayVar>,
/// Variables read but not owned (read-only capture)
pub captures: Vec<CapturedVar>,
/// Subset of captures used in conditions
pub condition_captures: Vec<CapturedVar>,
}
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<Item = &ScopeOwnedVar> {
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<Item = &ScopeOwnedVar> {
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());
}
}