feat(mir): Phase 26-H JoinIR型定義実装完了 - ChatGPT設計

## 実装内容(Step 1-3 完全達成)

### Step 1: src/mir/join_ir.rs 型定義追加
- **JoinFuncId / JoinContId**: 関数・継続ID型
- **JoinFunction**: 関数(引数 = φノード)
- **JoinInst**: Call/Jump/Ret/Compute 最小命令セット
- **MirLikeInst**: 算術・比較命令ラッパー
- **JoinModule**: 複数関数保持コンテナ
- **単体テスト**: 型サニティチェック追加

### Step 2: テストケース追加
- **apps/tests/joinir_min_loop.hako**: 最小ループ+breakカナリア
- **src/tests/mir_joinir_min.rs**: 手書きJoinIR構築テスト
  - MIR → JoinIR手動構築で型妥当性確認
  - #[ignore] で手動実行専用化
  - NYASH_JOINIR_EXPERIMENT=1 トグル制御

### Step 3: 環境変数トグル実装
- **NYASH_JOINIR_EXPERIMENT=1**: 実験モード有効化
- **デフォルト挙動**: 既存MIR/LoopForm経路のみ(破壊的変更なし)
- **トグルON時**: JoinIR手書き構築テスト実行

## Phase 26-H スコープ遵守
 型定義のみ(変換ロジックは未実装)
 最小限の命令セット
 Debug 出力で妥当性確認
 既存パイプライン無影響

## テスト結果
```
$ NYASH_JOINIR_EXPERIMENT=1 cargo test --release mir_joinir_min_manual_construction -- --ignored --nocapture
[joinir/min] MIR module compiled, 3 functions
[joinir/min] JoinIR module constructed:
[joinir/min]  JoinIR型定義は妥当(Phase 26-H)
test result: ok. 1 passed; 0 failed
```

## JoinIR理論の実証
- **φノード = 関数引数**: `fn loop_step(i, k_exit)`
- **merge = join関数**: 分岐後の合流点
- **ループ = 再帰関数**: `loop_step` 自己呼び出し
- **break = 継続呼び出し**: `k_exit(i)`

## 次フェーズ (Phase 27.x)
- LoopForm v2 → JoinIR 自動変換実装
- break/continue ハンドリング
- Exit PHI の JoinIR 引数化

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: ChatGPT <noreply@openai.com>
This commit is contained in:
nyash-codex
2025-11-23 04:10:12 +09:00
parent 0bba67a72c
commit 2692eafbbf
29 changed files with 2081 additions and 58 deletions

View File

