Status: Completed (Blocked by Phase 255) Scope: Phase 254 (`--profile quick` 回帰: JoinIR 未対応 loop パターン(StringUtils.index_of/2)) Related: - docs/development/current/main/10-Now.md - docs/development/current/main/phases/phase-253/README.md - docs/development/current/main/phases/phase-255/README.md (次フェーズ: multi-param loop wiring) # Phase 254: `StringUtils.index_of/2` の loop パターンを JoinIR で受理する ## 現象(最初の FAIL) `./tools/smokes/v2/run.sh --profile quick` が `json_lint_vm` で失敗する。 エラー: ``` [joinir/freeze] Loop lowering failed: JoinIR does not support this pattern, and LoopBuilder has been removed. Function: StringUtils.index_of/2 ``` ## 対象の Nyash コード(最小形) `apps/lib/json_native/utils/string.hako`: ```nyash index_of(s, ch) { local i = 0 loop(i < s.length()) { if s.substring(i, i + 1) == ch { return i } i = i + 1 } return -1 } ``` 特徴: - while ループ(`i < s.length()`) - loop body 内に `if (...) { return i }` - tail に `i = i + 1` - ループ後に `return -1` ## 問題の構造(なぜ落ちるか) - 現状は LoopBuilder が削除済みなので、JoinIR パターンがマッチしない loop はすべて freeze する(フォールバックなし)。 - つまり “この loop 形” を JoinIR パターンとして受理できるようにするしかない。 ## 解決方針(構造的) ### 方針 A(推奨・最小): 「index_of 形」を Pattern として追加する - “箱”として 1つ追加し、1つの質問だけに答える: - 「`index_of` 形の loop を JoinIR に lowering できるか?」 - by-name(関数名/Box名)でのディスパッチは禁止。 - 形(構造)だけでマッチすること。 ### 方針 B(非推奨): freeze を回避するための例外ルート - LoopBuilder が無い以上、ここで例外 fallback を入れるのは Fail-Fast 原則違反になりやすい。 - 例: “Unsupported なら解釈器で実行” のような逃げ道は作らない。 ## 実装タスク(P0) ### 1) 構造抽出(docs → interface) - どの AST/StepTree 形を受理するかを README or doc comment に明記 - 必須: loop cond が `<`/`<=` の比較で、rhs が `s.length()` または定数/変数(Phase 251/252 の流れと整合) - 必須: loop 内 if が “then return i” である - 必須: update が `i = i + 1`(Phase 253 で analyzer を緩くしたので、ここは loop パターン側の契約にする) ### 2) 新しい lowering 箱(案) 候補配置: - `src/mir/join_ir/lowering/loop_patterns/` 配下に `index_of/` を作り、`lower_box.rs` を置く - 目的: 既存 `loop_with_*` 巨大ファイルに寄せず、責務を分離する(設計優先) 入出力(案): - 入力: loop condition AST, loop body AST, `current_static_box_name`, env/JoinValueSpace など(Pattern2 と同等の配線) - 出力: `(JoinModule, JoinFragmentMeta)`(既存パターンと同様) ### 3) condition lowering の利用 - `s.substring(i, i+1) == ch` の中で `substring` が必要になる。 - CoreMethodId では `StringSubstring` は `allowed_in_init=true` / `allowed_in_condition=false`。 - ここは “value expression” として lowering するため、 - 既存 `condition_lowerer::lower_value_expression` / `MethodCallLowerer::lower_for_init` のルールに合わせる - 条件全体の bool は `Compare` で生成(JoinIR の `MirLikeInst::Compare`) ### 4) テストと fixture(仕様固定) - unit test: - “index_of 形の loop がマッチする” / “JoinIR が生成される” を固定 - v2 smoke fixture: - `apps/tests/phase254_p0_index_of_min.hako` - `tools/smokes/v2/profiles/quick/apps/phase254_p0_index_of_vm.sh`(軽いなら quick へ、重いなら integration) ## 受け入れ基準 - `./tools/smokes/v2/run.sh --profile quick` が PASS - by-name 分岐を追加していない(構造のみでマッチ) - unsupported の場合は明確な Err(freeze)を維持しつつ、対象形は通る ## 進捗(P0/P1 完了) ### ✅ 完了項目 - **Task 1**: 最小 fixture + smoke scripts(integration): ✅ 完了 - `apps/tests/phase254_p0_index_of_min.hako` - `tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh` - `tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_llvm_exe.sh` - **Task 2**: デバッグ実行(現状把握): ✅ 完了 - Pattern 3 と判定されるが "if-sum ではない" で reject される - loop_canonicalizer の `ConstStep` 不足で fail するケースがある - **Task 3**: Pattern6 DetectorBox 実装: ✅ 完了 - `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs` - can_lower() - Pattern 3 より前に配置(router priority 調整) - `Pattern2Break` と `Pattern3IfPhi` の双方の分類に対応 - **検出成功**: `Pattern6_ScanWithInit MATCHED` を確認 - **Task 4**: ScanWithInit Lowerer 実装: ✅ 完了 - **Task 4-1**: extract_scan_with_init_parts() - 構造抽出関数実装 - **Task 4-2**: scan_with_init_minimal.rs - JoinIR lowerer(main/loop_step/k_exit 生成) - **Task 4-3**: MirBuilder 統合 - boundary 構築と JoinIRConversionPipeline 実行 - **Task 4-4**: mod.rs 登録 ### ❌ ブロッカー(Phase 255 へ引き継ぎ) **現象**: Integration テスト失敗 ``` [ERROR] ❌ [rust-vm] VM error: Invalid value: [rust-vm] use of undefined value ValueId(10) (fn=StringUtils.index_of/2, last_block=Some(BasicBlockId(4)), last_inst=Some(Compare { dst: ValueId(14), op: Ge, lhs: ValueId(10), rhs: ValueId(13) })) ``` **根本原因**: JoinIR→MIR merge/boundary システムが**複数ループ変数を想定していない** - 現状の仕様: Pattern 1-5 は単一ループ変数前提(例: `i` のみ) - Pattern 6 の要求: 3変数ループ(`s`, `ch`, `i`) - 問題: PHI ノードが 1つしか作られない(`s` のみ)、`ch` と `i` が undefined **影響範囲**: - `JoinInlineBoundaryBuilder` の `with_loop_var_name()` が単一変数想定 - `exit_bindings` が単一 carrier 用に設計 - `JoinIRConversionPipeline` が複数 PHI 作成に未対応 **Phase 254 の受け入れ境界**: - ✅ Pattern 6 が検出される - ✅ JoinIR が正しく生成される(main/loop_step/k_exit 構造) - ✅ substring が BoxCall として init-time に emit される - ❌ 実行(VM/LLVM)で PASS にするのは **Phase 255 の範囲** ### 次フェーズ(Phase 255) 詳細: [docs/development/current/main/phases/phase-255/README.md](phase-255/README.md) 課題: - Multi-param loop の boundary/PHI/wiring を SSOT 化 - LoopState(i)と invariants(s, ch)を分けて wiring - 受け入れ: phase254_p0_index_of の integration テストが PASS