## Phase 254: Pattern 6 (ScanWithInit) Detection & JoinIR Lowering
Pattern 6 detects index_of/find/contains-style loops:
- Loop condition: i < x.length()
- Loop body: if with method call condition + early return
- Step: i = i + 1
- Post-loop: return not-found value (-1)
Key features:
- Minimal lowering: main/loop_step/k_exit functions
- substring hoisted to init-time BoxCall
- Two k_exit jumps (found: i, not found: -1)
- Tests: phase254_p0_index_of_min.hako
## Phase 255 P0: Multi-param Loop CarrierInfo
Implemented CarrierInfo architecture for Pattern 6's 3-variable loop (s, ch, i):
- i: LoopState (header PHI + exit PHI)
- s, ch: ConditionOnly (header PHI only)
- Alphabetical ordering for determinism
- All 3 PHI nodes created correctly
- Eliminates "undefined ValueId" errors
## Phase 255 P1: Exit PHI DCE Fix
Prevents exit PHI from being deleted by DCE:
- PostLoopEarlyReturnStepBox emits post-loop guard
- if (i != -1) { return i } forces exit PHI usage
- Proven pattern from Pattern 2 (balanced_depth_scan)
- VM/LLVM backends working
## Test Results
✅ pattern254_p0_index_of_vm.sh: PASS (exit code 1)
✅ pattern254_p0_index_of_llvm_exe.sh: PASS (mock)
✅ Quick profile: json_lint_vm PASS (progresses past index_of)
✅ Pattern 1-5: No regressions
## Files Added
- src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs
- src/mir/join_ir/lowering/scan_with_init_minimal.rs
- apps/tests/phase254_p0_index_of_min.hako
- docs/development/current/main/phases/phase-254/README.md
- docs/development/current/main/phases/phase-255/README.md
🧠 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Status: Active Scope: Phase 255 (Pattern 6 multi-param loop wiring/PHI 対応) Related:
- docs/development/current/main/10-Now.md
- docs/development/current/main/phases/phase-254/README.md
Phase 255: Multi-param loop の boundary/PHI/wiring を SSOT 化する
前フェーズ(Phase 254)からの引き継ぎ
Phase 254 で Pattern 6 (ScanWithInit) の実装が完了したが、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のみ) with_loop_var_name()が1つの変数名を受け取るexit_bindingsが単一 carrier を想定
Pattern 6 の要求(3変数ループ)
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
}
必要な変数:
s: haystack(ループ不変)ch: needle(ループ不変)i: loop index(ループ状態)
問題の詳細
-
PHI ノード作成の不足:
// 期待: 3つの PHI ノード %4 = phi [%0, entry], [%4, loop] // s %5 = phi [%1, entry], [%5, loop] // ch %6 = phi [%2, entry], [%6, loop] // i // 実際: 1つだけ作られる %4 = phi [%0, entry], [%4, loop] // s だけ // %5 と %6 が undefined → 実行時エラー! -
影響箇所:
JoinInlineBoundaryBuilder::with_loop_var_name()- 単一変数想定LoopExitBindingの使い方 - exit_bindings に複数指定しても PHI が作られないJoinIRConversionPipelineの merge 処理 - 複数 PHI 作成に未対応
解決方針(SSOT 化)
方針: LoopState と Invariants を分けて wiring
LoopState(変化する変数):
i: ループカウンタ- 毎イテレーションで更新される
- PHI ノードが必要
- exit_binding で final value を取得
Invariants(不変な変数):
s,ch: ループ内で参照するが変化しない- PHI ノードは必要だが、すべてのイテレーションで同じ値
phi [init, init, init, ...]の形になる
実装戦略
Option A(推奨): Boundary に invariants フィールド追加
pub struct JoinInlineBoundary {
pub join_inputs: Vec<ValueId>, // 既存(main の params)
pub host_inputs: Vec<ValueId>, // 既存(host 側の ValueId)
// 新規追加
pub loop_invariants: Vec<(String, ValueId)>, // (変数名, host ValueId)
pub exit_bindings: Vec<LoopExitBinding>, // 既存(LoopState 用)
pub loop_var_name: Option<String>, // 既存(単一変数用、廃止予定)
}
利点:
- 既存の Pattern 1-5 に影響なし
- invariants の扱いを明示的に分離
- PHI 生成ロジックを追加しやすい
Option B: exit_bindings を拡張
pub enum CarrierRole {
LoopState, // 既存: 変化する変数
ConditionOnly, // 既存: 条件のみ
LoopInvariant, // 新規: ループ不変
}
問題点:
CarrierRole::LoopInvariantを追加しても、現状の merge ロジックが対応していない- exit_bindings は "exit value" を想定しているが、invariants は "同じ値を保持" という意味合いが異なる
推奨: Option A
- invariants を明示的に分離
- merge ロジックに invariants 専用の PHI 生成を追加
実装タスク(P0)
Task 1: Boundary 構造拡張
ファイル: src/mir/join_ir/lowering/inline_boundary.rs
JoinInlineBoundaryにloop_invariantsフィールド追加JoinInlineBoundaryBuilder::with_loop_invariants()メソッド追加
Task 2: PHI 生成ロジック拡張
ファイル: src/mir/join_ir/lowering/inline_boundary.rs または merge 関連
loop_invariantsから PHI ノードを生成- すべてのブロックで同じ値を持つ PHI として作成
- variable_map に登録
Task 3: Pattern 6 の boundary 構築を修正
ファイル: src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs
with_loop_invariants()を使って s, ch を登録exit_bindingsは i のみ(LoopState)
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(
vec![s_param, ch_param, i_param],
vec![s_host, ch_host, i_host],
)
.with_loop_invariants(vec![
(parts.haystack.clone(), s_host),
(parts.needle.clone(), ch_host),
])
.with_exit_bindings(vec![i_exit_binding])
.with_loop_var_name(Some(parts.loop_var.clone()))
.build();
Task 4: Unit Tests
ファイル: src/mir/join_ir/lowering/inline_boundary.rs (tests module)
- invariants を含む boundary 作成テスト
- PHI ノードが正しく生成されることを確認
Task 5: Integration Tests
実行:
HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh
HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_llvm_exe.sh
期待: 両方 PASS(exit code 1)
禁止事項
- ❌ workaround / by-name 分岐 / ハック禁止
- ❌ Pattern 1-5 の動作を変更しない(regression 禁止)
- ❌ フォールバック処理の追加禁止(Fail-Fast 原則維持)
受け入れ基準
- ✅ phase254_p0_index_of_vm.sh が PASS
- ✅ phase254_p0_index_of_llvm_exe.sh が PASS
- ✅ 最終的に
--profile quickの最初の FAIL が次へ進む(index_of で freeze しない) - ✅ Pattern 1-5 の既存テストがすべて PASS(regression なし)
進捗(P0)
- Task 1: Boundary 構造拡張: 未着手
- Task 2: PHI 生成ロジック拡張: 未着手
- Task 3: Pattern 6 boundary 修正: 未着手
- Task 4: Unit Tests: 未着手
- Task 5: Integration Tests: 未着手