@ -0,0 +1,278 @@
# JoinIR — 関数正規化 IR関数と継続だけで制御を表現する層
目的
- Hakorune の制御構造(`if` / `loop` / `break` / `continue` / `return`)を、**関数呼び出し継続continuationだけに正規化する IR 層**として設計する。
- これにより:
- φ ノード = 関数の引数
- merge ブロック = join 関数
- ループ = 再帰関数loop_step exit 継続k_exit
- break / continue = 適切な関数呼び出し
という形に落とし込み、LoopForm v2 / Exit PHI / BodyLocal / ExitLiveness の負担を**構造的に軽くする**。
前提・方針
- LoopForm / ControlForm は「構造の前段」として残す(削除しない)。
- JoinIR は「PHI/SSA の実装負担を肩代わりする層」として導入し、ヘッダ/exit φ や BodyLocal の扱いを **関数の引数と継続** に吸収していく。
- PHI 専用の箱HeaderPhiBuilder / ExitPhiBuilder / BodyLocalPhiBuilder など)は、最終的には JoinIR 降ろしの補助に縮退させることを目標にする。
位置づけ
- 変換パイプラインにおける位置:
```text
AST → MIR+LoopForm v2 → JoinIR → VM / LLVM
```
- AST / MIR では従来どおり Nyash 構文if / loop 等)を扱い、
JoinIR 以降では **関数と値BoxPrimitiveだけ**を見る。
---
## 1. JoinIR のコアアイデア
### 1-1. 「ループ = 関数を何回も呼ぶこと」
通常の while/loop は:
```hako
loop(i < n) {
if i >= n { break }
i = i + 1
}
return i
```
JoinIR 的に見ると:
```text
fn main(k_exit) {
loop_step(0, k_exit)
}
fn loop_step(i, k_exit) {
if i >= n {
// break
k_exit(i)
} else {
// continue
loop_step(i + 1, k_exit)
}
}
```
- ループ本体 = `loop_step(i, k_exit)` という関数。
- `i` は LoopCarriedcarrier変数 → 関数の引数で表現。
- break = exit 継続 `k_exit` の呼び出し。
- continue = `loop_step` をもう一度呼ぶこと。
### 1-2. 「if の merge = join 関数」
ソース:
```hako
if cond {
x = 1
} else {
x = 2
}
print(x)
```
JoinIR 的には:
```text
fn main(k_exit) {
if cond {
then_branch(k_exit)
} else {
else_branch(k_exit)
}
}
fn then_branch(k_exit) {
x = 1
join_after_if(x, k_exit)
}
fn else_branch(k_exit) {
x = 2
join_after_if(x, k_exit)
}
fn join_after_if(x, k_exit) {
print(x)
k_exit(0)
}
```
- φ ノード = `join_after_if` の引数 `x`
- merge ブロック = `join_after_if` 関数。
- 「どのブランチから来たか」の情報は、**関数呼び出し**に吸収される。
---
## 2. JoinIR の型イメージ
※ Phase 26-H 時点では「設計のみ」を置いておき、実装は最小ケースから。
### 2-1. 関数と継続
```rust
/// JoinIR 関数IDMIR 関数とは別 ID でもよい)
struct JoinFuncId(u32);
/// 継続join / ループ step / exit continuationを識別するID
struct JoinContId(u32);
/// JoinIR 関数
struct JoinFunction {
id: JoinFuncId,
params: Vec<VarId>, // 引数(φ に相当)
body: Vec<JoinInst>, // 命令列
exit_cont: Option<JoinContId>, // 呼び出し元に返す継続(ルートは None
}
```
### 2-2. 命令セット(最小)
```rust
enum JoinInst {
/// 通常の関数呼び出し: f(args..., k_next)
Call {
func: JoinFuncId,
args: Vec<VarId>,
k_next: JoinContId,
},
/// 継続呼び出しjoin / exit 継続など)
Jump {
cont: JoinContId,
args: Vec<VarId>,
},
/// ルート関数 or 上位への戻り
Ret {
value: Option<VarId>,
},
/// それ以外の演算は、現行 MIR の算術/比較/boxcall を再利用
Compute(MirLikeInst),
}
```
### 2-3. 継続の分類
- join 継続:
- if の merge や、ループ body 終了後の「次の場所」を表す。
- loop_step:
- ループ 1ステップ分の処理。
- exit 継続:
- ループを抜けた後の処理(`k_exit`)。
- これらはすべて「関数 or 継続 ID」と「引数」で表現される。
---
## 3. MIR/LoopForm から JoinIR への変換ルールv0 草案)
ここでは Phase 26-H で扱う最小のルールだけを書く。
### 3-1. ループLoopForm v2→ loop_step + k_exit
前提: LoopForm v2 が提供する情報
- preheader / header / body / latch / exit / continue_merge / break ブロック集合。
- LoopVarClassBox による分類Pinned / Carrier / BodyLocalExit / BodyLocalInternal
変換方針(概略):
1. LoopCarried 変数の集合 `C = {v1, v2, ...}` を求める。
2. 「ループを抜けた後で使う変数」の集合 `E` を ExitLiveness から得る(長期的には MirScanExitLiveness
3. JoinIR 上で:
- `fn loop_step(C, k_exit)` を新設。
- ループ前の値から `loop_step(C0, k_exit0)` を呼び出す。
- body 内の `break``k_exit(E)` に変換。
- body 内の `continue``loop_step(C_next, k_exit)` に変換。
今の LoopForm / PhiBuilderBox がやっている仕事header φ / exit φ 生成)は、
最終的には「`C``E` を決める補助」に寄せていくイメージになる。
### 3-2. if → join_after_if
前提:
- control_flow / if_form.rs で IfFormcond / then / else / mergeが取れる。
変換方針:
1. merge ブロックで φ が必要な変数集合 `M = {x1, x2, ...}` を求める。
2. JoinIR 上で:
- `fn join_after_if(M, k_exit)` を新設。
- then ブランチの末尾に `join_after_if(x1_then, x2_then, ..., k_exit)` を挿入。
- else ブランチの末尾に `join_after_if(x1_else, x2_else, ..., k_exit)` を挿入。
3. merge ブロックの本体は `join_after_if` に移す。
これにより、φ ノードは `join_after_if` の引数に吸収される。
---
## 4. フェーズ計画Phase 26-H 〜 27.x の流れ)
### Phase 26-H現在フェーズ— 設計+ミニ実験
- スコープ:
- JoinIR の設計ドキュメント(このファイル+ phase-26-H/README
- `src/mir/join_ir.rs` に型だけ入れる(変換ロジックは最小)。
- 1 ケース限定の変換実験:
- 例: `apps/tests/joinir_min_loop.hako` → MIR → JoinIR → JoinIR のダンプを Rust テストで確認。
- ゴール:
- 「関数正規化 IRJoinIRが箱理論LoopForm v2 と矛盾しない」ことを確認する。
- 大規模リプレースに進む価値があるか、感触を掴む。
### Phase 27.x— 段階的な採用案(まだ構想段階)
この段階はまだ「構想メモ」として置いておく。
- 27.1: LoopForm v2 → JoinIR の変換を部分的に実装
- まずは「break なしの loop + if」だけを対象にする。
- Exit φ / header φ の一部を JoinIR の引数に置き換える。
- 27.2: break / continue / BodyLocalExit を JoinIR で扱う
- BodyLocalPhiBuilder / LoopExitLiveness の「決定」を JoinIR 引数設計に寄せる。
- ExitLiveness は JoinIR 関数の use/def から算出可能かを検証する。
- 27.3: SSA/PHI との役割調整
- LoopForm v2 / PhiBuilderBox は「JoinIR 生成補助」の位置付けに縮退。
- MirVerifier は JoinIR ベースのチェック(又は MIR/JoinIR 並列)に切り替え検討。
※これら 27.x フェーズは、26-F / 26-G で現行ラインの根治が一段落してから進める前提。
---
## 5. 他ドキュメントとの関係
- `docs/development/roadmap/phases/phase-26-H/README.md`
- Phase 26-H のスコープ設計ミニ実験、他フェーズ25.1 / 26-F / 26-Gとの関係を定義。
- `docs/development/architecture/loops/loopform_ssot.md`
- LoopForm v2 / Exit PHI / 4箱構成LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBoxの設計ート。
- 将来、JoinIR を導入するときは「LoopForm v2 → JoinIR 変換の前段」としてこの SSOT を活かす。
このファイルは、JoinIR を「全部関数な言語」のコアとして扱うための設計メモだよ。
実際の導入は小さな実験Phase 26-Hから始めて、問題なければ 27.x 以降で段階的に進める想定にしておく。
## 6. φ と merge の対応表(抜粋)
| 構文/概念 | JoinIR での表現 | φ/合流の扱い |
|-----------|-----------------|--------------|
| if/merge | join 関数呼び出し | join 関数の引数が φ 相当 |
| loop | step 再帰 + k_exit 継続 | LoopCarried/Exit の値を引数で渡す |
| break | k_exit 呼び出し | φ 不要(引数で値を渡す) |
| continue | step 呼び出し | φ 不要(引数で値を渡す) |
| return | 継続または ret | そのまま値を返す |
## 7. 26-H で増やす実装箱 / 概念ラベル
- 実装として増やす(このフェーズで手を動かす)
- `join_ir.rs`: `JoinFunction/JoinBlock/JoinInst` の最小定義とダンプ
- LoopForm→JoinIR のミニ変換関数1 ケース限定で OK
- 実験トグル(例: `NYASH_JOINIR_EXPERIMENT=1`)で JoinIR をダンプするフック
- 概念ラベルのみ27.x 以降で拡張を検討)
- MirQueryBox のような MIR ビュー層reads/writes/succs を trait 化)
- LoopFnLoweringBox / JoinIRBox の分割(必要になったら分ける)
- JoinIR 上での最適化や VM/LLVM 統合
このフェーズでは「箱を増やしすぎない」ことを意識し、型定義+最小変換+ダンプ確認だけに留める。

