Files
hakorune/docs/development/current/main/phases/phase-188.3/README.md
tomoaki a04b48416e fix(joinir): Phase 287 P2 - Pattern6 nested loop latch overwrite fix
Fix infinite loop in Pattern6 (nested loop minimal) caused by main→loop_step
overwriting k_inner_exit→loop_step latch values.

Root cause: JoinIR main entry block was incorrectly treated as BackEdge,
causing it to overwrite the correct latch incoming values set by the true
back edge (k_inner_exit → loop_step).

Solution:
- Restrict latch recording to TailCallKind::BackEdge only
- Treat only MAIN's entry block as entry-like (not loop_step's entry block)
- Add debug_assert! to detect double latch set in future

Refactoring:
- Extract latch recording to latch_incoming_recorder module (SSOT)
- Add boundary.loop_header_func_name for explicit header identification
- Strengthen tail_call_classifier with is_source_entry_like parameter

Tests: apps/tests/phase1883_nested_minimal.hako → RC:9 (was infinite loop)
Smoke: 154/154 PASS, no regressions

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 09:39:29 +09:00

9.2 KiB
Raw Blame History

Phase 188.3: Nested loop lowering (1-level) — make Pattern 6 real

Date: 2025-12-27 Status: Complete (Pattern6 lowering + merge/latch fix) Prereq: Phase 188.2 Option A is complete (StepTree depth SSOT + strict Fail-Fast)


Goal

max_loop_depth == 21-level nested loopJoinIR lowering で実際に通す

  • 既知の事実: LoopForm (=LoopShape) にはネスト情報が無いので、LoopFormベースの Pattern6 検出/ルーティングでは実装できない
  • 実装は **StepTreeAST側**を SSOT として扱うPhase 188.2 Option A を継続)

Definition: max_loop_depth (SSOT)

StepTreeFeatures.max_loop_depth は「その関数or 評価対象ブロック)の中で、最も深い loop{} ネストの深さ」を表す。

  • loop が無い: 0※StepTree実装に依存。現状の運用では “loopがある前提”で扱う
  • loop が 1 段: 1
  • loop の中に loop が 1 つ: 2
  • loop{ loop{ loop{ ... } } }: 3

Phase 188.2 の strict ガードは「depth > 2 を明示エラー」にしている。 これは言語仕様の制限ではなく、「JoinIR lowering の実装範囲compiler capability」の制限。


Scope (minimal)

対応するのは “NestedLoop Minimal” の 1形だけに限定する。

  • depth: max_loop_depth == 2 のみ
  • inner loop: Pattern1相当break/continue 無し)
  • outer loop: Pattern1相当break/continue 無し)を優先
  • それ以外:
    • strict mode: 明示エラーPhase 188.2 の depth check とは別タグで良い)
    • non-strict mode: 既存の fallback 経路に任せる(ただし silent fallback を増やさない)

Current Code Status (reality)

Phase 188.3 は “選択ロジックPattern6選定→ lowering → merge/rewriter 安定化” まで完了している。

  • Selection SSOT: src/mir/builder/control_flow/joinir/routing.rschoose_pattern_kind()
    • cheap check → StepTree → AST validation
    • max_loop_depth == 2 かつ “Pattern6 lowerable” のときだけ Pattern6NestedLoopMinimal を返す
  • Lowering: src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs
    • JoinIR pipeline で inner_step/k_inner_exit/k_exit を含む関数群を生成して merge する
    • Fixture: apps/tests/phase1883_nested_minimal.hakoRC=9
  • Merge/Rewrite contractSSOT:
    • latch_incoming を記録してよいのは TailCallKind::BackEdge のみLoopEntry は上書き禁止)
    • entry-like は “JoinIR main の entry block のみ”
    • 二重 latch は debug_assert! で fail-fast
    • 実装: src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs, src/mir/builder/control_flow/joinir/merge/loop_header_phi_info.rs

SSOT (what to rely on)

  • nesting depth SSOT: StepTreeFeatures.max_loop_depth
  • depth > 2: strict mode で control_tree/nested_loop/depth_exceededPhase 188.2

Scope & Variable Model (SSOT)

Nyash の変数スコープ方針に合わせて、nested loop lowering でも以下を SSOT として固定する。

Visibility (read)

  • inner loop から outer の binding は参照できるlexical scope: “1つ上は見える”
  • ただし JoinIR lowering では「見える」を 明示的な引数/継続で表現する(暗黙キャプチャを増やさない)

Mutation (write-back)

Phase 188.3 では段階的に進める:

  • P188.3 (minimal): “最小の write-back” を 明示的な carrier として許す
    • inner loop が outer 変数を更新する場合、その変数は carrier として引数で受けて、k_inner_exit(...) で返す
    • 対応範囲は最小fixtureが要求する 1 carrier 程度)に限定する
  • P188.4+ (generalize): write-back を一般化(複数 carrier / 代入形 / break/continue を含む形)

