# Phase 33‑16: Loop Header PHI as Exit SSOT — Design 日付: 2025‑12‑07 状態: 設計フェーズ完了(実装前の設計メモ) このフェーズでは、JoinIR → MIR マージ時の「ループ出口値の扱い」を、 - expr 結果ライン(`loop` を式として使うケース) - carrier ライン(`start/end/sum` など状態更新だけを行うケース) で構造的に整理し直すよ。 目的は: 1. SSA‑undef を根本的に防ぐ(継続関数パラメータを PHI 入力に使わない) 2. ループヘッダ PHI を「ループ変数の真の現在値」の SSOT にする 3. ExitLine(ExitMeta/ExitBinding/ExitLineReconnector)と expr PHI をきれいに分離する --- ## 1. 現状の問題整理 ### 1.1 何が壊れているか - Pattern2(`joinir_min_loop.hako`)などで: - 以前は `Return { value: i_param }` をそのまま exit PHI の入力に使っていた - JoinIR のパラメータ `i_param` は、MIR に inline された時点では **SSA 定義を持たない** - そのため PHI 入力に remap された ValueId が未定義になり、SSA‑undef が発生していた - Phase 33‑15 では: - `value_collector` から JoinIR パラメータを除外 - `instruction_rewriter` の `exit_phi_inputs` / `carrier_inputs` 収集を一時停止 - → SSA‑undef は解消したが、「ループ出口値」が初期値のままになっている ### 1.2 何が足りていないか 本来必要だったものは: 1. ループヘッダに置かれる PHI(`i_phi`, `sum_phi` など)を、 「ループの現在値」として扱う仕組み(構造上の SSOT)。 2. expr 結果ライン: - 「どの ValueId が `loop` 式の最終結果か」を Loop ヘッダ PHI を通じて知ること。 3. carrier ライン: - ExitLine が、ヘッダ PHI の `dst` をキャリア出口として利用できるようにすること。 今まではこの部分が暗黙のまま進んでいたため、JoinIR パラメータに依存した不安定な出口線になっていた。 --- ## 2. マージパイプラインと Phase 3.5 追加 現在の JoinIR → MIR マージパイプラインは概念的にこうなっている: 1. Phase 1: block_allocator JoinIR 関数群をホスト MIR 関数に inline するためのブロック ID の「型」を決める。 2. Phase 2: value_collector JoinIR 内で必要となる ValueId を収集し、remap のための準備をする。 3. Phase 3: remap_values JoinIR の ValueId → MIR 側の ValueId にマップする。 4. Phase 4: instruction_rewriter Return → Jump, Call → Jump など命令を書き換える。 5. Phase 5: exit_phi_builder expr 結果用の exit PHI を生成する。 6. Phase 6: exit_line ExitMetaCollector + ExitLineReconnector で carrier を variable_map に反映。 Phase 33‑16 ではここに **Phase 3.5: LoopHeaderPhiBuilder** を追加する: ```text Phase 1: block_allocator Phase 2: value_collector Phase 3: remap_values Phase 3.5: loop_header_phi_builder ← NEW Phase 4: instruction_rewriter Phase 5: exit_phi_builder Phase 6: exit_line ``` LoopHeaderPhiBuilder の責務は: - ループヘッダブロックを特定し - そのブロックに PHI を生成して - 各キャリアの「現在値」の `dst` を決める - expr 結果として使うべき値(あれば)を決める - それらを構造体で返す --- ## 3. LoopHeaderPhiInfo 箱 ### 3.1 構造 ```rust /// ループヘッダ PHI の SSOT pub struct LoopHeaderPhiInfo { /// ヘッダブロック(PHI が置かれるブロック) pub header_block: BasicBlockId, /// 各キャリアごとに「ヘッダ PHI の dst」を持つ /// 例: [("i", v_i_phi), ("sum", v_sum_phi)] pub carrier_phis: Vec, /// ループを式として使う場合の expr 結果 PHI /// ない場合は None(キャリアのみのループ) pub expr_result_phi: Option, } pub struct CarrierPhiEntry { pub name: String, // carrier 名 ("i", "sum" 等) pub dst: ValueId, // その carrier のヘッダ PHI の dst } ``` ### 3.2 入口 / 出口 LoopHeaderPhiBuilder は次の入力を取るイメージだよ: - `loop_header: BasicBlockId` Pattern lowerer / ルーティングで「ここがヘッダ」と決めたブロック。 - `carrier_names: &[String]` `CarrierInfo` や `LoopFeatures` から得たキャリア名一覧。 - `join_fragment_meta: &JoinFragmentMeta` expr 結果があるかどうかのフラグ(後述)。 そして `LoopHeaderPhiInfo` を返す: ```rust fn build_loop_header_phis( func: &mut MirFunction, loop_header: BasicBlockId, carrier_names: &[String], fragment_meta: &JoinFragmentMeta, ) -> LoopHeaderPhiInfo; ``` --- ## 4. JoinFragmentMeta / ExitMeta / Boundary との接続 ### 4.1 JoinFragmentMeta すでに次のような形になっている: ```rust pub struct JoinFragmentMeta { pub expr_result: Option, // JoinIR ローカルの expr 結果 pub exit_meta: ExitMeta, // carrier 用メタ } ``` Phase 33‑16 では: - lowerer 側(Pattern2 等)は「Loop header PHI を前提にした expr_result」を設定する方向に揃える。 - もしくは LoopHeaderPhiBuilder の結果から `expr_result_phi` を JoinFragmentMeta に反映する。 - 重要なのは、「expr_result に JoinIR パラメータ(`i_param` 等)を直接入れない」こと。 ### 4.2 ExitMeta / ExitLine 側 ExitMeta / ExitLine は既に carrier 専用ラインになっているので、方針は: - ExitMeta 内の `join_exit_value` は「ヘッダ PHI の dst」を指すようにする。 - LoopHeaderPhiInfo の `carrier_phis` から情報を取る形にリファクタリングする。 - これにより、ExitLineReconnector は単に ```rust variable_map[carrier_name] = remapper.remap(phi_dst); ``` と書けるようになる。 ### 4.3 JoinInlineBoundary Boundary 構造体自体には新フィールドを足さず、 - join_inputs / host_inputs(ループパラメータ) - condition_bindings(条件専用変数) - exit_bindings(ExitMetaCollector が作るキャリア出口) の契約はそのまま維持する。LoopHeaderPhiInfo は merge フェーズ内部のメタとして扱う。 --- ## 5. 変更範囲(Module boundaries) ### 5.1 触る予定のファイル | ファイル | 予定される変更 | |---------------------------------------------------|-----------------------------------------------| | `src/mir/builder/control_flow/joinir/merge/mod.rs` | merge パイプラインに Phase 3.5 呼び出しを追加 | | `merge/loop_header_phi_builder.rs` (NEW) | LoopHeaderPhiBuilder 実装 | | `merge/exit_phi_builder.rs` | LoopHeaderPhiInfo を受け取って expr PHI を構築 | | `merge/instruction_rewriter.rs` | 33‑15 の暫定 skip ロジックを削除し、LoopHeaderPhiInfo を前提に整理 | | `merge/exit_line/reconnector.rs` | carrier_phis を入口として使うように変更(ExitMeta と連携) | | `join_ir/lowering/loop_with_break_minimal.rs` 等 | LoopHeaderPhiBuilder に必要なメタの受け渡し | ### 5.2 触らない場所 - `CarrierInfo` / `LoopUpdateAnalyzer` - `ExitMetaCollector` - `JoinInlineBoundary` 構造体(フィールド追加なし) - BoolExprLowerer / condition_to_joinir これらは既に箱化されており、Loop ヘッダ PHI だけを中継する小箱を増やせば整合性を保てる設計になっている。 --- ## 6. テスト戦略(完了条件) ### 6.1 expr 結果ライン - `apps/tests/joinir_min_loop.hako` - 期待: RC が「ループ終了時の i」の値になること(現在は 0)。 - SSA トレース: `NYASH_SSA_UNDEF_DEBUG=1` で undefined が出ないこと。 ### 6.2 carrier ライン - `local_tests/test_trim_main_pattern.hako` など trim 系: - 期待: start/end が正しく更新され、既存の期待出力から変わらないこと。 - ExitLineReconnector が LoopHeaderPhiInfo 経由でも正常に variable_map を更新すること。 ### 6.3 回帰 - 既存 Pattern1–4 のループテスト: - 結果・RC・SSA すべて元と同じであること。 --- ## 7. 次のフェーズ この設計フェーズ(33‑16)はここまで。 次の 33‑16 実装フェーズでは: 1. `loop_header_phi_builder.rs` を実装し、LoopHeaderPhiInfo を実際に構築。 2. `merge/mod.rs` に Phase 3.5 を組み込み。 3. `exit_phi_builder.rs` / `exit_line/reconnector.rs` / `instruction_rewriter.rs` を LoopHeaderPhiInfo 前提で整理。 4. 上記テスト戦略に沿って回帰テスト・SSA 検証を行う。 実装時に設計が変わった場合は、このファイルと `joinir-architecture-overview.md` を SSOT として必ず更新すること。