# 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.params` SSOT 化) - P1.13.5(= Phase 256.8.5): Pattern4/6/7 でも `boundary.join_inputs` を `join_module.entry.params` SSOT に統一(hardcoded ValueId/PARAM_MIN を撤去) - P1.10: DCE が `jump_args` 参照を保持し、`instruction_spans` と同期するよう修正(回帰テスト追加) - P1.7: SSA undef(`%49/%67`)根治(continuation 関数名の SSOT 不一致) - P1.6: pipeline contract checks を `run_all_pipeline_checks()` に集約 - 次の作業: Phase 257(last_index_of pattern - loop with return support) - 設計メモ(ChatGPT Pro 相談まとめ): `docs/development/current/main/investigations/phase-256-joinir-contract-questions.md` - Known issue(非ブロッカー): - Pattern7 integration smoke で `phi predecessor mismatch` が残っている(今回の boundary SSOT 統一とは独立) --- ## 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` を要求しているが、このループはステップが複雑で定数ではない。 ### 最小再現コード ```nyash 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 } ``` ### 分析 #### ループ構造 1. **条件**: `i <= s.length() - separator.length()` 2. **ボディ**: - If branch: マッチング検出時 - `result.push()` でセグメント追加 - `start = i + separator.length()` で次の開始位置更新 - `i = start` で大きくジャンプ(可変ステップ) - Else branch: マッチなし - `i = i + 1` で 1 進む(定数ステップ) 3. **特徴**: - **可変ステップ**: マッチング時は `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 での異なるステップ幅処理 **検出条件**: 1. Loop condition: `i <= expr - len` 2. Body has if statement: - Then: `i = something_big` (可変ジャンプ) - Else: `i = i + 1` (定数ステップ) 3. Accumulator への追加操作 (`push` など) #### Option B: Pattern 2 拡張 **既存 Pattern 2 を拡張**: - ConstStep 要件を緩和 - If-else で異なるステップ幅を許可 - balanced_depth_scan_policy を拡張 #### Option C: Normalization 経路 **ループ正規化で対応**: - 可変ステップを定数ステップに変換 - Carrier 追加で状態管理 ### 次のステップ(P0時点の初期計画) 1. **StepTree 詳細解析**: split ループの完全な AST 構造確認 2. **類似パターン調査**: 他の可変ステップループ(indexOf, contains など) 3. **Option 選択**: Pattern 7 新設 vs Pattern 2 拡張 vs Normalization 4. **実装戦略策定**: 選択した 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 タスク 1) 最小 fixture + v2 smoke(integration) - `apps/tests/phase256_p0_split_min.hako` - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh` - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.sh` 2) 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” がある(任意だがあると精度が上がる) 3) 抽出箱(Parts) - `i` / `start` / `result` / `s` / `separator` を抽出 - then/else の更新式(可変 step と const step)を抽出 4) JoinIR lowerer(専用) - loop_state: `i`, `start` - invariants: `s`, `separator`, `result`(result は更新されるので carrier 扱いが必要なら role を明確に) - then/else で異なる `i_next` を `Select` もしくは branch で表現(設計 SSOT は JoinIR 側で決める) 5) 検証 - 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.hako` - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh` - `tools/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")` で定義される想定) - つまり「remap の欠落」ではなく、「定義命令(dst を持つ BoxCall)が MIR に落ちていない/順序が壊れている」問題が濃厚 #### 次(P1.5 Task 3): sep_len 定義トレース(SSOT) 目的: `sep_len` の「定義(dst)」が MIR に存在し、かつ use より前に配置されることを確認し、欠落しているなら根治する。 1) MIR ダンプで「%57 の定義があるか」を確認 - `./target/release/hakorune --backend vm --dump-mir --mir-verbose apps/tests/phase256_p0_split_min.hako > /tmp/split.mir` - `rg -n \"\\bValueId\\(57\\)\\b|%57\" /tmp/split.mir` - 期待: `BoxCall` かそれに相当する命令で `dst=%57` が use より前に出る 2) 定義が無い場合: JoinIR→MIR converter を疑う - `JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(sep_len), method: \"length\", .. })` が `MirInstruction::BoxCall { dst: Some(remapped), .. }` として出力されているかを点検する - もし `dst: None` になっている/命令自体が落ちているなら、converter か value-collector の取りこぼしを修正する 3) 定義があるが use より後の場合: ブロック内の命令順の生成/マージ規約を疑う - joinir→mir の「生成順」か merge の「挿入位置」がおかしい - 期待: “def-before-use” が各 BasicBlock 内で成立する 4) 受け入れ基準(P1.5) - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh` が PASS - `tools/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)` → Host `ValueId(57)` 結論: - 「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` ガード)。 ⚠️ 注記: - これにより「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 bb10` - `Undefined 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 5` - `Phase 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.rs` - `verify_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.rs` - `run_all_pipeline_checks()`(薄い集約エントリ) - `debug_log_boundary_contract()` を同ファイルへ移設(`is_joinir_debug()` ガード) - `src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs` - `run_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 bb10` - `Undefined 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.hako` PASS - `./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.sh` PASS(exit=3) - `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh` PASS(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 抽出の広域統一:既存の `MirInstruction` helper との重複が出やすいので要調査の上で ### 小さな整理(今後の予定 / 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 結果: - `./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` ### P1.13.5: Boundary SSOT Unification - Pattern4/6/7 (完了) 背景: - P1.13 で Pattern2 の boundary entry_param_mismatch を根治したが、同じ hardcoded ValueId パターンが Pattern4/6/7 にも存在していた 実施内容: - Pattern4/6/7 の `boundary.join_inputs` を hardcoded ValueId(0), ValueId(PARAM_MIN + k) から撤去 - 全パターン(2, 3, 4, 6, 7)で `join_module.entry.params.clone()` を SSOT として統一 - Fail-fast validation(params count mismatch)を全パターンに追加 - 共通ヘルパ `get_entry_function()` を抽出(`patterns/common/joinir_helpers.rs`) 修正ファイル: - `src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs` - `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs` - `src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs` - `src/mir/builder/control_flow/joinir/patterns/common/joinir_helpers.rs` (新規・ヘルパ追加) 結果: - Entry param mismatch の構造的防止(全パターン統一) - Magic number の完全撤去(PARAM_MIN の hardcoded 使用ゼロ) - コードの保守性・一貫性向上(~40行の重複削減) コミット: - `636b1406b` refactor(joinir): extract get_entry_function helper - `17049d236` docs(phase256): document P1.13.5 boundary SSOT unification 次(P1.5 Task 3): - `ValueId(57)` が「何の JoinIR 値の remap 結果か」を確定し、定義側(dst)が MIR に落ちているかを追う - 例: `sep_len = sep.length()` の BoxCall dst が収集/変換/順序のどこかで欠けていないか