Files
hakorune/docs/development/current/main/phases/phase-256

Phase 256: StringUtils.split/2 Pattern Support

Status: Active Scope: Loop pattern recognition for split/tokenization operations Related:

  • Phase 255 完了loop_invariants 導入、Pattern 6 完成)
  • Phase 254 完了Pattern 6 index_of 実装)

Current Status (SSOT)

  • Current first FAIL: StringUtils.split/2jump_args length mismatchCarrier 'i' has no latch incoming set
  • Pattern6 は PASS 維持
  • 直近の完了:
    • P1.7: SSA undef%49/%67根治continuation 関数名の SSOT 不一致)
    • P1.6: pipeline contract checks を run_all_pipeline_checks() に集約
  • 次の作業: P1.8Pattern7 の carrier PHI wiring / ExitLine + jump_args 契約の修正)

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
}

分析

ループ構造

  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 APattern 7 新設)を推奨。

理由:

  • 可変 stepthen: i = start / else: i = i + 1)は既存の ConstStep 前提と相性が悪い。
  • Pattern 2 を膨らませず、tokenization 系の “専用パターン” として箱化した方が責務が綺麗。

P0 タスク

  1. 最小 fixture + v2 smokeintegration
  • 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
  1. 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” がある(任意だがあると精度が上がる)
  1. 抽出箱Parts
  • i / start / result / s / separator を抽出
  • then/else の更新式(可変 step と const stepを抽出
  1. JoinIR lowerer専用
  • loop_state: i, start
  • invariants: s, separator, resultresult は更新されるので carrier 扱いが必要なら role を明確に)
  • then/else で異なる i_nextSelect もしくは branch で表現(設計 SSOT は JoinIR 側で決める)
  1. 検証
  • 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 & smokesintegration:
    • 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 6index_of回帰: PASSSelect 導入後もインフラは健全)
  • 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_1JoinValueSpace のローカル ValueId初期化されないまま i + const_1 に使用されていた。
    • これが remap 後に ValueId(57) となり、最終 MIR で「use のみ / def が無い」になっていた。
  • 修正:
    • const_1 = 1JoinInst::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 の対応(個数/順序/ValueIdVM 実行前に 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.6候補 / 先送り推奨)

現時点split がまだ FAILでは、箱化のための箱化で複雑さが増えやすいので、以下を推奨する:

  • 先にやってよい(低リスク / 価値が高い)

    • contract_checks.rs 内で「チェックの実行順」を run_all_*() のような薄い関数にまとめるdyn trait は不要)
    • debug ログは既存の仕組み(is_joinir_debug() / DebugOutputBox)に寄せる(新 logger box を増やさない)
    • テストの JoinModule 構築は、重複が 3 箇所以上になった時点で共通化
  • 先送りsplit を PASS してから)

    • ContractCheckerBoxtrait 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.5 Task 3:

  • ValueId(57) が「何の JoinIR 値の remap 結果か」を確定し、定義側dstが MIR に落ちているかを追う
    • 例: sep_len = sep.length() の BoxCall dst が収集/変換/順序のどこかで欠けていないか