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 近似)。
---

View File

@ -0,0 +1,95 @@
# Phase 26-F — Loop Exit Liveness / BodyLocal PHI Guard Line
Status: planning設計 + 受け口整備。MIRスキャン本体は後続フェーズ
## ゴール
- LoopForm v2 / Exit PHI まわりで残っている BodyLocal 変数の未定義バグを、「箱」と「MIR スキャン」に分けて根治する。
- すでに Phase 26-E までで固めた PHI SSOTPhiBuilderBox / IfForm / ExitPhiBuilder / BodyLocalPhiBuilderを前提に
- **分類**LoopVarClassBox
- **実際の使用**LoopExitLivenessBox, 将来の MIR スキャン)
をきちんと分離する。
- その上で FuncScanner / StageB / Stage1 入口で発生している `use of undefined value` を、構造から潰す足場を固める。
## スコープ26-F でやること)
### FA: Exit PHI 4箱構成の確定とガード
- ファイル:
- `src/mir/phi_core/loop_var_classifier.rs`
- `src/mir/phi_core/local_scope_inspector.rs`
- `src/mir/phi_core/body_local_phi_builder.rs`
- `src/mir/phi_core/loop_exit_liveness.rs`(新設済み)
- `src/mir/phi_core/phi_invariants.rs`
- `src/mir/phi_core/exit_phi_builder.rs`
- やること:
- 4箱構成をドキュメントに固定するコードはほぼ出来ているので設計を追記する:
- LoopVarClassBox: Pinned / Carrier / BodyLocalExit / BodyLocalInternal の分類専用。
- LoopExitLivenessBox: ループ exit 後で「生きている変数」の集合を返す箱Phase 26-F 時点では保守的近似+環境変数ガード)。
- BodyLocalPhiBuilder: 分類結果と `live_at_exit` を OR 判定し、「どの BodyLocal に exit PHI が必要か」だけを決める箱。
- PhiInvariantsBox: 「全 pred で定義されているか」を FailFast でチェックする箱。
- `NYASH_EXIT_LIVE_ENABLE` が未設定のときは **従来挙動Phase 26-F-3 相当)** に固定されることを明文化。
- `NYASH_EXIT_LIVENESS_TRACE=1` で、将来の MIR スキャン実装時にトレースを出す方針を記録。
### FB: MIR スキャン前提の ExitLiveness 受け口設計
- ファイル:
- `src/mir/phi_core/loop_exit_liveness.rs`
- `src/mir/phi_core/exit_phi_builder.rs`
- (将来)`src/mir/loop_builder.rs` / `src/mir/function.rs`
- `src/mir/query.rs`MirQuery/MirQueryBox
- やること:
- ExitLiveness 用のトレイトを決める → **実装済み**
- `ExitLivenessProvider::compute_live_at_exit(header_vals, exit_snapshots) -> BTreeSet<String>`
- 既定実装は `LoopExitLivenessBox`Phase 26-F-3 と同じ空集合、env ガード付き)
- Phase 2+ 用の `MirScanExitLiveness` を追加済み。現時点では「header_vals + exit_snapshots に出現する変数の union」を返す簡易スキャンで、`NYASH_EXIT_LIVE_ENABLE=1` で opt-in。後続フェーズ26-G 以降)で MIR 命令列スキャンに差し替える想定。
- Phase 26-F では「箱とインターフェース」だけ決めて、実装は**保守的近似 or ダミー**のままに留める。
- `ExitPhiBuilder` は **ExitLivenessProvider の結果だけを見る**ようにしたので、後続フェーズで `MirScanExitLiveness` に差し替え可能(依存逆転)。
### FC: FuncScanner / parse_params / trim 用の再現ケース固定
- ファイル:
- `lang/src/compiler/entry/func_scanner.hako`
- `lang/src/compiler/tests/funcscanner_skip_ws_min.hako`
- `lang/src/compiler/tests/funcscanner_parse_params_trim_min.hako`
- `src/tests/mir_funcscanner_skip_ws.rs`
- `src/tests/mir_funcscanner_parse_params_trim_min.rs`
- やること:
- すでに作成済みの 2 本の Rust テストを「ExitLiveness / BodyLocal PHI のカナリア」として位置づける:
- `mir_funcscanner_skip_ws_direct_vm`
- `mir_funcscanner_parse_params_trim_min_verify_and_vm`
- この 2 本が、今後の MIR スキャン実装Phase 26-G 相当で「ExitLiveness を差し込んだときに必ず緑になるべき」ターゲットであることを docs に固定。
- `_trim` / `skip_whitespace` 本体には、`__mir__.log` ベースの軽量観測が既に仕込まれているので、その存在を `mir-logs-observability.md` 側にリンクしておく。
### FD: 将来フェーズMIR スキャン本体)への橋渡し
- ファイル候補:
- `docs/development/roadmap/phases/phase-26-F/README.md`(本ファイル)
- `docs/development/architecture/loops/loopform_ssot.md`
- やること:
- Phase 26-F では「箱」と「受け口」と「環境変数ガード」までに留め、MIR 命令列の実スキャンは次フェーズ26-G など)に分離する方針を書き切る。
- LoopFormOps を拡張して MIR 命令列にアクセスする案(`get_block_instructions` 等)を、設計レベルでメモしておく。
- ExitLiveness の MIR スキャン実装は:
- exit ブロック(必要ならその直後)での use/def を収集し、
- 逆 RPO で固定点反復して `live_in/ live_out` を決める、
といった最小の liveness アルゴリズムで良いことを明記。
## このフェーズで「やらない」こと
- LoopFormOps / MirBuilder の広範な API 拡張や、大規模な構造変更。
- MIR スキャン本体の導入は 26-F ではなく 26-G 以降に分離し、ここではあくまで箱と受け口とドキュメントに留める。
- 既存の LoopForm v2 / Exit PHI ロジックの意味的変更。
- `NYASH_EXIT_LIVE_ENABLE` が未設定のときの挙動は、Phase 26-F-3 と同等に保つ(テストもそれを期待する)。
- StageB / Stage1 CLI / UsingResolver の本線仕様変更。
- 26-F で触るのはあくまで PHI/SSA のインフラ層のみ。高レベル仕様は 25.x の各フェーズに従う。
## 受け入れ条件26-F
- Docs:
- `docs/development/architecture/loops/loopform_ssot.md` に 4箱構成LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBoxの役割が追記されている。
- 本 README に ExitLiveness の受け口設計ExitLivenessProvider 相当と、MIR スキャン本体を次フェーズに送る方針が書かれている。
- コード:
- `NYASH_EXIT_LIVE_ENABLE` が未設定のとき、Phase 26-F-3 と同等かそれ以上のテスト結果PASS 増 / FAIL 減)を維持している。
- `LoopExitLivenessBox` / `BodyLocalPhiBuilder` / `PhiInvariantsBox` / `ExitPhiBuilder` の依存関係が一方向(解析→判定→生成→検証)に整理されている。
- テスト:
- `mir_funcscanner_skip_ws_direct_vm` / `mir_funcscanner_parse_params_trim_min_verify_and_vm` が引き続き「ExitLiveness/BodyLocal PHI カナリア」として動作し、PHI/SSA の変更時に必ず確認される位置づけになっている。

