**Problem**: Phase 188.3 Pattern6 (nested loop) encounters infinite loop - inner_step → inner_step (self-recursion) incorrectly classified as BackEdge - → redirects to outer header (loop_step) instead of inner_step entry - is_recursive_call causes inner recursion to overwrite outer latch values - → PHI receives wrong values → i doesn't increment → infinite loop **Fix 1: BackEdge classification strictness** (tail_call_classifier.rs) - Add `is_target_loop_entry` parameter to classify_tail_call() - BackEdge ONLY when target==loop_step (entry_func), not inner_step - Prevents inner_step → inner_step from redirecting to outer header **Fix 2: latch_incoming guard** (instruction_rewriter.rs) - Change condition from `is_recursive_call || is_target_loop_entry` to `is_target_loop_entry` only - Prevents inner_step self-recursion from overwriting outer loop's latch - set_latch_incoming() now called with correct values (verified by debug) **Status**: 🚧 WIP - Infinite loop still occurs - set_latch_incoming('i', BasicBlockId(8), ValueId(21)) ✅ Called correctly - But final PHI: `phi [%4, bb8]` instead of `phi [%21, bb8]` ❌ - Root cause likely in PHI generation (merge/mod.rs), not latch_incoming - Next: Investigate why latch_incoming values aren't used in PHI **Files**: - src/mir/builder/control_flow/joinir/merge/tail_call_classifier.rs - src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
6.4 KiB
Phase 188.3 P1: Pattern6(NestedLoopMinimal)lowering を “実装済み” にする
Date: 2025-12-27
Scope: Pattern6 の lowering 実装(fixture を PASS)
Non-goals: 汎用 nested loop / break/continue / 多段ネスト / 多重 inner loop
✅ Success Criteria
apps/tests/phase1883_nested_minimal.hakoが--backend vmで exit=9./tools/smokes/v2/run.sh --profile quickが 154/154 PASS- integration selfhost が FAIL=0 維持
- Pattern6 を選んだ後に silent fallback しない(Fail-Fast)
SSOT / Constraints
- ネスト深さ SSOT:
StepTreeFeatures.max_loop_depth - Pattern6 選択 SSOT:
src/mir/builder/control_flow/joinir/routing.rs::choose_pattern_kind() - Phase 188.2: strict では depth > 2 を明示エラー
- 本タスクで触るのは “lowering stub を実装” のみ
Fixture(目標)
apps/tests/phase1883_nested_minimal.hako を SSOT とする(Add/Compare のみ)。
実装方針(重要)
1) sum は carrier として渡す(グローバル禁止)
inner loop が outer の sum を更新しているので、JoinIR では sum を引数で運び、k_inner_exit で outer に戻す。
sumを “グローバル変数” 扱いにしてはいけない(箱理論と Fail-Fast 的にも事故る)
2) merge の「loop_step 選定」を壊さない
JoinIR merge は「main でも continuation でもない関数」を 1つ選んで “loop header” とみなす。
nested loop では inner_step が混ざるので、inner_step / k_inner_exit を boundary の continuation_func_ids に入れて除外する。
これをしないと、merge が誤って inner_step を loop header として選び、PHI/exit binding が壊れる。
実装タスク(順番)
Task A: lowering を実装する
対象: src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs
やること:
lower()でErrしている stub を置き換えて、JoinIR pipeline を呼ぶ- 最小形だけ対応し、外れた形は Pattern6 選択前に落とす(
is_pattern6_lowerable()側を強化)か、ここで明示エラー
推奨構成(Pattern1 と同じ流儀):
- JoinIR 生成は
src/mir/join_ir/lowering/nested_loop_minimal.rs(新規)に切り出す - builder 側は「context 作成 → JoinModule → boundary → conversion pipeline」のみ
(ただし PoC なので builder 側に直書きでも可。差分は最小に。)
Task B: JoinIR(nested minimal)を生成する
参考: src/mir/join_ir/lowering/simple_while_minimal.rs
最小形の JoinIR 関数構成(推奨):
main(i0, sum0):Call(loop_step, [i0, sum0])Ret 0(statement-position)
loop_step(i, sum)(outer):exit_cond = !(i < N_outer)Jump(k_exit, [sum], cond=exit_cond)Call(inner_step, [j0, i, sum])
inner_step(j, i_outer, sum):exit_cond = !(j < N_inner)Jump(k_inner_exit, [i_outer, sum], cond=exit_cond)sum_next = sum + 1j_next = j + 1Call(inner_step, [j_next, i_outer, sum_next])
k_inner_exit(i, sum):i_next = i + 1Call(loop_step, [i_next, sum])
k_exit(sum):Ret sum
命名:
k_exitは canonical name:src/mir/join_ir/lowering/canonical_names.rs::K_EXITloop_stepは canonical name:...::LOOP_STEPinner_step/k_inner_exitは Phase 188.3 で追加(文字列でよいが、可能なら canonical_names に追加)
Task C: boundary(exit binding + continuation funcs)を正しく構築する
参考: src/mir/builder/control_flow/joinir/patterns/pattern1_minimal.rs
必須:
- join_inputs/host_inputs は JoinModule.entry.params と同順・同数
exit_bindingsはsumを host slot に reconnect- join_exit_value は
k_exitの param(Pattern1 と同じくjoin_module.require_function("k_exit").params[0]から取得)
- join_exit_value は
continuation_func_idsに以下を含める:k_exitk_inner_exitinner_step
Task D: integration smoke を追加する(exit code SSOT)
新規:
tools/smokes/v2/profiles/integration/joinir/phase1883_nested_minimal_vm.shapps/tests/phase1883_nested_minimal.hakoを実行し、exit code == 9 で PASS- stdout 比較はしない
注意:
tools/smokes/v2はmanifest.txt方式ではない(findベース)- 既存の helper を使う:
source tools/smokes/v2/lib/test_runner.sh
検証手順
cargo build --release./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako(exit=9)./tools/smokes/v2/run.sh --profile integration --filter "phase1883_nested_minimal"./tools/smokes/v2/run.sh --profile quick(154/154 PASS)./tools/smokes/v2/run.sh --profile integration --filter "selfhost_"(FAIL=0)
Troubleshooting: use of undefined value ValueId(...)(Pattern6)
典型ログ:
[cf_loop/joinir] Function 'inner_step' params: [ValueId(104), ...]use of undefined value ValueId(104)
意味:
- JoinIR の “param ValueId” は SSA 命令で定義されないため、
Copy(dst=param, src=arg)が入らないと undefined になる。
優先して疑う場所(責務の順):
src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rsplan_rewrites()の “tail-call param binding” の skip 条件- SSOT: skip は “target が loop header” のときだけ(header PHI dst を上書きしないため)
loop_step(entry func)→ inner_stepまで “entry func だから” という理由で skip すると、inner_step param が未定義になる
JoinInlineBoundary.continuation_func_idsinner_step/k_inner_exitが continuation 扱いになっていないと、merge 側の entry func 選定がズレて skip 判定も破綻しやすい
注意(避けたい対処):
merge/mod.rsで “param ValueId を index で PHI dst にリマップする” は危険- Pattern6 は
inner_step(j, i, sum)のように先頭に loop-local があり、carrier index と一致しない - index remap は
jをiの PHI dst に誤接続しやすい
- Pattern6 は
追加の注意(Fail-Fast)
- Pattern6 が選ばれたあとに
Ok(None)で他パターンに流すのは禁止(silent fallback) - “選ぶ前に落とす” が最も安全:
is_pattern6_lowerable()を「lowering が確実に通る形だけ true」に強化する