Apply Phase 256.8 SSOT fix to Pattern4/6/7: - Use join_module.entry.params.clone() instead of hardcoded ValueIds - Add fail-fast validation for params count mismatch - Remove ValueId(0), ValueId(PARAM_MIN + k) patterns - Clean up unused PARAM_MIN imports This prevents entry_param_mismatch errors structurally and maintains consistency with Pattern2/3. Changes: - pattern4_with_continue.rs: Lines 442-476 (SSOT extraction + validation) - pattern6_scan_with_init.rs: Lines 447-471 (SSOT extraction + validation) - pattern7_split_scan.rs: Lines 495-526 (SSOT extraction + validation) All patterns now use the same SSOT principle: 1. Extract entry function (priority: join_module.entry → fallback "main") 2. Use params as SSOT: join_inputs = entry_func.params.clone() 3. Build host_inputs in expected order (pattern-specific) 4. Fail-fast validation: join_inputs.len() == host_inputs.len() Verification: - cargo build --release: ✅ PASS (no PARAM_MIN warnings) - Quick profile: ✅ First FAIL still json_lint_vm (baseline maintained) - Pattern6 smoke: ✅ PASS (index_of test) - Pattern7 smoke: Pre-existing phi pred mismatch (not introduced by SSOT) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Phase 256: StringUtils.split/2 Pattern Support
Status: Completed Scope: Loop pattern recognition for split/tokenization operations Related:
- Phase 255 完了(loop_invariants 導入、Pattern 6 完成)
- Phase 254 完了(Pattern 6 index_of 実装)
- North star(設計目標):
docs/development/current/main/design/join-explicit-cfg-construction.md
Current Status (SSOT)
- Current first FAIL:
json_lint_vm / StringUtils.last_index_of/2(Loop with early return pattern - unsupported) StringUtils.split/2は VM--verify/ smoke まで PASS- Pattern6(index_of)は PASS 維持
- 次フェーズ: Phase 257(
last_index_of/2の reverse scan + early return loop) - 直近の完了:
- P1.13: Pattern2 boundary entry_param_mismatch 根治(
join_module.entry.paramsSSOT 化) - P1.10: DCE が
jump_args参照を保持し、instruction_spansと同期するよう修正(回帰テスト追加) - P1.7: SSA undef(
%49/%67)根治(continuation 関数名の SSOT 不一致) - P1.6: pipeline contract checks を
run_all_pipeline_checks()に集約
- P1.13: Pattern2 boundary entry_param_mismatch 根治(
- 次の作業: Phase 257(last_index_of pattern - loop with return support)
- 設計メモ(ChatGPT Pro 相談まとめ):
docs/development/current/main/investigations/phase-256-joinir-contract-questions.md
Background (P0 Archive)
このセクションは初期の失敗詳細とP0設計の記録。現状の作業は上記の Current Status を参照。
失敗詳細
テスト: json_lint_vm (quick profile)
エラー: [joinir/freeze] Loop lowering failed: JoinIR does not support this pattern
関数: StringUtils.split/2
エラーメッセージ全体
[trace:dev] loop_canonicalizer: Decision: FAIL_FAST
[trace:dev] loop_canonicalizer: Missing caps: [ConstStep]
[trace:dev] loop_canonicalizer: Reason: Phase 143-P2: Loop does not match read_digits(loop(true)),
skip_whitespace, parse_number, continue, parse_string, or parse_array pattern
[ERROR] ❌ MIR compilation error: [joinir/freeze] Loop lowering failed: JoinIR does not support this pattern,
and LoopBuilder has been removed.
Function: StringUtils.split/2
Hint: This loop pattern is not supported. All loops must use JoinIR lowering.
期待される動作
StringUtils.split(s, separator) が正常にコンパイルされ、文字列分割が動作すること。
実際の動作
Loop canonicalizer が ConstStep を要求しているが、このループはステップが複雑で定数ではない。
最小再現コード
split(s, separator) {
local result = new ArrayBox()
// Early return for empty separator
if separator.length() == 0 {
result.push(s)
return result
}
local start = 0
local i = 0
// Main scan loop
loop(i <= s.length() - separator.length()) {
if s.substring(i, i + separator.length()) == separator {
result.push(s.substring(start, i))
start = i + separator.length()
i = start // Variable step - moves by separator.length()
} else {
i = i + 1 // Constant step - moves by 1
}
}
// Push remaining segment
if start <= s.length() {
result.push(s.substring(start, s.length()))
}
return result
}
分析
ループ構造
- 条件:
i <= s.length() - separator.length() - ボディ:
- If branch: マッチング検出時
result.push()でセグメント追加start = i + separator.length()で次の開始位置更新i = startで大きくジャンプ(可変ステップ)
- Else branch: マッチなし
i = i + 1で 1 進む(定数ステップ)
- If branch: マッチング検出時
- 特徴:
- 可変ステップ: マッチング時は
separator.length()分ジャンプ - 複数キャリア:
i,start,resultを更新 - MethodCall:
substring(),push(),length()を使用
- 可変ステップ: マッチング時は
Canonicalizer の問題
Missing caps: [ConstStep]
- 既存の Pattern 1-6 は定数ステップを想定
- このループは条件分岐で異なるステップ幅を使う
- Pattern 2 (balanced_depth_scan) に近いが、可変ステップがネック
実装計画
Option A: Pattern 7 - Split/Tokenization Pattern
新しいパターン追加:
- 可変ステップサポート
- 複数キャリア(i, start, accumulator)
- If-else での異なるステップ幅処理
検出条件:
- Loop condition:
i <= expr - len - Body has if statement:
- Then:
i = something_big(可変ジャンプ) - Else:
i = i + 1(定数ステップ)
- Then:
- Accumulator への追加操作 (
pushなど)
Option B: Pattern 2 拡張
既存 Pattern 2 を拡張:
- ConstStep 要件を緩和
- If-else で異なるステップ幅を許可
- balanced_depth_scan_policy を拡張
Option C: Normalization 経路
ループ正規化で対応:
- 可変ステップを定数ステップに変換
- Carrier 追加で状態管理
次のステップ(P0時点の初期計画)
- StepTree 詳細解析: split ループの完全な AST 構造確認
- 類似パターン調査: 他の可変ステップループ(indexOf, contains など)
- Option 選択: Pattern 7 新設 vs Pattern 2 拡張 vs Normalization
- 実装戦略策定: 選択した Option の詳細設計
Phase 256 指示書(P0 / 完了済み)
目標
StringUtils.split/2の loop を JoinIR で受理し、json_lint_vmを PASS に戻す。- by-name 分岐禁止(
StringUtils.split/2だけを特別扱いしない)。 - workaround 禁止(fallback は作らない)。
推奨方針(P0)
Option A(Pattern 7 新設)を推奨。
理由:
- 可変 step(then:
i = start/ else:i = i + 1)は既存の ConstStep 前提と相性が悪い。 - Pattern 2 を膨らませず、tokenization 系の “専用パターン” として箱化した方が責務が綺麗。
P0 タスク
- 最小 fixture + v2 smoke(integration)
apps/tests/phase256_p0_split_min.hakotools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.shtools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.sh
- DetectorBox(構造のみ)
- ループ条件が
i <= s.length() - sep.length()形 - body に
if substring(i, i + sep.length()) == sep { ... i = start } else { i = i + 1 } result.push(...)を含む(ArrayBox accumulator)- ループ後に “残り push” がある(任意だがあると精度が上がる)
- 抽出箱(Parts)
i/start/result/s/separatorを抽出- then/else の更新式(可変 step と const step)を抽出
- JoinIR lowerer(専用)
- loop_state:
i,start - invariants:
s,separator,result(result は更新されるので carrier 扱いが必要なら role を明確に) - then/else で異なる
i_nextをSelectもしくは branch で表現(設計 SSOT は JoinIR 側で決める)
- 検証
- integration smokes 2本が PASS
./tools/smokes/v2/run.sh --profile quickの最初の FAIL が次へ進む
注意(P0ではやらない)
- 既存 Pattern の大改造(Pattern 2 の全面拡張)は避ける
- 正規化(Normalization 経路)は P1 以降の検討に回す
備考
- Phase 255 で loop_invariants が導入されたが、このケースは invariants 以前の問題(可変ステップ)
- Phase 254-256 の流れで Pattern 6 → Pattern 7 の自然な進化が期待される
- split/tokenization は一般的なパターンなので、汎用的な解決策が望ましい
進捗(P0/P1)
P0: Pattern 7 基本実装(完了)
- Fixture & smokes(integration):
apps/tests/phase256_p0_split_min.hakotools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.shtools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.sh
- Pattern 7:
- Detector / Extractor / JoinIR lowerer / MirBuilder 統合まで実装
- JoinIR lowerer は可変 step を
JoinInst::Selectで表現
P1: Carrier/param 配線の整流(完了)
- “Carriers-first” の順序を SSOT として固定し、latch incoming 不足を解消
- exit bindings を明示的に構築(
i,start)
P1.5: JoinIR→MIR 変換の根治(進行中)
背景:
- Pattern 7 は
JoinInst::Selectを使用するが、JoinIR→MIR 変換経路で Select が未対応だったため、 “dst が定義されない ValueId” が発生し得る。
対応(完了):
- boundary の伝播を bridge 経路全体へ追加
MirInstruction::Selectの追加、および JoinIR→MIR 変換 / remap / value collection を実装
現状(ブロッカー):
- integration VM が still FAIL:
use of undefined value ValueId(57)(StringUtils.split/2)
追加の事実(2025-12-20)
- Pattern 6(index_of)回帰: PASS(Select 導入後もインフラは健全)
ValueId(57)の正体:- JoinIR
ValueId(1004)の remap 先(JoinIR→Host の remap は成功している) sep_len = sep.length()のローカル値(loop_step 内でBoxCall("length")で定義される想定)
- JoinIR
- つまり「remap の欠落」ではなく、「定義命令(dst を持つ BoxCall)が MIR に落ちていない/順序が壊れている」問題が濃厚
次(P1.5 Task 3): sep_len 定義トレース(SSOT)
目的: sep_len の「定義(dst)」が MIR に存在し、かつ use より前に配置されることを確認し、欠落しているなら根治する。
-
MIR ダンプで「%57 の定義があるか」を確認
./target/release/hakorune --backend vm --dump-mir --mir-verbose apps/tests/phase256_p0_split_min.hako > /tmp/split.mirrg -n \"\\bValueId\\(57\\)\\b|%57\" /tmp/split.mir- 期待:
BoxCallかそれに相当する命令でdst=%57が use より前に出る
-
定義が無い場合: JoinIR→MIR converter を疑う
JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(sep_len), method: \"length\", .. })がMirInstruction::BoxCall { dst: Some(remapped), .. }として出力されているかを点検する- もし
dst: Noneになっている/命令自体が落ちているなら、converter か value-collector の取りこぼしを修正する
-
定義があるが use より後の場合: ブロック内の命令順の生成/マージ規約を疑う
- joinir→mir の「生成順」か merge の「挿入位置」がおかしい
- 期待: “def-before-use” が各 BasicBlock 内で成立する
-
受け入れ基準(P1.5)
tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.shが PASStools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.shが PASS
- 既存:
tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.shが PASS 維持
診断アップデート(2025-12-20)
ValueId(57)(= sep_len)について、Step 2 の結果で「Case A」が確定した:
- JoinIR には定義が存在する:
JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(sep_len), method: "length", args: [sep] })
- しかし最終 MIR(
--dump-mir)には%57 = ...相当の def が存在しない(use のみ) - remap 自体は成功している:
- JoinIR
ValueId(1004)→ HostValueId(57)
- JoinIR
結論:
- 「remap が壊れている」ではなく、
JoinInst::Compute(MirLikeInst::BoxCall)が JoinIR→MIR 変換/マージ経路のどこかで落ちている。
次の実装タスク(P1.5 Task 3.1):
- JoinIR→MIR の中間生成物(bridge 側の MirModule)をダンプして、
dst: Some(1004)のMirInstruction::BoxCallが生成されているか(生成されていないなら converter バグ)- 生成されているのに最終 MIR から消えるなら merge/DCE バグ を二分探索で確定する。
Task 3.1 結果(2025-12-20)
ValueId(57) undefined は根治できた(def-before-use 不変条件が回復)。
- 最終原因:
split_scan_minimal.rs内でconst_1(JoinValueSpaceのローカル ValueId)が 初期化されないままi + const_1に使用されていた。- これが remap 後に
ValueId(57)となり、最終 MIR で「use のみ / def が無い」になっていた。
- 修正:
const_1 = 1をJoinInst::Compute(MirLikeInst::Const { .. })で use より前に挿入。
- 付随:
- bridge 生成物 MirModule を
/tmp/joinir_bridge_split.mirへダンプできるようにした(HAKO_JOINIR_DEBUG=1ガード)。
- bridge 生成物 MirModule を
⚠️ 注記:
- これにより「ValueId(57) = sep_len」仮説は撤回する。
- 以降は、各ローカル ValueId の意味は bridge dump / join module dump を SSOT として確定すること。
次のブロッカー(P1.5 Task 3.2)
ValueId(57) は直ったが、Pattern7(split) はまだ PASS していない。新たに以下が露出:
- SSA:
Undefined value %49 used in block bb10Undefined value %67 used in block bb10
- 型:
unsupported compare Le on BoxRef(ArrayBox) and Integer(...)
次のタスク(P1.5 Task 3.2):
- まず
--verifyと--dump-mirで %49 / %67 が何の値で、どの命令で定義されるべきかを確定し、- (A) joinir lowerer が def を吐いていないのか
- (B) bridge が落としているのか
- (C) merge/optimizer が落としているのか を二分探索する。
追記(P1.7完了後)
- SSA undef(
%49/%67)は P1.7 で根治済み - 現在の first FAIL は carrier PHI 配線へ移動:
[joinir/exit-line] jump_args length mismatch: expected 3 or 4 but got 5Phase 33-16: Carrier 'i' has no latch incoming set
進捗(P1.5-DBG)
P1.5-DBG: Boundary Entry Parameter Contract Check(完了)
目的:
boundary.join_inputsと JoinIR entry function のparamsの対応(個数/順序/ValueId)を VM 実行前に fail-fast で検出する。- これにより、Pattern6 で起きた
loop_invariants順序バグ(例:[s, ch]↔[ch, s])のような問題を「実行時の undef」ではなく「構造エラー」として落とせる。
実装(SSOT):
src/mir/builder/control_flow/joinir/merge/contract_checks.rsverify_boundary_entry_params()(個数/順序/ValueId の検証)get_entry_function()(join_module.entry→"main"へのフォールバック)- unit tests を追加(正常/順序ミスマッチ/個数ミスマッチ)
src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs- JoinIR→MIR 変換直前に検証を追加
is_joinir_debug()時にログ出力
運用:
HAKO_JOINIR_DEBUG=1で[joinir/boundary-contract] ...を出す(既存トグルのみ、env 追加なし)。
P1.6: Pipeline Contract Checks の薄い集約(完了)
目的:
conversion_pipeline.rsから契約チェック呼び出しと debug ログの詳細を排除し、 契約チェックの SSOT をcontract_checks.rsに集約する(dyn trait など過剰な箱化はしない)。
実装(SSOT):
src/mir/builder/control_flow/joinir/merge/contract_checks.rsrun_all_pipeline_checks()(薄い集約エントリ)debug_log_boundary_contract()を同ファイルへ移設(is_joinir_debug()ガード)
src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rsrun_all_pipeline_checks()の 1 呼び出しに縮退
効果:
- pipeline 側の責務を「パイプラインの制御」に戻し、契約チェックは
contract_checks.rsに閉じ込めた。 今後チェック項目を増やす場合もrun_all_pipeline_checks()に追記するだけで済む。
進捗(P1.7)
P1.7: SSA undef(%49/%67)根治(完了)
症状:
Undefined value %49 used in block bb10Undefined value %67 used in block bb10
根本原因:
- JoinIR→MIR bridge が
JoinFunction.nameをそのまま MirModule の関数名にしていた(例:"k_exit","loop_step") - merge 側が
join_func_name(id)(例:"join_func_2")で関数を探索していた - その結果、continuation 関数が見つからず inline/merge がスキップされ、SSA undef が露出した
修正方針:
- continuation 関数の識別を「関数ID→暗黙変換」に依存させず、MirModule 上の関数名(String)で SSOT 化する
結果:
./target/release/hakorune --backend vm --verify apps/tests/phase256_p0_split_min.hakoで SSA undef は消滅
進捗(P1.8)
P1.8: ExitLine/jump_args と関数名マッピング整流(完了)
変更(要旨):
- ExitArgsCollector 側で「余剰 jump_args(invariants)」を許容し、
expected 3 or 4 but got 5を解消 - JoinIR→MIR bridge 側で “join_func_N” 由来の名前と “JoinFunction.name” の不一致を解消するため、関数名マッピングを導入/伝播
結果:
- 旧 first FAIL(jump_args length mismatch)は解消
P1.9: Jump を tail call として表現(完了)
変更(要旨):
- JoinIR→MIR bridge で
JoinInst::Jumpを “continuation への tail call” として落とす BasicBlock.jump_argsを tail call と同様に SSOT として保持(ExitLine/collector の復元入力)
結果:
JoinInst::Jumpが “ret args[0]” 相当になり continuation が失われる問題は解消
P1.10: DCE の jump_args + spans 同期(完了)
変更(要旨):
- DCE が
jump_argsで使われる値を used として扱い、純命令の除去で Copy が消えないようにする instruction_spansとinstructionsの同期不変条件を維持(SPAN MISMATCH 根治)- 回帰テストを追加(
test_dce_keeps_jump_args_values,test_dce_syncs_instruction_spans)
進捗(P1.11)
P1.11: ExitArgsCollector の expr_result slot 判定を明確化(完了)
症状(split の誤動作):
- runtime 側で
startとresultが入れ替わり、ExitLine の配線が崩れる - 結果として VM 実行で型エラー(例:
ArrayBox <= 5)に到達
根本原因(SSOT):
- ExitArgsCollector が
jump_args[0]を “expr_result slot” とみなす条件が粗く、expr_resultが exit_bindings の LoopState carrier と同一 ValueId(例:result)のケースでも offset=1 側に寄っていた
修正(方針):
expect_expr_result_slotを明示的に渡し、かつexpr_resultが exit_bindings の LoopState carriers に含まれる場合はexpect_expr_result_slot=falseとするexit_args_collector.rs側はexpect_expr_result_slotの意味に合わせて整理し、 「末尾の invariants は無視」の仕様は維持する
受け入れ(確認):
./target/release/hakorune --backend vm --verify apps/tests/phase256_p0_split_min.hakoPASS./target/release/hakorune --backend vm apps/tests/phase256_p0_split_min.hakoが期待 RC(3)で終了
進捗(P1.12)
P1.12: integration smoke scripts の終了コード取得を安定化(完了)
変更(要旨):
set -eのまま “対象コマンドだけ” をset +eでラップし、非0終了でも exit code を取得できるようにするHAKORUNE_BINの既定値を追加し、手動実行の再現性を上げる
受け入れ(確認):
HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.shPASS(exit=3)HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.shPASS(exit=1)
リファクタリング方針(P1.6候補 / 先送り推奨)
現時点(split がまだ FAIL)では、箱化のための箱化で複雑さが増えやすいので、以下を推奨する:
-
✅ 先にやってよい(低リスク / 価値が高い)
contract_checks.rs内で「チェックの実行順」をrun_all_*()のような薄い関数にまとめる(dyn trait は不要)- debug ログは既存の仕組み(
is_joinir_debug()/DebugOutputBox)に寄せる(新 logger box を増やさない) - テストの JoinModule 構築は、重複が 3 箇所以上になった時点で共通化
-
⛔ 先送り(split を PASS してから)
ContractCheckerBox(trait object)化:柔軟だが、ここでは過剰になりやすいJoinIRDebugLoggerBox新設:既存の DebugOutputBox と二重化しやすい- MIR 命令 dst 抽出の広域統一:既存の
MirInstructionhelper との重複が出やすいので要調査の上で
小さな整理(今後の予定 / P1.8以降)
- JoinIR の関数名は
src/mir/join_ir/lowering/canonical_names.rsを SSOT とする"k_exit"/"loop_step"/"main"の直書きは段階的にcanonical_names::*へ置換- 正規化 shadow の
"join_func_2"はcanonical_names::K_EXIT_LEGACYとして隔離し、統一は Phase 256 完了後に検討
- legacy 掃除候補:
join_func_name(id)の利用箇所を棚卸しし、「structured JoinIR では使用禁止 / normalized shadow だけで使用」など境界を明文化
進捗(P1.13)
P1.13: Pattern2 boundary entry_param_mismatch 根治(完了)
症状(json_lint_vm / StringUtils.trim_end/1):
[ERROR] ❌ MIR compilation error: [joinir/phase1.5/boundary/entry_param_mismatch]
Entry param[0] in 'main': expected ValueId(1000), but boundary.join_inputs[0] = ValueId(0)
Hint: parameter ValueId mismatch indicates boundary.join_inputs constructed in wrong order
根本原因(SSOT):
emit_joinir_step_box.rsがboundary.join_inputsを hardcoded ValueId(0), ValueId(1)... で構築していた- JoinIR lowerer は
alloc_param()/alloc_local()で実際のパラメータ ValueId を割り当てている - 両者が一致しないため、boundary contract check で fail-fast
修正方針(SSOT原則):
- SSOT:
join_module.entry.paramsがboundary.join_inputsの唯一の真実 - 禁止: ValueId(0..N) の推測生成、Param/Local 領域の決めつけ、JoinModule とは独立に ValueId を作ること
- 実装:
emit_joinir_step_box.rsでjoin_input_slots = entry_func.params.clone()に置き換え
実装(SSOT):
src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs(lines 71-96)- Entry function extraction (priority:
join_module.entry→ fallback to "main") join_input_slots = main_func.params.clone()(SSOT from JoinModule)host_input_valuesを同じ順序で構築(loop_var + carriers)- Fail-fast validation for params count mismatch
- Entry function extraction (priority:
結果:
./tools/smokes/v2/run.sh --profile quickの first FAIL がStringUtils.trim_end/1からStringUtils.last_index_of/2へ移動- Pattern2 の boundary contract は安定化
次のブロッカー(Phase 257):
StringUtils.last_index_of/2- Loop with early return pattern (unsupported)- Structure:
loop(i >= 0) { if (cond) { return value } i = i - 1 } return default - Capabilities:
caps=If,Loop,Return(no Break) - Missing: ConstStep capability
- Approach: Extend Pattern2 to handle return (similar to break) or create Pattern2Return variant
- Fixture:
apps/tests/phase257_p0_last_index_of_min.hako - Integration smokes:
phase257_p0_last_index_of_vm.sh,phase257_p0_last_index_of_llvm_exe.sh
- Structure:
次(P1.5 Task 3):
ValueId(57)が「何の JoinIR 値の remap 結果か」を確定し、定義側(dst)が MIR に落ちているかを追う- 例:
sep_len = sep.length()の BoxCall dst が収集/変換/順序のどこかで欠けていないか
- 例: