Implementation: - Add make_pattern2_scope_manager() helper for DRY - Header conditions use ExprLowerer for supported patterns - Legacy fallback for unsupported patterns - Fail-Fast on supported patterns that fail Tests: - 4 new tests (all pass) - test_expr_lowerer_supports_simple_header_condition_i_less_literal - test_expr_lowerer_supports_header_condition_var_less_var - test_expr_lowerer_header_condition_generates_expected_instructions - test_pattern2_header_condition_via_exprlowerer Also: Archive old phase documentation (34k lines removed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.9 KiB
Phase 33‑16: Loop Header PHI as Exit SSOT — Design
日付: 2025‑12‑07
状態: 設計フェーズ完了(実装前の設計メモ)
このフェーズでは、JoinIR → MIR マージ時の「ループ出口値の扱い」を、
- expr 結果ライン(
loopを式として使うケース) - carrier ライン(
start/end/sumなど状態更新だけを行うケース)
で構造的に整理し直すよ。
目的は:
- SSA‑undef を根本的に防ぐ(継続関数パラメータを PHI 入力に使わない)
- ループヘッダ PHI を「ループ変数の真の現在値」の SSOT にする
- 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 何が足りていないか
本来必要だったものは:
- ループヘッダに置かれる PHI(
i_phi,sum_phiなど)を、 「ループの現在値」として扱う仕組み(構造上の SSOT)。 - expr 結果ライン:
- 「どの ValueId が
loop式の最終結果か」を Loop ヘッダ PHI を通じて知ること。
- 「どの ValueId が
- carrier ライン:
- ExitLine が、ヘッダ PHI の
dstをキャリア出口として利用できるようにすること。
- ExitLine が、ヘッダ PHI の
今まではこの部分が暗黙のまま進んでいたため、JoinIR パラメータに依存した不安定な出口線になっていた。
2. マージパイプラインと Phase 3.5 追加
現在の JoinIR → MIR マージパイプラインは概念的にこうなっている:
-
Phase 1: block_allocator
JoinIR 関数群をホスト MIR 関数に inline するためのブロック ID の「型」を決める。 -
Phase 2: value_collector
JoinIR 内で必要となる ValueId を収集し、remap のための準備をする。 -
Phase 3: remap_values
JoinIR の ValueId → MIR 側の ValueId にマップする。 -
Phase 4: instruction_rewriter
Return → Jump, Call → Jump など命令を書き換える。 -
Phase 5: exit_phi_builder
expr 結果用の exit PHI を生成する。 -
Phase 6: exit_line
ExitMetaCollector + ExitLineReconnector で carrier を variable_map に反映。
Phase 33‑16 ではここに Phase 3.5: LoopHeaderPhiBuilder を追加する:
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 構造
/// ループヘッダ PHI の SSOT
pub struct LoopHeaderPhiInfo {
/// ヘッダブロック(PHI が置かれるブロック)
pub header_block: BasicBlockId,
/// 各キャリアごとに「ヘッダ PHI の dst」を持つ
/// 例: [("i", v_i_phi), ("sum", v_sum_phi)]
pub carrier_phis: Vec<CarrierPhiEntry>,
/// ループを式として使う場合の expr 結果 PHI
/// ない場合は None(キャリアのみのループ)
pub expr_result_phi: Option<ValueId>,
}
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 を返す:
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
すでに次のような形になっている:
pub struct JoinFragmentMeta {
pub expr_result: Option<ValueId>, // JoinIR ローカルの expr 結果
pub exit_meta: ExitMeta, // carrier 用メタ
}
Phase 33‑16 では:
- lowerer 側(Pattern2 等)は「Loop header PHI を前提にした expr_result」を設定する方向に揃える。
- もしくは LoopHeaderPhiBuilder の結果から
expr_result_phiを JoinFragmentMeta に反映する。
- もしくは LoopHeaderPhiBuilder の結果から
- 重要なのは、「expr_result に JoinIR パラメータ(
i_param等)を直接入れない」こと。
4.2 ExitMeta / ExitLine 側
ExitMeta / ExitLine は既に carrier 専用ラインになっているので、方針は:
- ExitMeta 内の
join_exit_valueは「ヘッダ PHI の dst」を指すようにする。- LoopHeaderPhiInfo の
carrier_phisから情報を取る形にリファクタリングする。
- LoopHeaderPhiInfo の
- これにより、ExitLineReconnector は単に
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/LoopUpdateAnalyzerExitMetaCollectorJoinInlineBoundary構造体(フィールド追加なし)- 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 実装フェーズでは:
loop_header_phi_builder.rsを実装し、LoopHeaderPhiInfo を実際に構築。merge/mod.rsに Phase 3.5 を組み込み。exit_phi_builder.rs/exit_line/reconnector.rs/instruction_rewriter.rsを LoopHeaderPhiInfo 前提で整理。- 上記テスト戦略に沿って回帰テスト・SSA 検証を行う。
実装時に設計が変わった場合は、このファイルと joinir-architecture-overview.md を SSOT として必ず更新すること。
Status: Historical