View File

@ -0,0 +1,109 @@
# Phase 26-G — Exit Liveness MIR Scan (実装フェーズ)
Status: planning
## ゴール(何を達成するフェーズか)
- ExitLivenessProvider の本実装MIR 命令列スキャン版を作り、FuncScanner のカナリア
- `mir_funcscanner_skip_ws_direct_vm`
- `mir_funcscanner_parse_params_trim_min_verify_and_vm`
を緑にする。
- 26-F で用意した差し替え口MirScanExitLivenessに、use/def ベースの liveness を実装する。
- `NYASH_EXIT_LIVE_ENABLE=1` で MIR スキャン版を有効化し、デフォルトは従来挙動のまま安全側。
## スコープ(このフェーズでやること)
- LoopFormOps / LoopBuilder / JSON bridge (LoopFormJsonOps) に「MirFunction への参照を返す API」を追加する設計と最小実装。
- MirScanExitLiveness を `MirQuery` ベースの use/def スキャンに置き換える。
- 代表テストFuncScanner カナリア)で緑を確認するところまで。
## 実装ステップ(順序)
### Step 1: API 設計と LoopFormOps 拡張
- 目的: Exit 側から MirFunctionまたは MirQueryに到達できるようにする。
- 具体案:
- `LoopFormOps` に「MirFunction への参照を返すメソッド」を追加する:
- 例: `fn mir_function(&self) -> &MirFunction;`
- 実装箇所:
- `impl<'a> LoopFormOps for LoopBuilder<'a>`:
- `self.parent_builder.current_function.as_ref().expect(...)` 経由で &MirFunction を返す。
- `impl LoopFormOps for LoopFormJsonOps<'_>`:
- 既に `f: &mut MirFunction` を保持しているので、その参照を返す。
- テスト用 `MockOps` など:
- 最小限の MirFunction スタブを持つ or `unimplemented!()` でテストを段階的に更新。
### Step 2: ExitPhiBuilder に MirQuery フックを追加
- 目的: Exit PHI 生成時に MirQuery にアクセスできるようにする。
- 具体案:
- `ExitPhiBuilder::build_exit_phis` のシグネチャを拡張:
- 現状: `fn build_exit_phis<O: LoopFormOps>(..., exit_snapshots, pinned, carrier) -> Result<(), String>`
- 追加: `mir_query: &dyn MirQuery` を引数に足す、または内部で `ops.mir_function()` から `MirQueryBox` を組み立てる。
- 検討:
- a) `build_exit_phis``&dyn MirQuery` を直渡し → テストで差し替えやすい。
- b) `LoopFormOps``mir_function()` を足し、`ExitPhiBuilder` 側で `MirQueryBox::new(ops.mir_function())` を作る。
- このフェーズでは b) 案を優先(既に LoopBuilder/JSON bridge は MirFunction を握っているため)。
### Step 3: MirScanExitLiveness の本実装use/def スキャン)
- 前提: Step 2 で `MirQueryBox` を構築できる。
- 実装方針:
- 入口:
- `MirScanExitLiveness::compute_live_at_exit(header_vals, exit_snapshots)` の中で
- `MirQueryBox``exit_blocks` を受け取れるようにする(必要なら `ExitLivenessProvider` トレイトのシグネチャ拡張も検討)。
- 最小アルゴリズム:
1. スキャン対象ブロック集合:
- exit ブロックLoopShape.exit exit_snapshots に現れる break 元ブロック。
2. 初期 live 集合:
- 対象ブロックの命令を後ろから走査し、`reads_of(inst)` を live に追加。
3. 1-step backward 伝播:
- それぞれのブロックで `writes_of(inst)` で kill、`reads_of(inst)` で add。
- `succs(bb)` が対象集合に含まれている場合、succ の live を bb に流す。
4. 固定点反復:
- live 集合が変わらなくなるまで 2〜3 を繰り返す。
5. 名前へのマッピング:
- header_vals / exit_snapshots に現れる `(name, ValueId)` を逆引きテーブルに集約し、
live に含まれる ValueId に対応する name だけを `live_at_exit` に含める。
- 返り値:
- `BTreeSet<String>`BodyLocalPhiBuilder からそのまま使える)。
- 既存ロジックとの整合:
- BodyLocalInternal でも「exit後で本当に live」なものだけが rescue されることが期待される。
### Step 4: ExitPhiBuilder / BodyLocalPhiBuilder 統合確認
- 目的: 新しい live_at_exit を使っても PhiInvariantsBox で落ちないことを確認する。
- 作業:
- `BodyLocalPhiBuilder::filter_exit_phi_candidates`
- `class.needs_exit_phi()` (Pinned/Carrier/BodyLocalExit) と
- `live_at_exit` + `is_available_in_all` の OR を行っていることを再確認。
- `PhiInvariantsBox::ensure_exit_phi_availability`
- 「選ばれた変数はすべての exit pred で定義済み」であることを保証し、
- それでも穴があれば即 Fail-Fast で教えてくれることを前提に、MirScan 側のロジックを調整。
### Step 5: カナリア検証
- コマンド:
- `NYASH_EXIT_LIVE_ENABLE=1 cargo test --release --lib mir_funcscanner_skip_ws_direct_vm`
- `NYASH_EXIT_LIVE_ENABLE=1 cargo test --release --lib mir_funcscanner_parse_params_trim_min_verify_and_vm`
- 期待:
- MirVerifier が `use of undefined value` を報告しない。
- VM 実行で RC が期待通りskip_ws の sentinel が正しく動き、trim/parse_params も undefined を出さない)。
### Step 6: ドキュメント更新
- `loopform_ssot.md` に:
- MirQuery / MirQueryBox の役割と ExitLiveness との関係。
- 「MirScanExitLiveness は 26-G で use/def スキャン実装済み」と記載。
- `phase-26-G/README.md` 自体も「実装完了」セクションを追記。
## やらないこと
- Loop 形状や PHI 生成ロジックの意味変更ExitPhiBuilder/BodyLocalPhiBuilder のアルゴリズム変更はしない)。
- env 名の変更や追加(既存の `NYASH_EXIT_LIVE_ENABLE` を継続利用)。
## 受け入れ条件
- `NYASH_EXIT_LIVE_ENABLE=0/未設定` で従来のテスト結果を維持。
- `NYASH_EXIT_LIVE_ENABLE=1` で FuncScanner カナリアが緑MirVerifier/VM
- docs 更新: 26-F/loopform_ssot に「MIR スキャン実装済み」を追記。