この分離により、write-back の複雑さ(複数 carrier / break / continue / PHI の再接続)を Phase 188.3 に持ち込まずに済む。


Lowering sketch (how it should look)

Nested loop は JoinIR の「tail recursion + continuation」を再帰的に合成して表現する。

  • outer: outer_step(state..., k_outer_exit)
  • inner: inner_step(state..., k_inner_exit)
  • inner が終わったら k_inner_exit(...) で outer の“残り”へ戻る

この k_inner_exit がスコープ境界として働くので、将来の write-back もここに集約できる。

Phase 188.3 の最小形は「outer の loop_step の中で inner の loop_step を呼び、inner が終わったら k_inner_exit で outer に戻る」。

推奨の JoinIR 関数群(概念):

  • main(i0, sum0):
    • Call(loop_step, [i0, sum0])
    • Ret 0statement-position loop の場合)
  • loop_step(i, sum)outer の step。canonical name は loop_step に寄せる):
    • 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 + 1fixture の最小形)
    • j_next = j + 1
    • Call(inner_step, [j_next, i_outer, sum_next])
  • k_inner_exit(i, sum):
    • i_next = i + 1
    • Call(loop_step, [i_next, sum])
  • k_exit(sum)canonical name k_exit:
    • Ret sum

重要:

  • carrier はグローバル扱いしないsum は引数で運んで戻す。
  • merge 側の “loop_step 関数の選定” を壊さないため、inner_stepk_inner_exit は boundary の continuation_func_ids に含めて除外する(詳細は指示書)。

Merge/Rewrite contract (SSOT) — “undef ValueId” を防ぐ

JoinIR merge は「JoinIR の param ValueId は SSA 命令で定義されない」前提なので、適切な Copyparam binding が入らないと即 use of undefined value ValueId(...) になる。

特に Pattern6 では loop_stepouterから inner_step へ tail-call するため、以下を SSOT として固定する:

  • Skip するのは “target が loop header” のときだけ
    • header では PHI dst が carrier の SSOT なので、param Copy を入れて上書きしてはいけない
    • 一方で loop_step → inner_step は “target が header ではない” ため、inner_step params を定義する Copy が必要
  • param の index ベース remap は危険
    • inner_step(j, i, sum) のように、先頭に loop-localj)が混ざると carrier の index と一致しない
    • index remap は ji の PHI dst に誤接続しやすい

この契約に反する場合の典型症状:

  • Function 'inner_step' params: [ValueId(104), ...] の直後に use of undefined value ValueId(104)

修正の第一候補は merge/instruction_rewriter.rstail-call param binding の skip 条件であり、merge/mod.rs の “パラメータ再マップを拡張する” で逃げない(責務を混ぜない)。


Deliverables

  1. Fixture + integration smokeexit code SSOT

    • 1-level nested loop を最小で再現する .hako を追加
    • integration で実行し、exit code で判定stdout比較はしない
  2. StepTree-based lowering implementation

    • StepTree を辿って、inner loop を outer loop の中で正しく lowering できるようにする
    • 入口は “StepTree→JoinIR” のどこかLoopFormベースの router は使わない)
  3. Docs update

    • Phase 188.1 の “Pattern6 specification” が design であることは維持
    • Phase 188.3 で “実装済み/未実装の境界” を明確に書く

Acceptance Criteria

  • ./tools/smokes/v2/run.sh --profile quick が常にグリーン維持
  • integration selfhost が FAIL=0 を維持
  • 追加した nested loop fixture が PASSJoinIR lowering が使われたことをログ/タグで確認可能)
    • 実測: ./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako → RC=9

Instructions (handoff)

実装者Claude Code向けの手順書は docs/development/current/main/phases/phase-188.3/P1-INSTRUCTIONS.md を参照。


Next (schedule)

  • Phase 188.3: depth=2 の最小形を “確実に通す” + PoC fixture を smoke 固定
  • Phase 188.4+: write-backouter carrier reconnectionと “再帰 lowering の一般化depthを増やしても壊れない” を docs-first で設計してから実装

Post-completion (refactoring window)

実装完了後のリファクタ(意味論不変)を挟む場合は、P2-REFACTORING-INSTRUCTIONS.md を入口にする。

Planned cleanup (after Phase 188.3)

Pattern6 を通す過程で露出しやすい “暗黙ルール” を SSOT 化して、今後の nested/generalization を楽にする:

  • JoinInlineBoundaryloop header func name を明示する SSOTmerge の “entry func 推定” を段階的に減らす)
  • ParamBinding の規則を “source-based ではなく target-based” に統一header だけ特別扱い)
  • 将来param rolecarrier vs localを明文化し、index remap の誘惑を消す

Out of Scope

  • nested loop + break/continue の一般対応
  • LoopRegion を使った MIR-level nesting SSOTOption B