Files
hakorune/docs/development/roadmap/phases/phase-25.1e/README.md
nyash-codex f74b7d2b04 📦 Hotfix 1 & 2: Parameter ValueId Reservation + Exit PHI Validation (Box-First Theory)
**箱理論に基づく根治的修正**:

## 🎯 Hotfix 1: Parameter ValueId Reservation (パラメータ ValueId 予約)

### 根本原因
- MirFunction counter が params.len() を考慮していなかった
- local variables が parameter ValueIds を上書き

### 箱理論的解決
1. **LoopFormContext Box**
   - パラメータ予約を明示的に管理
   - 境界をはっきりさせる

2. **MirFunction::new() 改善**
   - `initial_counter = param_count.max(1)` でパラメータ予約
   - Parameters are %0, %1, ..., %N-1

3. **ensure_counter_after() 強化**
   - パラメータ数 + 既存 ValueIds 両方を考慮
   - `min_counter = param_count.max(max_id + 1)`

4. **reserve_parameter_value_ids() 追加**
   - 明示的な予約メソッド(Box-First)

## 🎯 Hotfix 2: Exit PHI Predecessor Validation (Exit PHI 検証)

### 根本原因
- LoopForm builder が存在しないブロックを PHI predecessor に追加
- 「幽霊ブロック」問題

### 箱理論的解決
1. **LoopFormOps.block_exists() 追加**
   - CFG 存在確認メソッド
   - 境界を明確化

2. **build_exit_phis() 検証**
   - 非存在ブロックをスキップ
   - デバッグログ付き

### 実装ファイル
- `src/mir/function.rs`: Parameter reservation
- `src/mir/phi_core/loopform_builder.rs`: Context + validation
- `src/mir/loop_builder.rs`: LoopFormOps impl
- `src/mir/builder/stmts.rs`: Local variable allocation

### 業界標準準拠
-  LLVM IR: Parameters are %0, %1, ...
-  SSA Form: PHI predecessors must exist in CFG
-  Cytron et al. (1991): Parameter reservation principle

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 06:39:45 +09:00