View File

@ -0,0 +1,217 @@
# Phase 26-H — JoinIR / 関数正規化フェーズ設計図
目的: これまで「構文 → LoopForm → PHI」で説明してきた制御構造を、もう一段抽象度を上げて「関数呼び出し継続」に正規化する中間層JoinIR / LoopFnIRとして整理し直すこと。
最終的には「ループや if の合流点で悩む」のではなく、「関数の引数と戻り先で意味が決まる」世界に寄せ、箱の数と責務を減らしていく。
このフェーズ 26H ではあくまで「設計とミニ実験」に留め、スモークや本線は既存の MIR/LoopForm ルートのまま維持する。
---
## 1. 現状: LoopForm 正規化ベースの世界
現在のパイプライン(概略):
```text
AST → MIR / LoopForm v2 → VM / LLVM
```
LoopForm v2 / PHI 周辺には、だいたい次のような箱が存在している:
- 構造系
- `LoopFormBuilder` / `LoopFormOps`
- `ControlForm`If/Loop の形と preds
- PHI 生成系
- `HeaderPhiBuilder`
- `ExitPhiBuilder`
- `BodyLocalPhiBuilder`
- `IfBodyLocalMergeBox`
- `PhiBuilderBox`If φ 統合)
- `PhiInvariantsBox`Fail-Fast チェック)
- 解析/分類系
- `LoopVarClassBox`Pinned / Carrier / BodyLocal*
- `LoopExitLivenessBox`ExitLiveness、実装は段階的
- `LocalScopeInspectorBox`
- if 解析系(`if_phi.rs` の補助群)
これらの箱が「どの変数がループをまたぐか」「どこで φ が必要か」「Exit で何を Live とみなすか」を決めているが、その分、箱の数と責務が多く、ループの形を変えるたびに PHI 側の負担が増えている。
---
## 2. 代案: 「関数を呼ぶ回数=ループ」というモデル
発想の転換:
- 今: 構文を LoopForm に正規化し、ループ構造header/body/latch/exitを中心に世界を説明している。
- 代案: 構文を「関数呼び出し」に正規化し、**関数を繰り返し呼ぶこと自体がループ**というモデルに寄せる。
### 2.1 ループの例
元のコード(擬似 Nyash:
```hako
var x = 0
loop {
x = x + 1
if x >= 10 { break }
}
print(x)
```
関数ループモデルで見ると:
```hako
// ループ一歩ぶんの関数Box
step(x, k_exit) {
if x >= 10 {
k_exit(x) // ループ終了して「先」に進む
} else {
step(x + 1, k_exit) // もう一周
}
}
// ループの「先」の処理
k_exit = (v) => {
print(v)
}
// 実行開始
step(0, k_exit)
```
- ループ = `step` を何回も呼ぶこと
- `break` = `k_exit(...)` を呼ぶこと
- `continue` = `step(...)` を呼ぶこと
- φ / LoopCarried 変数 = `step` の引数
ここでは「ループヘッダの φ で悩む」のではなく、「step の引数・k_exit の引数をどう定義するか」に責務が集中する。
### 2.2 パイプラインの再構成案
現在:
```text
AST → MIR / LoopForm v2 → VM/LLVM
```
ここに 1 段挟む:
```text
AST → MIR / LoopForm v2 → ★LoopFnIR(関数ループ層) → VM/LLVM
```
この LoopFnIR/JoinIR 層で:
- 各 LoopForm について「ループ関数(step) + 継続関数(k_exit)」を合成。
- ループの PHI / carrier / exit φ はすべて `step` / `k_exit` の引数として表現。
- 下流VM / LLVMは「関数呼び出しおよび再帰のループ化や展開」だけを見ればよい。
結果として:
- LoopForm v2 は「LoopFnIR を作る前段」に役割縮小。
- BodyLocal / Exit φ の詳細設計は「引数に何を持っていくか?」という関数インターフェース設計に吸収される。
---
## 4. このフェーズで実装する箱 / 概念ラベル
- 実装として増やす26-H 内で手を動かすもの)
- `join_ir.rs`: JoinIR 型(関数/ブロック/命令)+ダンプ
- LoopForm→JoinIR のミニ変換1 ケース限定で OK
- 実験トグル(例: `NYASH_JOINIR_EXPERIMENT=1`)で JoinIR をダンプするフック
- 概念ラベル27.x 以降に検討)
- MirQuery のようなビュー層reads/writes/succs を trait 化)
- LoopFnLoweringBox / JoinIRBox の分割や最適化パス
- VM/LLVM への統合
※ このフェーズでは「設計+ミニ実験のみ」で、本線スモークは既存 MIR/LoopForm 経路を維持する。
---
## 3. 箱の数と最終形のイメージ
### 3.1 現在の PHI/Loop 周辺の箱(概略)
ざっくりカテゴリ分けすると:
- 構造:
- `LoopFormBuilder`
- `ControlForm`
- PHI 生成:
- `HeaderPhiBuilder`
- `ExitPhiBuilder`
- `BodyLocalPhiBuilder`
- `IfBodyLocalMergeBox`
- `PhiBuilderBox`
- `PhiInvariantsBox`
- 解析:
- `LoopVarClassBox`
- `LoopExitLivenessBox`
- `LocalScopeInspectorBox`
- if 解析系IfAnalysisBox 的なもの)
関数正規化前提で進むと、最終的には:
- PHI を直接扱う箱は「LoopForm→LoopFnIR に変換する前段」に閉じ込める。
- LoopFnIR 導入後の本線では、次のような少数の箱が中心になる:
- `LoopFnLoweringBox`LoopForm → LoopFnIR / JoinIR
- `JoinIRBox`JoinIR の保持・最適化)
- 既存の VM/LLVM バックエンドJoinIR からのコード生成側)
という構造に寄せられる見込み。
このフェーズ 26H では、「最終的にそこに寄せるための設計図」を書くところまでを目標とする。
---
## 4. 26-H でやること(スコープ)
- JoinIR / LoopFnIR の設計ドキュメント作成
- 命令セットcall / ret / jump / 継続)の最小定義。
- if / loop / break / continue / return を JoinIR に落とす書き換え規則。
- φ = 関数引数、merge = join 関数、loop = 再帰関数exit 継続、という対応表。
- 最小 1 ケースの手書き変換実験MIR → JoinIR
- ループbreak を含む簡単な関数を 1 例だけ JoinIR に落とし、形を確認。
- MirQueryBox 経由で必要な MIR ビュー API の確認
- reads/writes/succs など、JoinIR 変換に必要な情報がすでに `MirQuery` で取れるかチェック。
- すべてトグル OFF で行い、本線MIR/LoopForm ルート)のスモークには影響させない。
---
## 5. やらないこと26-H では保留)
- 既存ルートMIR/LoopForm/VM/LLVMを JoinIR で置き換える。
- スモーク / CI のデフォルト経路変更。
- Loop/PHI 既存実装の削除(これは 27.x 以降の段階で検討)。
---
## 6. 実験計画(段階)
1. **設計シート**
- `docs/development/architecture/join-ir.md` に命令セット・変換規則・対応表を記述(φ=引数, merge=join, loop=再帰)。
2. **ミニ変換実験 1 ケース**
- 最小ループ(例: `loop(i < 3) { if i >= 2 { break } i = i + 1 } return i`)を MIR → JoinIR へ手書き変換し、テストでダンプを確認。VM/LLVM 実行までは行わない。
3. **トランスレータ骨格**
- `src/mir/join_ir.rs` などに型定義だけ追加(未配線、トグル OFF。MirQueryBoxreads/writes/succsで必要なビューが揃っているか確認。
4. **トグル付き実験**
- `NYASH_JOINIR_EXPERIMENT=1` などのトグルで最小ケースを JoinIR 変換・ダンプするルートを作る(デフォルト OFF でスモーク影響なし)。
---
## 7. 受け入れ基準(このフェーズ)
- docs に JoinIR / LoopFnIR の設計と変換規則が明記されている。
- 最小 1 ケースの JoinIR 変換がテストでダンプできるjoin/step/k_exit の形になっている)。
- 本線スモーク(既存 MIR ルート)は影響なし(トグル OFF
---
## 8. 次フェーズへの橋渡し
- 変換器を拡張して FuncScanner / StageB などカナリアを JoinIR で通す(トグル付き)。
- ExitLiveness や BodyLocal PHI の一部を LoopFnIR 側に吸収し、PHI/Loop 周辺の箱を徐々に減らす。
- VM/LLVM 実行経路に JoinIR を統合するのは 27.x 以降を想定し、当面は「設計+ミニ実験」に留める。

View File

@ -0,0 +1,69 @@
# Phase 26-H — JoinIR / 関数正規化 タスクチェックリスト
このフェーズは「設計ミニ実験」専用だよ。本線のスモークCI は既存の MIR/LoopForm 経路のまま維持しつつ、関数正規化JoinIR/LoopFnIRの足場を小さく固めることをゴールにする。
## A. 設計ドキュメントまわり
- [ ] A-1: join-ir 設計の骨格を固める
- [ ] `docs/development/architecture/join-ir.md` に:
- [ ] 命令セット(`call`, `ret`, 必要なら `jump`)の最小定義を明記する
- [ ] if / loop / break / continue / return → JoinIR の変換規則を列挙する
- [ ] φ = 関数引数 / merge = join 関数 / loop = 再帰 + exit 継続 の対応表を書く
- [ ] 26-H で「実装として本当に増やす箱」と、「概念ラベルとしてだけ残す箱」を分けてコメントしておく
- [ ] A-2: 26-H フェーズ README の整備
- [ ] `docs/development/roadmap/phases/phase-26-H/README.md` に:
- [ ] 現在の箱群LoopForm/PHI/解析系)の棚卸し
- [ ] 最終的に残したい箱セットLoopForm 前段 + LoopFnIR/JoinIR + 最小解析箱)を 1 セクションでまとめる
- [ ] 「このフェーズでは設計+ミニ実験のみ、本線は既存ルート」というスコープ/制限を明記する
## B. JoinIR 型・変換の最小実装
- [ ] B-1: JoinIR 型定義ファイルの追加
- [ ] `src/mir/join_ir.rs` を追加し、以下を定義する最小限でOK:
- [ ] `JoinFunction`(名前・引数・ブロック一覧)
- [ ] `JoinBlock`(ラベルと命令列)
- [ ] `JoinInst`call/ret などのバリアント)
- [ ] `Debug`/`Display` などダンプに必要な実装だけ付ける(最初は `Debug` だけでも可)
- [ ] B-2: LoopForm→JoinIR ミニ変換関数1ケース用
- [ ] まずは **汎用変換ではなく、特定の小関数だけを対象にした試験的関数** を 1つ実装する:
- 例: `fn lower_simple_loop_to_joinir(mir: &MirModule, func_name: &str) -> Option<JoinFunction>`
- [ ] ルールは join-ir.md の「Loop→step/k_exit」変換に従って手作業気味でよいこのフェーズでは general solution を目指さない)
## C. 最小 .hako + テスト(カナリア)
- [ ] C-1: 最小 JoinIR 用 .hako を追加
- [ ] `apps/tests/joinir_min_loop.hako`(例)を作成:
- 例: `static box JoinIrMin { main() { var i = 0; loop(i < 3) { if i >= 2 { break } i = i + 1 } return i } }`
- [ ] C-2: Rust テストで MIR→JoinIR ダンプを確認
- [ ] `src/tests/mir_joinir_min.rs`(仮ファイル)を追加し:
- [ ] 上記 .hako を AST→MIR までコンパイル
- [ ] `lower_simple_loop_to_joinir` を呼んで `JoinFunction` を生成
- [ ] `eprintln!("{:?}", join_fn)` などで形を確認(当面は assert よりも形の手動確認を重視)
- [ ] テストは **デフォルト ON でも軽い** ことを確認(重ければ `#[ignore]` + 手動実行でもOK
## D. トグル付き実験フック
- [ ] D-1: 実験用トグルの追加
- [ ] `NYASH_JOINIR_EXPERIMENT=1` のときだけ JoinIR ダンプを有効にするパスを Runner 側に追加する(例: `--debug-joinir-min` に相乗りでも可)
- [ ] トグル OFF のときは既存の MIR/LoopForm 経路しか走らないことを確認する
## E. ループ/PHI ラインとの関係メモ
- [ ] E-1: 「どの箱が将来 JoinIR に吸収されるか」を一覧にする
- [ ] `loopform_ssot.md` か 26-H README に:
- [ ] Header/Exit PHI まわりの箱で、JoinIR 導入後に **不要or縮退** するもの
- [ ] LoopForm / ControlForm は「前段の構造箱」として残すもの
- [ ] 解析系LoopVarClassBox / LoopExitLivenessBox などのうち、JoinIR 後も必要なもの
を簡易な表にして残す
- [ ] E-2: 「今はここまで、27.x でここまで」を線引きする
- [ ] 26-H のスコープ: 設計+最小 JoinIR ダンプ
- [ ] 27.x 候補: JoinIR 経由で 1〜2 個の本番ループFuncScanner/Stage-Bを通す、PHI/ExitLiveness の一部を JoinIR 側に移す
---
このファイルは「26H でやることを一覧で見たいときのチェックリスト」として使ってね。
タスクが増えたら、このファイルに A/B/C… の形で足していく想定だよ。

View File

@ -1,6 +1,8 @@
# Stage1 Hakorune CLI DesignProposal
Status: design-only + Stage0 stub 実装済みPhase 25.1 時点では仕様策定と導線の整理まで。selfhost EXE 本体は未実装)
Status: design-only + Stage0 stub 実装済みPhase 25.1 時点では仕様策定と導線の整理まで。selfhost EXE 本体は未実装)
Phase 25.1 A-3: `.hako` 側 Stage1Cli skeleton に env-only 実処理を実装emit program-json / emit mir-json / run stub
ブリッジStage0 → `.hako` stub`NYASH_USE_STAGE1_CLI=1` / `STAGE1_EMIT_PROGRAM_JSON=1` 等の ENV で制御する。
## ゴール
@ -55,7 +57,8 @@ hakorune <command> [<subcommand>] [options] [-- script_args...]
| `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済みStage0 ブリッジ + `.hako` Stage1Cli 完了) |
| `check` | 将来の構文/型/using チェック(予約) | プレースホルダ(`[hakorune] check: not implemented yet` |
Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード90〜93やログ形式は docs と実装を同期済み。
Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード90〜93やログ形式は docs と実装を同期済み。
Phase 25.1 A-3 時点の stub 実装(`.hako` 側 Stage1Cliは env-only 仕様で、成功時 0 / 入力不足 96 / 無効モード 97 / 実行失敗 98 を返す。
### 実装ステータスPhase 25.1a+