View File

@ -23,9 +23,24 @@ SSOTSingle Source of Truth
- 1 predecessor なら直接 bindPHI省略、2つ以上で PHI を生成。
- 検証は FailFast ではなく開発時 WARN`debug_assert`)だが、将来 Core 側で整形に移管予定。
4箱構成Phase 26-F 系の整理)
- **LoopVarClassBox**`loop_var_classifier.rs`
- 変数のスコープ分類専用箱だよ。Pinned / Carrier / BodyLocalExit / BodyLocalInternal を決めるだけで、PHI 発行はしない。
- **LoopExitLivenessBox**`loop_exit_liveness.rs`
- ループ `exit` 直後で「実際に使われる可能性がある変数」を集める箱だよ。Phase 26-F 時点では `live_at_exit` は保守的近似で、将来 `get_block_instructions()` などを使った MIR スキャンに差し替える予定。
- `ExitLivenessProvider` を実装していて、`ExitPhiBuilder` は Box<dyn ExitLivenessProvider> を受け取る形にしたので、Legacy既定と MirScan 版の差し替えがそのまま出来る。
- 環境変数 `NYASH_EXIT_LIVE_ENABLE=1` で将来の実装を段階的に有効化、`NYASH_EXIT_LIVENESS_TRACE=1` でトレースを出せるようにしてある。
- **BodyLocalPhiBuilder**`body_local_phi_builder.rs`
- 上の 2つの箱の結果を統合する決定箱だよ。
- `class.needs_exit_phi()` が truePinned / Carrier / BodyLocalExitのものは従来どおり exit PHI 候補。
- それ以外でも、`BodyLocalInternal` かつ `live_at_exit` に含まれ、かつ `is_available_in_all` で全 pred 定義が確認できるものだけを安全側で救済候補にできるようにしてある(この救済ロジック自体は `NYASH_EXIT_LIVE_ENABLE` でガード)。
- **PhiInvariantsBox**`phi_invariants.rs`
- 最後に「全 pred で定義されているか」「不正な incoming が無いか」を Fail-Fast でチェックする箱だよ。ここで落ちる場合は LoopForm/BodyLocal 側の構造バグとみなしている。
今後の移行
- Bridge 側に `LoopPhiOps` 実装を追加し、`prepare/seal/exit` を直接呼ぶ。
- ループ形状の生成をユーティリティ化builder/bridge 双方から共通呼び出し)。
- ExitLivenessProvider は 26-G 以降で MIR 命令列スキャン版に差し替える予定(現状の MirScanExitLiveness は header/exit_snapshots の union 近似)。
---