【変更内容】 1. LoopExitKind enum定義 - Break / Continue の型安全な区別 2. do_loop_exit() 共通メソッド作成(47行) - スナップショット取得(共通処理) - kind別のスナップショット保存 - kind別のジャンプターゲット - unreachable ブロック切り替え(共通処理) 3. do_break/continue をthin wrapperに変換 - do_break: 13行 → 4行 - do_continue: 12行 → 4行 - 合計21行削減 【効果】 - 構造改善: break/continue の共通ロジック一箇所に集約 - 保守性向上: デバッグログなどの共通処理が統一管理 - 拡張性向上: labeled break/continue等の将来拡張が容易 【検証結果】 - ビルド成功(警告なし) - mir_stageb_loop_break_continue_verifies: PASS - /tmp/loop_continue_fixed.hako: RC=3(期待通り) 関連: Phase 25.1m (continue PHI修正), Phase 25.1n (レガシー削除)
16 KiB
Phase 25.1e — LoopForm PHI v2 Migration (Rust MIR)
Status: planning(LoopForm/PHI 正規化フェーズ・挙動は変えない/Rust側のみ)
ゴール
- ループまわりの SSA / PHI 生成の「SSOT(単一の正)」を LoopForm v2 + phi_core に寄せて、Legacy 経路との二重管理を解消する。
- Stage‑1 / Stage‑B / selfhost で見えている以下の問題を、LoopForm 側の設計として整理して直す:
- 複雑ループ(
Stage1UsingResolverFull._find_from/3など)での「同一 ValueId の二重定義(PHI vs 既存値)」。 - Merge block(ヘッダ/合流ブロック)で、predecessor 定義値を PHI なしで読むことによる non‑dominating use。
- pinned 受信箱(
__pin$*@recv)や Loop carrier 変数の PHI 対象範囲が曖昧で、legacy/local_ssa/LoopForm の責務が重なっている問題。
- 複雑ループ(
- 既存テスト/Stage‑B 最小ハーネス/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の導入により、Stage‑B / Stage‑1 の 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 の薄いラッパに縮退させる。
- ループの PHI 生成の「正」は LoopForm v2 (
- Feature Flag で段階導入(※この段階の設計メモ):
- 当初は
NYASH_LOOPFORM_PHI_V2=1のときだけ LoopForm v2 経路を使う案だったが、 現在は LoopForm v2 が既定実装となっており、legacy 経路は撤去済み。 NYASH_LOOPFORM_PHI_V2は互換性のために残っているが、挙動切り替えには使われない。
- 当初は
- 1 バグ 1 パターンで前進:
mir_stage1_using_resolver_full_collect_entries_verifiesや Stage‑B 最小ハーネスで見えている赤ログは、それぞれ最小 Hako に絞って LoopForm v2 側で再現・修正する。- 同時に legacy 側からは同じ責務(PHI 生成ロジック)を抜いていき、二重管理を減らす。
タスク粒度(やることリスト)
1. LoopForm v2 の足場をテストで固める
1.1 LoopForm v2 専用テストモードの追加(→ 現在は「LoopForm v2 が既定」の前提で完了)
mir_stage1_using_resolver_full_collect_entries_verifiesは LoopForm v2 前提で緑になっており、 もはやフラグは不要(テスト内のNYASH_LOOPFORM_PHI_V2設定も削除済み)。
1.2 Stage‑B 最小ハーネス用ループ抜き出しテスト
lang/src/compiler/tests/stageb_min_sample.hakoから、代表的なループだけを抜き出した Hako を作り、LoopForm v2 経路(NYASH_LOOPFORM_PHI_V2=1)でMirVerifierを通すテストを追加。- 目的: Stage‑B / 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
- タスク:
_find_fromループに対応する Hako 断片を LoopForm v2 経路でミニテスト化。- LoopForm v2 側で:
- preheader/header/latch/exit の各ブロックに対して、どの変数が carrier/pinned なのかを明示的に計算。
- PHI dst の ValueId 割り当てを
MirFunction::next_value_idに完全委譲し、既存 SSA 値と衝突しないようにする。 - header での PHI 再定義(
%24copy +phi %24のような形)を避けるため、古い値と新しい値のバインディングを LoopForm 内部で完結させる。
- 修正後、
mir_stage1_using_resolver_full_collect_entries_verifiesが LoopForm v2 経路で緑になることを確認。
4. Stage‑B / selfhost CLI への波及
- Stage‑B 最小ハーネス(
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 に従う:
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 でマージする。
- header entry で
- Pinned:
meレシーバや__pin$*@recvのように、ループをまたいで同じ Box を指し続ける必要がある値。- ループ内の再代入はほとんどないが、「PHI に乗せておかないと次のイテレーションで UseBeforeDef になる」種類の値。
- LoopScope では:
- header PHI の入力として preheader の Copy を用意する(
prepare_structure相当)。 - break/continue/exit でも pinned 値が破綻しないように header/exit PHI に含める。
- header PHI の入力として preheader の Copy を用意する(
- 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, ...)を作る。
- header fall-through の Snapshot (header_exit_snapshot)と
- ここでも 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。
- LoopForm v2 はここから:
- 出力:
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 が出るケース:
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 ブロックから統合。
- header PHI:
これにより、「どのスコープからどの変数が外に出るか」「break/continue でどの値が生き残るか」が LoopForm v2 の規則で明示される。
7. この設計図の適用範囲
- 対象:
- Rust MIR builder (
MirBuilder+LoopBuilder+LoopFormBuilder) が生成するすべての loop/if/else 構造。 - JSON v0 Bridge の loop lowering(Bridge 側にも LoopFormOps 実装を追加して同じアルゴリズムを使う)。
- Rust MIR builder (
- スコープ外(25.1e 時点):
- Nyash
.hako側 MirBuilder(selfhost builder)の loop/if 実装。 - try/catch/finally の完全な LoopForm への統合(現状は独自の cf_try_catch ルールで SSA を保っている)。
- Nyash
25.1e では、この設計図をベースに「_find_from ループ」「Stage‑B 最小ループ」などの代表ケースから LoopForm v2 に寄せていき、
legacy LoopBuilder 側から重複ロジックを削っていくのが次のステップになる。
スコープ外 / 後続フェーズ候補
- Nyash 側 MirBuilder(.hako 実装)の LoopForm 対応:
- ここでは Rust MIR builder 側の LoopForm/PHI を整えることに集中し、
.hako側 MirBuilder への LoopForm 移植は Phase 25.1f 以降のタスクとする。
- ここでは Rust MIR builder 側の LoopForm/PHI を整えることに集中し、
- ループ最適化(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 など)で predecessor(bb59, bb61)由来の値を PHI なしで読んでいる箇所があり、
non‑dominating use/Merge block uses predecessor-defined value without Phiがレポートされている。
LoopBuilder::prepare_loop_variablesによる「全変数 PHI 化」は、LocalSSA+LoopForm の両方が入った状態では過剰であり、キャリア+pinned のみに制限する必要があることが分かっている。