260 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 25.1e — LoopForm PHI v2 Migration (Rust MIR)
Status: planningLoopForm/PHI 正規化フェーズ・挙動は変えないRust側のみ
## ゴール
- ループまわりの SSA / PHI 生成の「SSOT単一の正」を **LoopForm v2 + phi_core** に寄せて、Legacy 経路との二重管理を解消する。
- Stage1 / StageB / selfhost で見えている以下の問題を、LoopForm 側の設計として整理して直す:
- 複雑ループ(`Stage1UsingResolverFull._find_from/3` など)での「同一 ValueId の二重定義PHI vs 既存値)」。
- Merge blockヘッダ合流ブロックで、predecessor 定義値を PHI なしで読むことによる nondominating use。
- pinned 受信箱(`__pin$*@recv`)や Loop carrier 変数の PHI 対象範囲が曖昧で、legacy/local_ssa/LoopForm の責務が重なっている問題。
- 既存テストStageB 最小ハーネスselfhost CLI から見える「SSA/PHI 赤ログ」を、LoopForm v2 経路を正とすることで構造的に潰す。
## 前提 / これまでにやったこと25.1d まで)
- Local 変数の SSA 化:
- `build_local_statement` を修正し、`local a = expr` ごとに新しい ValueId を払い出して `Copy` 命令で初期化値をコピーするように統一。
- `src/tests/mir_locals_ssa.rs``local a,b,c` パターンを固定し、Const/NewBox とローカル変数レジスタが分離されることを確認済み。
- Callee 解決/ガード:
- `CalleeBoxKind` / `CalleeResolverBox` / `CalleeGuardBox` の導入により、StageB / Stage1 の static compiler Box と runtime Box の混線を構造的に防止。
- `StageBArgsBox.resolve_src/1` 内の `args.get(i)``Stage1UsingResolverBox.get` に化ける問題は解消済み。
- Loop/PHI まわりの scaffolding:
- `phi_core::loop_phi::{prepare_loop_variables_with, seal_incomplete_phis_with, build_exit_phis_with}``LoopPhiOps` を導入し、LoopBuilder から PHI 生成を委譲可能な構造は整備済み。
- LoopForm v2 (`LoopFormBuilder` + `LoopFormOps`) は導線のみ実装済みで、既定では legacy 経路(`build_loop_legacy`)が使われている。
残っている問題は、主に legacy LoopBuilder / loop_phi / LoopForm v2 の責務が重なっているところだよ。
## 方針25.1e
- **SSOT を決める**:
- ループの PHI 生成の「正」は LoopForm v2 (`LoopFormBuilder` + `LoopFormOps` + `phi_core::loop_phi/if_phi`) に置く。
- `build_loop_legacy` + `prepare_loop_variables_with` は「互換レイヤ/移行レイヤ」と位置づけ、最終的には LoopForm v2 の薄いラッパに縮退させる。
- **Feature Flag で段階導入**:
- `NYASH_LOOPFORM_PHI_V2=1` のときだけ LoopForm v2 経路を使い、当面は StageB / Stage1 / selfhost 用テストの中でピンポイントに有効化する。
- デフォルト挙動は変えない(フラグ未設定時はこれまでどおり legacy 経路)。
- **1 バグ 1 パターンで前進**:
- `mir_stage1_using_resolver_full_collect_entries_verifies` や StageB 最小ハーネスで見えている赤ログは、それぞれ最小 Hako に絞って LoopForm v2 側で再現・修正する。
- 同時に legacy 側からは同じ責務PHI 生成ロジック)を抜いていき、二重管理を減らす。
## タスク粒度(やることリスト)
### 1. LoopForm v2 の足場をテストで固める
1.1 LoopForm v2 専用テストモードの追加
- `mir_stage1_using_resolver_full_collect_entries_verifies` をベースに、`NYASH_LOOPFORM_PHI_V2=1` を立てた状態でのみ走るサブテストを追加。
- 目的: v2 経路で `_find_from` ループの SSA/PHI が整合していることを検証する足場を作る(まだ赤でもよい)。
1.2 StageB 最小ハーネス用ループ抜き出しテスト
- `lang/src/compiler/tests/stageb_min_sample.hako` から、代表的なループだけを抜き出した Hako を作り、LoopForm v2 経路(`NYASH_LOOPFORM_PHI_V2=1`)で `MirVerifier` を通すテストを追加。
- 目的: StageB / selfhost CLI で見えているループ系のバグを、純粋な LoopForm/PHI 問題として Rust テストに落とし込む。
### 2. Legacy / v2 の責務切り分け
2.1 prepare_loop_variables の責務縮小(実施中の変更の整備)
- 既に導入したフィルタリング:
- `prepare_loop_variables` が preheader の `variable_map` 全体ではなく、「ループキャリア変数body で再代入されるもの)+ pinned 変数(`__pin$*`)」だけを PHI 対象にするように変更。
- 効果: `text_len` / `pattern_len` などループ不変なローカルに PHI を張らないことで、ValueId の二重定義/UseBeforeDef が起きにくくなる。
- 25.1e では、この変更を LoopForm v2 側の設計として明文化し、legacy 側のコメントやドキュメントもそれに揃える。
2.2 LoopForm v2 での PHI 生成を SSOT にする
- `LoopFormBuilder::prepare_structure` / `emit_preheader` / `emit_header_phis` の挙動をドキュメント化し、「どの変数に PHI を張るか」のルールを固定:
- 関数パラメータ 明示的なループキャリア変数body 内で再代入される)+ pinned 変数のみ。
- ループ不変変数は preheader の値をそのまま使い、PHI を作らない。
- `build_loop_legacy` の PHI 補助ロジックheader/exit での snapshot + PHI 生成は、LoopForm v2 のロジックと重複しないように段階的に削る。
### 3. mir_stage1_using_resolver_full_collect_entries の赤ログ解消
- 現状の代表的なエラー:
- `Value %24 / %25 / %26 defined multiple times (bb53 vs bb54)`
- `Merge block bb54 uses predecessor-defined value %28/%29/%27 from bb59/bb61 without Phi`
- タスク:
1. `_find_from` ループに対応する Hako 断片を LoopForm v2 経路でミニテスト化。
2. LoopForm v2 側で:
- preheader/header/latch/exit の各ブロックに対して、どの変数が carrier/pinned なのかを明示的に計算。
- PHI dst の ValueId 割り当てを `MirFunction::next_value_id` に完全委譲し、既存 SSA 値と衝突しないようにする。
- header での PHI 再定義(`%24` copy + `phi %24` のような形)を避けるため、古い値と新しい値のバインディングを LoopForm 内部で完結させる。
3. 修正後、`mir_stage1_using_resolver_full_collect_entries_verifies` が LoopForm v2 経路で緑になることを確認。
### 4. StageB / selfhost CLI への波及
- StageB 最小ハーネス(`tools/test_stageb_min.sh`)と selfhost CLI スモークを、LoopForm v2 経路で試験的に実行:
- `NYASH_LOOPFORM_PHI_V2=1` にした状態で Test2`compiler_stageb.hako` 経由)と selfhost CLI を実行し、ValueId 未定義や PHI 不整合が減っていることを確認。
- 25.1e のスコープでは、「v2 経路での挙動が legacy より悪化しない(少なくとも同程度、可能なら改善)」ことを目標に、必要最小限の修正に留める。
## 設計図 — LoopForm v2 Scope モデル
25.1e では「すべてのループif/else を LoopForm v2 のスコープとして見る」という前提で設計を固める。
ここでは **スコープ単位の入出力と break/continue の扱い** を明文化する。
### 1. 基本モデルLoopScope / IfScope
- すべての制御構造は「スコープ」として扱う:
- `LoopScope`: `while/loop` 相当Nyash の `loop (cond) { ... }`)。
- `IfScope`: `if (cond) { then } else { else }`
- 各スコープは次の情報を持つ:
- 入力: `Env_in` … スコープ入口時点の `variable_map`名前→ValueId
- 出力: `Env_out` … スコープ出口時点の `variable_map`
- 内部状態:
- `Carriers`: ループ本体で再代入される変数名の集合。
- `Pinned`: ループをまたいで保持する必要がある値(`__pin$*@recv``me` の一部など)。
- `BreakSnaps`: break 到達時の `Env` スナップショットの集合。
- `ContinueSnaps`: continue 到達時の `Env` スナップショットの集合。
LoopForm v2 は「各スコープの `Env_in``Env_out` を定義し、SSA/PHI をその範囲で完結させる」ことを目標にする。
### 2. LoopScope の形状とブロック
LoopScope は LLVM の canonical form に従う:
```text
preheader → header (PHI) → body → latch → header
↘ exit
```
- preheader:
- ループに入る直前のブロック。
- `Env_in(loop)` をそのまま保持し、loop entry 用の Copy をここで emit するCarrier/Pinned のみ)。
- header:
- ループ条件・合流点。
- Entry 時点では preheader からの Copy を入力に PHI を seed するだけlatch/continue は後で seal
- body:
- ループ本体。`Carriers` に対する再代入や break/continue が発生する。
- latch:
- body の末尾ブロックcontinue でヘッダに戻る前に通る最後のブロック)。
- `Env_latch` として LoopForm v2 に引き継がれ、header PHI の backedge 値に使われる。
- exit:
- ループ脱出ブロック。`BreakSnaps` と header fall-through をマージして exit PHI を作る。
### 3. 変数分類 — Carrier / Pinned / Invariant
- Carrier:
- ループ本体body**再代入される** 変数。
- 例: `i`, `a`, `b` などのインデックスや累積値。
- LoopScope では:
- header entry で `phi(entry, latch)` を必ず持つ。
- exit でも header 値と break 値を PHI でマージする。
- Pinned:
- `me` レシーバや `__pin$*@recv` のように、ループをまたいで同じ Box を指し続ける必要がある値。
- ループ内の再代入はほとんどないが、「PHI に乗せておかないと次のイテレーションで UseBeforeDef になる」種類の値。
- LoopScope では:
- header PHI の入力として preheader の Copy を用意する(`prepare_structure` 相当)。
- break/continue/exit でも pinned 値が破綻しないように header/exit PHI に含める。
- Invariant:
- ループ内で再代入されない、純粋な不変ローカル・パラメータ。
- 例: `text_len`, `pattern_len` のような長さ。
- LoopScope では:
- preheader 値をそのまま使い、PHI には乗せないValueId の二重定義を避ける)。
LoopForm v2 のルール:
- **PHI の対象は Carrier + Pinned のみ**。Invariant は preheader の値を直接参照する。
### 4. break / continue の扱い
#### 4.1 continue
- continue は「現在のループの latch ブロックにジャンプして header に戻る」。
- LoopScope では:
- `ContinueSnaps``(block_id, VarSnapshot)` を記録するblock_id は continue が現れたブロック)。
- `seal_phis` 実行時に:
- `ContinueSnaps` のスナップショットから Carrier/Pinned 変数の値を集め、
- header の IncompletePhi`IncompletePhi { var_name, phi_id, known_inputs }`)に `(continue_block, value)` を追加する。
- 条件: continue は「現在のループスコープ」からのみ脱出し、外側のスコープには影響しない。
#### 4.2 break
- break は「現在のループを脱出し、exit ブロックへ遷移する」。
- LoopScope では:
- `BreakSnaps``(block_id, VarSnapshot)` を記録する。
- `build_exit_phis` 実行時に:
- header fall-through の Snapshot header_exit_snapshot`BreakSnaps` をマージし、
- exit ブロックで PHI を生成する:
- 1 predecessor のみ → 直接 bind。
- 2 つ以上 → `phi(header, break1, break2, ...)` を作る。
- ここでも PHI 対象は Carrier + Pinned のみ。Invariant は preheader/header の値で十分。
### 5. スコープ入出力と変数の「渡し方」
#### 5.1 LoopScope の Env 入出力
- 入力: `Env_in(loop)` = ループ直前preheader 手前)の `variable_map`
- LoopForm v2 はここから:
- Carrier/Pinned を抽出して PHI 用の構造を準備。
- preheader に必要な Copy を emit。
- 出力: `Env_out(loop)` = exit ブロック直後の `variable_map`
- Carrier/Pinned は exit PHI の結果に更新される。
- Invariant は `Env_in(loop)` の値をそのまま引き継ぐ。
LoopScope の契約:
- 「ループの外側から見える変数」は Carrier/Pinned に限らず全部だが、
- ループ内で変わり得るのは Carrier/Pinned。
- ループ内で決して変わらないものは Envin と Envout で同じ ValueId になる。
#### 5.2 IfScope の Env 入出力
- IfScope も同様に:
- `Env_in(if)` = pre-if スナップショット。
- `Env_then_end`, `Env_else_end` を計算し、
- 変化した変数についてのみ merge PHI を生成(`phi_core::if_phi::merge_modified_at_merge_with`)。
- LoopScope と組み合わせる場合loop header 内の if など)は:
- LoopForm が header/body/latch の枠を作り、
- その中の if は IfScope として φ を 張る。
- LoopScope は「if によって更新された Carrier/Pinned の最終値」を snapshot として扱い、次の header/latch PHI の入力に使う。
### 6. break/continue を含む複雑パターン
代表的な難パターン:
- ループ内 if の中で break/continue が出るケース:
```hako
loop (i < n) {
local ch = text.substring(i, i+1)
if ch == " " {
i = i + 1
continue
}
if ch == "," {
break
}
i = i + 1
}
```
LoopForm v2 での扱い:
- Carrier: `i`
- Pinned: `text`, `n`、必要に応じて pinned recv`__pin$*@recv`)。
- IfScope 内で:
- `continue``ContinueSnaps` に i/text/n のスナップショットを保存。
- `break``BreakSnaps` に同様のスナップショットを保存。
- seal/build_exit 時:
- header PHI: `i` と pinned recv のみを対象に、preheader/latch/continue からの値を統合。
- exit PHI: `i` や pinned recv を header fall-through と break ブロックから統合。
これにより、「どのスコープからどの変数が外に出るか」「break/continue でどの値が生き残るか」が LoopForm v2 の規則で明示される。
### 7. この設計図の適用範囲
- 対象:
- Rust MIR builder (`MirBuilder` + `LoopBuilder` + `LoopFormBuilder`) が生成するすべての loop/if/else 構造。
- JSON v0 Bridge の loop loweringBridge 側にも LoopFormOps 実装を追加して同じアルゴリズムを使う)。
- スコープ外25.1e 時点):
- Nyash `.hako` 側 MirBuilderselfhost builderの loop/if 実装。
- try/catch/finally の完全な LoopForm への統合(現状は独自の cf_try_catch ルールで SSA を保っている)。
25.1e では、この設計図をベースに「_find_from ループ」「StageB 最小ループ」などの代表ケースから LoopForm v2 に寄せていき、
legacy LoopBuilder 側から重複ロジックを削っていくのが次のステップになる。
## スコープ外 / 後続フェーズ候補
- Nyash 側 MirBuilder.hako 実装)の LoopForm 対応:
- ここでは Rust MIR builder 側の LoopForm/PHI を整えることに集中し、`.hako` 側 MirBuilder への LoopForm 移植は Phase 25.1f 以降のタスクとする。
- ループ最適化unrolling / strength reduction など):
- 25.1e はあくまで「正しい SSA/PHI を作る」のが目的であり、性能最適化は Phase 26+ で扱う。
## メモ(現状の観測ログ)
- `mir_stage1_using_resolver_full_collect_entries_verifies` 実行時:
- `_find_from` / `collect_entries` 内で多数の PHI が生成されているが、header/merge ブロックで既存の ValueId と衝突して `Value %N defined multiple times` が発生。
- merge ブロックbb54 など)で predecessorbb59, bb61由来の値を PHI なしで読んでいる箇所があり、`nondominating use` / `Merge block uses predecessor-defined value without Phi` がレポートされている。
- `LoopBuilder::prepare_loop_variables` による「全変数 PHI 化」は、LocalSSALoopForm の両方が入った状態では過剰であり、キャリアpinned のみに制限する必要があることが分かっている。