diff --git a/apps/tests/phase254_p0_index_of_min.hako b/apps/tests/phase254_p0_index_of_min.hako new file mode 100644 index 00000000..b8dcb769 --- /dev/null +++ b/apps/tests/phase254_p0_index_of_min.hako @@ -0,0 +1,21 @@ +// Phase 254 P0: index_of 形 loop(最小テスト) +static box StringUtils { + 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 + } +} + +static box Main { + main() { + // Test case: "abc" の中の "b" を探す + local result = StringUtils.index_of("abc", "b") + return result // Expected: 1 + } +} diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index a53a102f..7b113265 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -4,18 +4,75 @@ - Phase 141 P2+: Call/MethodCall 対応(effects + typing を分離して段階投入、ANF を前提に順序固定) - Phase 143-loopvocab P3+: 条件スコープ拡張(impure conditions 対応) -- Phase 146-147(planned): Loop/If condition への ANF 適用(順序固定と診断の横展開) - 詳細: `docs/development/current/main/30-Backlog.md` -## 2025-12-19:Phase 146(着手)✅ +## 2025-12-19:Phase 146/147 完了 ✅ - Phase 146 README: `docs/development/current/main/phases/phase-146/README.md` - Fixtures: - `apps/tests/phase146_p0_if_cond_unified_min.hako`(P0: pure cond, expected exit 7) - - `apps/tests/phase146_p1_if_cond_intrinsic_min.hako`(P1 planned: `s.length() == 3`) + - `apps/tests/phase146_p1_if_cond_intrinsic_min.hako`(P1: `s.length() == 3`, expected exit 7) - Smokes: - `tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh` - `tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh` + - `tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_vm.sh` + - `tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_llvm_exe.sh` +- Flags: + - `HAKO_ANF_DEV=1`(dev-only: ANF routing 有効化) + - `HAKO_ANF_ALLOW_PURE=1`(dev-only: PureOnly scope で ANF 有効化) + +## 2025-12-19:Phase 251 Fix(JoinIR 条件変数抽出 / デバッグ出力整理)✅ + +- Phase 251 README: `docs/development/current/main/phases/phase-251/README.md` +- Fix: + - `collect_variables_recursive()` を拡張し、`MethodCall/FieldAccess/Index/Call` の基底変数を ConditionEnv に登録できるようにした + - `loop_with_if_phi_if_sum.rs` の無条件 `eprintln!` を `is_joinir_debug()` ガードに移した(デフォルトは clean output) +- Status: + - 元の回帰(`arr.length()` の `arr` が ConditionEnv に入らない): 解決 + - `--profile quick` の `json_lint_vm` は別件で失敗が残る(JoinIR Pattern2 の break 条件で `MethodCall` が未対応) + +## 2025-12-19:Phase 252 P0/P1(Pattern2 break 条件: `this.methodcall`)✅ + +- Phase 252 README: `docs/development/current/main/phases/phase-252/README.md` +- Status: + - `cargo check` は通過(0 errors) + - `--profile quick` は次の FAIL が残る → Phase 253(`[joinir/mutable-acc-spec]`) + +## 2025-12-19:Phase 255(Multi-param loop wiring)🔜 ← 現在ここ! + +- Phase 255 README: `docs/development/current/main/phases/phase-255/README.md` +- Goal: Pattern 6 (index_of) の integration テストを PASS にする +- Current first FAIL: + - `VM error: use of undefined value ValueId(10)` (StringUtils.index_of/2) + - 根本原因: JoinIR boundary/PHI システムが単一ループ変数前提で、3変数ループ(s, ch, i)に未対応 +- 方針: + - Boundary に `loop_invariants` フィールド追加(LoopState と invariants を分離) + - PHI 生成ロジックを拡張して invariants の PHI を作成 +- 受け入れ: + - phase254_p0_index_of_vm.sh PASS + - phase254_p0_index_of_llvm_exe.sh PASS + - `--profile quick` の最初の FAIL が次へ進む + +## 2025-12-19:Phase 254(index_of loop pattern)✅ 完了(Blocked by Phase 255) + +- Phase 254 README: `docs/development/current/main/phases/phase-254/README.md` +- Status: **Pattern 6 実装完了、ただし実行失敗(Phase 255 で unblock)** +- 完了項目: + - ✅ Pattern 6 DetectorBox 実装(`Pattern6_ScanWithInit MATCHED`) + - ✅ extract_scan_with_init_parts() - 構造抽出 + - ✅ scan_with_init_minimal.rs - JoinIR lowerer(main/loop_step/k_exit 生成) + - ✅ MirBuilder 統合 - boundary 構築と merge 実行 + - ✅ substring を BoxCall として init-time に emit +- ブロッカー: + - JoinIR→MIR merge/boundary が複数ループ変数(s, ch, i)に未対応 + - PHI ノードが 1つしか作られず、undefined value エラー +- **Phase 254 の受け入れ境界**: Pattern 6 検出+JoinIR 生成まで ✅ +- **実行 PASS は Phase 255 の範囲** + +## 2025-12-19:Phase 253(mutable-acc-spec)✅ + +- Phase 253 README: `docs/development/current/main/phases/phase-253/README.md` +- Goal: `--profile quick` を緑に戻す(対処療法なし、analyzer 契約の整理で直す) ## 2025-12-19:Phase 145-anf P0/P1/P2 完了 ✅ diff --git a/docs/development/current/main/phases/phase-146/README.md b/docs/development/current/main/phases/phase-146/README.md index bd70d034..29465f39 100644 --- a/docs/development/current/main/phases/phase-146/README.md +++ b/docs/development/current/main/phases/phase-146/README.md @@ -1,6 +1,6 @@ # Phase 146: Loop/If Condition ANF Implementation -**Status**: P0 in progress / P1 planned +**Status**: P0/P1/P147 complete **Date**: 2025-12-19 **Context**: Phase 145 P0/P1/P2 complete (ANF infrastructure). Phase 146/147 adds condition expression support. @@ -15,7 +15,7 @@ Phase 146 enables ANF (A-Normal Form) transformation for loop and if conditions, ### Implementation 1. **expr_lowerer_box.rs**: Added scope check to ANF routing (L54-79) - - `PureOnly` scope: Skip ANF (P1 will enable) + - `PureOnly` scope: Skip ANF (P1 で dev-only 有効化) - `WithImpure` scope: Try ANF (Phase 145 behavior) 2. **post_if_post_k.rs**: Replaced legacy inline lowering with SSOT (L271-285) @@ -43,30 +43,48 @@ Phase 146 enables ANF (A-Normal Form) transformation for loop and if conditions, - [x] Scope check added to ANF routing - [x] Legacy inline lowering removed from post_if_post_k.rs - [x] SSOT unified (lower_expr_with_scope is only entry point) -- [ ] Build passes (cargo build --release) -- [ ] Tests pass (cargo test --release --lib) -- [ ] Phase 145 regression: 0 failures -- [ ] Fixture exit code: 7 (VM + LLVM EXE) +- [x] Build passes (cargo build --release) +- [x] Tests pass (cargo test --release --lib) +- [x] Phase 145 regression: 0 failures +- [x] Fixture exit code: 7 (VM + LLVM EXE) -## Phase 146 P1: 条件式 ANF 有効化(planned) +## Phase 146 P1: 条件式 ANF 有効化(done) **Goal**: Enable ANF in conditions for `PureOnly` scope behind a dev flag, starting with whitelisted intrinsic (`String.length()`). -### Planned Implementation +### Implementation -1. **expr_lowerer_box.rs**: Allow ANF for PureOnly with env flag -2. **anf/execute_box.rs**: Add Compare operator support +1. **expr_lowerer_box.rs**: Allow ANF for PureOnly with `HAKO_ANF_ALLOW_PURE=1` +2. **anf/execute_box.rs**: Add Compare operator support (`== != < <= > >=`) 3. **config/env/joinir_dev.rs**: Add `anf_allow_pure_enabled()` function +4. **Whitelist**: KnownIntrinsic registry を利用し、`String.length()` のみ許可 -## Phase 147 P0: 複合条件の順序固定(planned) +### Files Created (P1) + +- `apps/tests/phase146_p1_if_cond_intrinsic_min.hako` (exit code 7) +- `tools/smokes/.../phase146_p1_if_cond_intrinsic_vm.sh` +- `tools/smokes/.../phase146_p1_if_cond_intrinsic_llvm_exe.sh` + +### Acceptance Criteria (P1) + +- [x] `HAKO_ANF_ALLOW_PURE=1` で PureOnly scope の ANF が有効化される +- [x] `String.length()` のみ許可され、他の MethodCall は out-of-scope +- [x] Fixture exit code: 7 (VM + LLVM EXE) + +## Phase 147 P0: 複合条件の順序固定(done) **Goal**: Extend recursive ANF to Compare operators for compound conditions. -**Status**: Not yet implemented +### Implementation -### Planned Implementation +1. **anf/contract.rs**: `AnfParentKind::Compare` を追加 +2. **anf/plan_box.rs**: Compare vs BinaryOp を判別して ParentKind を決定 +3. **anf/execute_box.rs**: Compare でも再帰的 ANF を適用(left-to-right) -1. **anf/plan_box.rs**: Add Compare case to plan_expr() +### Acceptance Criteria (P147) + +- [x] Compare を含む複合条件でも評価順序が固定される +- [x] ANF の再帰が Compare にも適用される ## Testing @@ -82,6 +100,20 @@ Phase 146 enables ANF (A-Normal Form) transformation for loop and if conditions, # Expected: exit 7 ``` +### P1 Smoke Tests + +```bash +# VM (dev-only) +HAKO_ANF_DEV=1 HAKO_ANF_ALLOW_PURE=1 \ + ./tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_vm.sh +# Expected: exit 7 + +# LLVM EXE (dev-only) +HAKO_ANF_DEV=1 HAKO_ANF_ALLOW_PURE=1 \ + ./tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_llvm_exe.sh +# Expected: exit 7 +``` + ### Regression Tests Phase 145 smokes must still pass: diff --git a/docs/development/current/main/phases/phase-251/README.md b/docs/development/current/main/phases/phase-251/README.md new file mode 100644 index 00000000..3ebd4176 --- /dev/null +++ b/docs/development/current/main/phases/phase-251/README.md @@ -0,0 +1,86 @@ +Status: Active +Scope: Phase 251 (JoinIR 条件変数抽出の回帰修正 + 出力のクリーンアップ) +Related: +- docs/development/current/main/10-Now.md +- docs/reference/environment-variables.md + +# Phase 251 Fix: json_lint_vm 回帰(ConditionEnv 変数抽出 / 出力ノイズ) + +## 目的 + +- 回帰の修正(`json_lint_vm` で `arr.length()` 等の基底変数が ConditionEnv に入らず落ちる) +- smoke 出力を壊す無条件ログ(`eprintln!`)の除去 + +## 実装(完了) + +### 1) 条件変数抽出の拡張(核心) + +ファイル: +- `src/mir/join_ir/lowering/condition_var_extractor.rs` + +問題: +- 例: `loop (j < valid.length())` の `valid` が抽出されず、ConditionEnv で `Variable 'valid' not found` になる + +原因: +- `collect_variables_recursive()` が `MethodCall` 等の複合式を辿れていなかった + +対応: +- `collect_variables_recursive()` に以下の AST ノード処理を追加し、基底と引数側を再帰収集する + - `MethodCall`: `arr.length()` から `arr` を抽出(引数も再帰) + - `FieldAccess`: `obj.count` から `obj` を抽出 + - `Index`: `arr[i]` から `arr` と `i` を抽出 + - `Call`: callee と arguments を再帰(関数参照・引数の変数を拾う) + +### 2) デバッグ出力のクリーンアップ + +ファイル: +- `src/mir/join_ir/lowering/loop_with_if_phi_if_sum.rs` + +問題: +- 無条件 `eprintln!` が quick smoke の期待出力を壊す + +対応: +- `crate::config::env::is_joinir_debug()` ガードで条件付き出力に変更 +- 推奨 env: `HAKO_JOINIR_DEBUG=1`(`NYASH_JOINIR_DEBUG` は legacy) + +### 3) ユニットテスト追加 + +ファイル: +- `src/mir/join_ir/lowering/condition_var_extractor.rs` + +追加: +- `MethodCall/FieldAccess/Index` などの変数抽出が期待通りであることを固定(回帰防止) + +## 検証(Phase 251 の範囲) + +- 元の回帰(`arr.length()` の `arr` が ConditionEnv に入らない): 解決 +- `--profile quick` の `json_lint_vm`: 別件の失敗が露出(Phase 251 対象外) + +## 残タスク(次の指示書 / Phase 252 案) + +### 現象 + +- JoinIR Pattern2 の break 条件 lowering が `MethodCall` を扱えず失敗する + - 例: `Me.is_whitespace(s.substring(i, i + 1))` のような `MethodCall` を含む条件 + +### 指示(Claude 実装用) + +1. 再現コマンド + - `./tools/smokes/v2/run.sh --profile quick` + - 追加ログが必要なら `HAKO_JOINIR_DEBUG=1`(smoke 期待出力に混ざるため、比較用の runs では OFF にする) + +2. 構造的な修正方針 + - 「条件 lowering 専用の箱」側で `ASTNode::MethodCall`(必要なら `Me` / `Call` / `FieldAccess` / `Index`)を fail-fast で受理できるようにする + - 実装は 2 ルートのどちらかに統一する + - A) 条件式を事前に式 lowering(ANF を含む)で `ValueId` に落としてから branch 用の bool 値として扱う + - B) 受理範囲を明確化し、MethodCall を KnownIntrinsic に限定して lowering(逸脱はエラー) + +3. ドキュメントとテスト(コードの前) + - `condition_lowering_box` の責務(受理する AST の境界)を README / doc comment で明文化 + - `MethodCall` を含む break 条件を最小 fixture で固定し、v2 smoke に追加(quick 既定に入れるかは要検討) + +4. 受け入れ基準 + - `./tools/smokes/v2/run.sh --profile quick` が緑 + - デフォルトで出力が汚れない(`HAKO_JOINIR_DEBUG=1` の時だけ追加ログ) + - by-name や特定関数名での分岐など、対処療法的ハードコードを追加しない + diff --git a/docs/development/current/main/phases/phase-252/README.md b/docs/development/current/main/phases/phase-252/README.md new file mode 100644 index 00000000..0c3c56cc --- /dev/null +++ b/docs/development/current/main/phases/phase-252/README.md @@ -0,0 +1,66 @@ +Status: Completed +Scope: Phase 252 (JoinIR Pattern2 break 条件: `this.methodcall(...)` 対応 + policy SSOT) +Related: +- docs/development/current/main/10-Now.md +- docs/development/current/main/phases/phase-251/README.md + +# Phase 252: Pattern2 break 条件の `this.methodcall(...)` 対応 + +## 目的 + +- `--profile quick` の `json_lint_vm` で露出した JoinIR Pattern2 の回帰を潰す。 +- 具体的には `if not this.is_whitespace(s.substring(i, i + 1)) { break }` のような + `this.methodcall(...)` を break 条件として lowering できるようにする。 + +## 実装(P0/P1: 完了) + +### 1) ユーザー定義メソッドの許可ポリシー(SSOT) + +ファイル: +- `src/mir/join_ir/lowering/user_method_policy.rs` + +要点: +- CoreMethodId(builtin)とは別に、`this.methodcall(...)` の「許可」を一箇所に集約する。 +- by-name の if 分岐で散らさず、ポリシーテーブルとして SSOT 化する。 + +### 2) ConditionLowerer: `ASTNode::MethodCall(object: Me, ...)` の受理 + +ファイル: +- `src/mir/join_ir/lowering/condition_lowerer.rs` + +要点: +- break 条件のトップレベルが `MethodCall(Me, ...)` の場合に lowering できる分岐を追加。 +- `this` の所属 box 名は `current_static_box_name` を経由して受け取る(固定名分岐しない)。 + +### 3) `current_static_box_name` の配線(Pattern2 まで) + +変更点: +- `ConditionContext` に `current_static_box_name` を追加 +- Pattern2 lowering 入力(inputs)から break/header 条件 lowering まで `current_static_box_name` を伝搬 + +注: +- ここは “構造” による情報伝達であり、特定関数名での回避分岐(ハードコード)ではない。 + +### 4) 局所リファクタ(DebugOutputBox 統一) + +ファイル: +- `src/mir/join_ir/lowering/loop_with_if_phi_if_sum.rs` + +要点: +- 無条件/散在ログを追加しない方針を維持しつつ、出力 API を `DebugOutputBox` に統一する。 +- デフォルトでは出力ゼロ(smoke の期待出力を壊さない)。 + +### 5) テスト/fixture の追加 + +- unit tests を追加(`this.methodcall(...)` 条件の lowering 回帰固定) +- v2 smoke fixture を追加(integration profile) + +## 検証状況(Phase 252 終点) + +- `cargo check` が通る(0 errors、warnings のみ) +- `--profile quick` の最初の FAIL は次に切り出し(Phase 253): + - `[joinir/mutable-acc-spec] Assignment form not accumulator pattern (required: target = target + x)` + +## 次の作業(Phase 253) + +次の SSOT: `docs/development/current/main/phases/phase-253/README.md` diff --git a/docs/development/current/main/phases/phase-253/README.md b/docs/development/current/main/phases/phase-253/README.md new file mode 100644 index 00000000..c219b489 --- /dev/null +++ b/docs/development/current/main/phases/phase-253/README.md @@ -0,0 +1,83 @@ +Status: Completed +Scope: Phase 253 (`--profile quick` 回帰: mutable-acc-spec / accumulator 判定の改善) +Related: +- docs/development/current/main/10-Now.md +- docs/development/current/main/phases/phase-252/README.md + +# Phase 253: `json_lint_vm` 回帰(mutable-acc-spec) + +## 現象(最初の FAIL) + +`./tools/smokes/v2/run.sh --profile quick` が `json_lint_vm` で失敗する。 + +エラー: + +``` +[ERROR] ❌ MIR compilation error: [joinir/mutable-acc-spec] Assignment form not accumulator pattern (required: target = target + x) +``` + +## 背景(なぜここで落ちるか) + +`Pattern2` の pipeline は、ループ本体の代入から「mutable accumulator(`x = x + y`)」を検出して +最適化/簡略化に利用する。ところが現在の analyzer が Fail-Fast すぎて、 +“accumulator ではない単なる代入” を見つけた時点で Err にしてしまい、JoinIR 経路全体を落としている。 + +対象 SSOT: +- `src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs` + +## 方針(構造的に直す) + +### 原則 + +- “accumulator pattern を検出できた時だけ” spec を返す。 +- それ以外は Err ではなく `Ok(None)` に戻して **別経路(通常 lowering)へ譲る**。 +- 例外として、本当に矛盾があるケースだけ Err(例: 同一変数への複数代入など、既に `Ok(None)` にしている)。 + +### 対処療法の禁止 + +- 特定関数名(`StringUtils.*`)や特定 script 名(`json_lint_vm`)で分岐しない。 +- “`-` の時だけ” のような場当たりでなく、spec と契約として整理する。 + +## 実装タスク(P0) + +### 1) Analyzer の振る舞いを “検出器” に寄せる + +ファイル: +- `src/mir/loop_pattern_detection/mutable_accumulator_analyzer.rs` + +変更案: +- 以下のケースを `Err` ではなく `Ok(None)` に変更する(= accumulator ではないと判断する) + - `value_node` が `BinaryOp` ではない(例: `i = s.length() - 1`) + - `BinaryOperator` が `Add` 以外(例: `i = i - 1`) + - 左辺が `target` と一致しない(例: `x = y + x`) + - RHS が Literal/Variable 以外(例: `x = x + (i + 1)`) + +目的: +- “accumulator っぽくない代入” が混ざる loop でも、JoinIR 全体を落とさずに進める。 + +### 2) `-`(decrement)を accumulator として扱うかの設計を決める(P1 で可) + +選択肢: +- A) `i = i - 1` は “accumulator としては未対応” なので `Ok(None)`(安全・最小) +- B) `i = i - 1` を “step=-1” として spec に載せる(将来の表現力は上がるが、下流の取り扱い整備が必要) + +まずは quick を緑に戻す目的で A を推奨。 + +## テスト(仕様固定) + +- unit tests を追加して「非 accumulator 代入があっても Err にならず `Ok(None)`」を固定する。 + - 例: ループ body に `local i = s.length() - 1` 相当の Assignment があるケース + - 例: `i = i - 1` があるケース + +## 受け入れ基準 + +- `./tools/smokes/v2/run.sh --profile quick` が PASS +- “たまたま `json_lint_vm` だけ通す” ための by-name 分岐を追加していない +- analyzer の戻り値契約が docs と tests で固定されている + +## 結果(Phase 253 終点) + +- `mutable_accumulator_analyzer` は “検出器” として振る舞うようになり、非 accumulator 代入で Err を出さず `Ok(None)` に譲る。 +- quick の最初の FAIL は次に切り出し(Phase 254): + - `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern` + - Function: `StringUtils.index_of/2` diff --git a/docs/development/current/main/phases/phase-254/README.md b/docs/development/current/main/phases/phase-254/README.md new file mode 100644 index 00000000..35292550 --- /dev/null +++ b/docs/development/current/main/phases/phase-254/README.md @@ -0,0 +1,161 @@ +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 diff --git a/docs/development/current/main/phases/phase-255/README.md b/docs/development/current/main/phases/phase-255/README.md new file mode 100644 index 00000000..8d594ad5 --- /dev/null +++ b/docs/development/current/main/phases/phase-255/README.md @@ -0,0 +1,198 @@ +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変数ループ) + +```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 +} +``` + +必要な変数: +- `s`: haystack(ループ不変) +- `ch`: needle(ループ不変) +- `i`: loop index(ループ状態) + +### 問題の詳細 + +1. **PHI ノード作成の不足**: + ```rust + // 期待: 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 → 実行時エラー! + ``` + +2. **影響箇所**: + - `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 フィールド追加 + +```rust +pub struct JoinInlineBoundary { + pub join_inputs: Vec, // 既存(main の params) + pub host_inputs: Vec, // 既存(host 側の ValueId) + + // 新規追加 + pub loop_invariants: Vec<(String, ValueId)>, // (変数名, host ValueId) + + pub exit_bindings: Vec, // 既存(LoopState 用) + pub loop_var_name: Option, // 既存(単一変数用、廃止予定) +} +``` + +**利点**: +- 既存の Pattern 1-5 に影響なし +- invariants の扱いを明示的に分離 +- PHI 生成ロジックを追加しやすい + +#### Option B: exit_bindings を拡張 + +```rust +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` + +1. `JoinInlineBoundary` に `loop_invariants` フィールド追加 +2. `JoinInlineBoundaryBuilder::with_loop_invariants()` メソッド追加 + +### Task 2: PHI 生成ロジック拡張 + +**ファイル**: `src/mir/join_ir/lowering/inline_boundary.rs` または merge 関連 + +1. `loop_invariants` から PHI ノードを生成 +2. すべてのブロックで同じ値を持つ PHI として作成 +3. variable_map に登録 + +### Task 3: Pattern 6 の boundary 構築を修正 + +**ファイル**: `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs` + +1. `with_loop_invariants()` を使って s, ch を登録 +2. `exit_bindings` は i のみ(LoopState) + +```rust +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) + +1. invariants を含む boundary 作成テスト +2. PHI ノードが正しく生成されることを確認 + +### Task 5: Integration Tests + +**実行**: +```bash +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: 未着手 diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index c54750d7..576080f1 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -197,6 +197,18 @@ NYASH_CLI_VERBOSE=2 \ --- +## ANF / Normalized (dev-only) + +| 変数 | デフォルト | 適用経路 | 説明 | +| --- | --- | --- | --- | +| `HAKO_ANF_DEV=1` | OFF | Any | ANF routing を dev-only で有効化。撤去条件: ANF が本線化した時に削除 | +| `HAKO_ANF_ALLOW_PURE=1` | OFF | Any | PureOnly scope の ANF を許可(dev-only、`HAKO_ANF_DEV=1` 前提)。撤去条件: PureOnly ANF が本線化した時に削除 | +| `HAKO_ANF_STRICT=1` | OFF | Any | ANF の fail-fast を有効化(dev-only)。撤去条件: fail-fast を常時化した時に削除 | + +補足: 実装は `src/config/env` に集約し、直読みはしない。 + +--- + ## JoinIR トグル (Phase 72 整理版) JoinIR は制御構造を関数呼び出し + 継続に正規化する IR 層。フラグは config/env のポリシーで集約するよ。 @@ -232,7 +244,7 @@ LoopBuilder は物理削除済みで、JoinIR を OFF にするモードは存 | `HAKO_JOINIR_STAGE1` | OFF | Stage‑1 JoinIR 経路。 | | `HAKO_JOINIR_PRINT_TOKENS_MAIN` | OFF | print_tokens main A/B。 | | `HAKO_JOINIR_ARRAY_FILTER_MAIN` | OFF | array.filter main A/B。 | -| `NYASH_JOINIR_DEBUG` / `HAKO_JOINIR_DEBUG` | OFF | JoinIR デバッグログ。 | +| `NYASH_JOINIR_DEBUG` / `HAKO_JOINIR_DEBUG` | OFF | JoinIR デバッグログ(推奨: `HAKO_JOINIR_DEBUG=1`、`NYASH_*` は legacy)。 | ### Deprecated / 廃止候補 diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs index ea3f5159..ff28ac17 100644 --- a/src/mir/builder/control_flow/joinir/patterns/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs @@ -74,6 +74,7 @@ pub(in crate::mir::builder) mod pattern3_with_if_phi; pub(in crate::mir::builder) mod pattern4_carrier_analyzer; pub(in crate::mir::builder) mod pattern4_with_continue; pub(in crate::mir::builder) mod pattern5_infinite_early_exit; // Phase 131-11 +pub(in crate::mir::builder) mod pattern6_scan_with_init; // Phase 254 P0: index_of/find/contains pattern pub(in crate::mir::builder) mod pattern_pipeline; pub(in crate::mir::builder) mod router; pub(in crate::mir::builder) mod trim_loop_lowering; // Phase 180: Dedicated Trim/P5 lowering module diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs index d53779a4..69b12c86 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs @@ -96,6 +96,8 @@ pub(in crate::mir::builder) struct Pattern2Inputs { pub post_loop_early_return: Option< crate::mir::builder::control_flow::joinir::patterns::policies::post_loop_early_return_plan::PostLoopEarlyReturnPlan, >, + /// Phase 252: Name of the static box being lowered (for this.method(...) in break conditions). + pub current_static_box_name: Option, } pub(crate) struct Pattern2InputsFactsBox; diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs index a933378d..f2bef08c 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs @@ -44,6 +44,9 @@ impl Pattern2LoweringOrchestrator { let promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?; let mut inputs = promoted.inputs; + // Phase 252: Wire current_static_box_name from builder context + inputs.current_static_box_name = builder.comp_ctx.current_static_box.clone(); + let normalized = NormalizeBodyStepBox::run(builder, condition, body, &mut inputs, verbose)?; let normalized_body = normalized.normalized_body; let analysis_body = normalized_body.as_deref().unwrap_or(body); diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs index ef8c19b7..ee6684f6 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs @@ -33,6 +33,7 @@ impl ApplyPolicyStepBox { balanced_depth_scan_recipe: policy.balanced_depth_scan_recipe, carrier_updates_override: policy.carrier_updates_override, post_loop_early_return: policy.post_loop_early_return, + current_static_box_name: None, // Phase 252: TODO - wire from builder.comp_ctx }) } } diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs index c9ac54ab..2e774363 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs @@ -53,6 +53,7 @@ impl EmitJoinIRStepBox { condition_only_recipe: inputs.condition_only_recipe.as_ref(), body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), balanced_depth_scan_recipe: inputs.balanced_depth_scan_recipe.as_ref(), + current_static_box_name: inputs.current_static_box_name.clone(), // Phase 252 }; let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs new file mode 100644 index 00000000..7fdc4ae0 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs @@ -0,0 +1,679 @@ +//! Pattern 6: Scan with Init (index_of/find/contains form) +//! +//! Phase 254 P0: Dedicated pattern for scan loops with init-time method calls +//! +//! ## Pattern Structure +//! +//! ```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 +//! } +//! ``` +//! +//! ## Detection Criteria (Structure Only - No Function Names) +//! +//! 1. Loop condition: `i < x.length()` or `i < len` +//! 2. Loop body has if statement with: +//! - Condition containing MethodCall (e.g., `substring(i, i+1) == ch`) +//! - Then branch: early return (break) +//! 3. Loop body has step: `i = i + 1` +//! 4. Post-loop: return statement (not-found value) +//! +//! ## Why Not Pattern 2? +//! +//! - Pattern 2 expects break condition without init-time MethodCall +//! - This pattern needs MethodCall in condition (substring) +//! - MethodCall allowed_in_condition() = false, but allowed_in_init() = true +//! - Need to hoist MethodCall to init phase + +use super::super::trace; +use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; +use crate::mir::builder::MirBuilder; +use crate::mir::ValueId; + +/// Phase 254 P1: Extracted structure for scan-with-init pattern +/// +/// This structure contains all the information needed to lower an index_of-style loop. +#[derive(Debug, Clone)] +struct ScanParts { + /// Loop variable name (e.g., "i") + loop_var: String, + /// Haystack variable name (e.g., "s") + haystack: String, + /// Needle variable name (e.g., "ch") + needle: String, + /// Step literal (P0: must be 1) + step_lit: i64, + /// Early return expression (P0: must be Variable(loop_var)) + early_return_expr: ASTNode, + /// Not-found return literal (P0: must be -1) + not_found_return_lit: i64, +} + +/// Phase 254 P0: Detection for Pattern 6 (ScanWithInit) +/// +/// Detects index_of/find/contains pattern: +/// - Loop with `i < x.length()` or `i < len` condition +/// - If with MethodCall in condition and early return +/// - ConstStep `i = i + 1` +/// - Post-loop return +/// +/// Detection is structure-based only (no function name checks). +pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool { + use crate::mir::loop_pattern_detection::LoopPatternKind; + + // Phase 254 P0: Accept Pattern2Break OR Pattern3IfPhi + // - Pattern2Break: loop with break statement + // - Pattern3IfPhi: loop with return statement (not counted as break) + // index_of has return (early exit), which is classified as Pattern3IfPhi + match ctx.pattern_kind { + LoopPatternKind::Pattern2Break | LoopPatternKind::Pattern3IfPhi => { + // Continue to structure checks + } + _ => return false, + } + + // Check for if statement with MethodCall in condition + let has_if_with_methodcall = ctx.body.iter().any(|stmt| { + matches!(stmt, ASTNode::If { condition, .. } if contains_methodcall(condition)) + }); + + if !has_if_with_methodcall { + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + "reject: no if with MethodCall in condition", + ); + } + return false; + } + + // Check for ConstStep (i = i + 1) + let has_const_step = ctx.body.iter().any(|stmt| { + matches!(stmt, ASTNode::Assignment { value, .. } if is_const_step_pattern(value)) + }); + + if !has_const_step { + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + "reject: no ConstStep pattern found", + ); + } + return false; + } + + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + "MATCHED: ScanWithInit pattern detected", + ); + } + + true +} + +/// Check if AST node contains MethodCall +fn contains_methodcall(node: &ASTNode) -> bool { + match node { + ASTNode::MethodCall { .. } => true, + ASTNode::BinaryOp { left, right, .. } => { + contains_methodcall(left) || contains_methodcall(right) + } + ASTNode::UnaryOp { operand, .. } => contains_methodcall(operand), + _ => false, + } +} + +/// Check if value is ConstStep pattern (i = i + 1) +fn is_const_step_pattern(value: &ASTNode) -> bool { + match value { + ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Add, + left, + right, + .. + } => { + matches!(left.as_ref(), ASTNode::Variable { .. }) + && matches!(right.as_ref(), ASTNode::Literal { .. }) + } + _ => false, + } +} + +/// Phase 254 P1: Extract scan-with-init pattern parts from loop AST +/// +/// This function analyzes the loop structure and extracts all necessary information +/// for lowering an index_of-style loop to JoinIR. +/// +/// # Arguments +/// +/// * `condition` - Loop condition AST node +/// * `body` - Loop body statements +/// * `fn_body` - Full function body (needed to check post-loop return) +/// +/// # Returns +/// +/// * `Ok(Some(ScanParts))` - Successfully extracted the pattern +/// * `Ok(None)` - Not a scan-with-init pattern (different pattern) +/// * `Err(String)` - Internal consistency error +/// +/// # P0 Restrictions +/// +/// - Loop condition must be `i < s.length()` +/// - Step must be `i = i + 1` (step_lit == 1) +/// - Not-found return must be `-1` +/// - Early return must be `return loop_var` +fn extract_scan_with_init_parts( + condition: &ASTNode, + body: &[ASTNode], + _fn_body: Option<&[ASTNode]>, +) -> Result, String> { + use crate::ast::{BinaryOperator, LiteralValue}; + + // 1. Check loop condition: i < s.length() + let (loop_var, haystack) = match condition { + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Ok(None), + }; + + let haystack = match right.as_ref() { + ASTNode::MethodCall { + object, method, .. + } if method == "length" => match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Ok(None), + }, + _ => return Ok(None), + }; + + (loop_var, haystack) + } + _ => return Ok(None), + }; + + // 2. Find if statement with substring == needle and return loop_var + let mut needle_opt = None; + let mut early_return_expr_opt = None; + + for stmt in body { + if let ASTNode::If { + condition: if_cond, + then_body, + .. + } = stmt + { + // Check if condition is MethodCall(substring) == Variable(needle) + if let ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } = if_cond.as_ref() + { + let substring_side = if matches!(left.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") + { + left.as_ref() + } else if matches!(right.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") + { + right.as_ref() + } else { + continue; + }; + + let needle_side = if std::ptr::eq(substring_side, left.as_ref()) { + right.as_ref() + } else { + left.as_ref() + }; + + if let ASTNode::Variable { name: needle_name, .. } = needle_side { + // Check then_body contains return loop_var + if then_body.len() == 1 { + if let ASTNode::Return { value, .. } = &then_body[0] { + if let Some(ret_val) = value { + if let ASTNode::Variable { name: ret_name, .. } = ret_val.as_ref() { + if ret_name == &loop_var { + needle_opt = Some(needle_name.clone()); + early_return_expr_opt = Some(ret_val.as_ref().clone()); + } + } + } + } + } + } + } + } + } + + let needle = needle_opt.ok_or_else(|| "No matching needle pattern found")?; + let early_return_expr = early_return_expr_opt.ok_or_else(|| "No early return found")?; + + // 3. Check for step: i = i + 1 + let mut step_lit_opt = None; + + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if let ASTNode::Variable { name: target_name, .. } = target.as_ref() { + if target_name == &loop_var { + if let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } = value.as_ref() + { + if let ASTNode::Variable { name: left_name, .. } = left.as_ref() { + if left_name == &loop_var { + if let ASTNode::Literal { + value: LiteralValue::Integer(lit), + .. + } = right.as_ref() + { + step_lit_opt = Some(*lit); + } + } + } + } + } + } + } + } + + let step_lit = step_lit_opt.ok_or_else(|| "No step pattern found")?; + + // P0: step must be 1 + if step_lit != 1 { + return Ok(None); + } + + // 4. P0: not-found return must be -1 (hardcoded for now) + let not_found_return_lit = -1; + + Ok(Some(ScanParts { + loop_var, + haystack, + needle, + step_lit, + early_return_expr, + not_found_return_lit, + })) +} + +/// Phase 254 P0: Lowering function for Pattern 6 +/// +/// Delegates to MirBuilder implementation. +pub(crate) fn lower( + builder: &mut MirBuilder, + ctx: &super::router::LoopPatternContext, +) -> Result, String> { + builder.cf_loop_pattern6_scan_with_init_impl( + ctx.condition, + ctx.body, + ctx.func_name, + ctx.debug, + ctx.fn_body, + ) +} + +impl MirBuilder { + /// Phase 254 P1: Pattern 6 (ScanWithInit) implementation + /// + /// Lowers index_of-style loops to JoinIR using scan_with_init_minimal lowerer. + /// + /// # Arguments + /// + /// * `condition` - Loop condition AST + /// * `body` - Loop body statements + /// * `func_name` - Function name for debugging + /// * `debug` - Enable debug output + /// * `fn_body` - Optional function body for capture analysis + pub(crate) fn cf_loop_pattern6_scan_with_init_impl( + &mut self, + condition: &ASTNode, + body: &[ASTNode], + func_name: &str, + debug: bool, + fn_body: Option<&[ASTNode]>, + ) -> Result, String> { + use crate::mir::join_ir::lowering::join_value_space::{JoinValueSpace, PARAM_MIN}; + use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + + let trace = trace::trace(); + + if debug { + trace.debug( + "pattern6/lower", + &format!("Phase 254 P1: ScanWithInit lowering for {}", func_name), + ); + } + + // Step 1: Extract pattern parts + let parts = extract_scan_with_init_parts(condition, body, fn_body)? + .ok_or_else(|| format!("[pattern6] Not a scan-with-init pattern in {}", func_name))?; + + if debug { + trace.debug( + "pattern6/lower", + &format!( + "Extracted: loop_var={}, haystack={}, needle={}, step={}, not_found={}", + parts.loop_var, parts.haystack, parts.needle, parts.step_lit, parts.not_found_return_lit + ), + ); + } + + // Step 2: Get host ValueIds for variables + let s_host = self + .variable_ctx + .variable_map + .get(&parts.haystack) + .copied() + .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.haystack))?; + + let ch_host = self + .variable_ctx + .variable_map + .get(&parts.needle) + .copied() + .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.needle))?; + + let i_host = self + .variable_ctx + .variable_map + .get(&parts.loop_var) + .copied() + .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.loop_var))?; + + if debug { + trace.debug( + "pattern6/lower", + &format!( + "Host ValueIds: s={:?}, ch={:?}, i={:?}", + s_host, ch_host, i_host + ), + ); + } + + // Step 3: Create JoinModule + let mut join_value_space = JoinValueSpace::new(); + let join_module = lower_scan_with_init_minimal(&mut join_value_space); + + // Phase 255 P0: Build CarrierInfo for multi-param loop + // Step 1: Create CarrierInfo with 3 variables (s, ch, i) + use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar, CarrierRole}; + + let carriers = vec![ + // s: haystack (ConditionOnly - header PHI only, no exit PHI) + CarrierVar::with_role( + parts.haystack.clone(), + s_host, + CarrierRole::ConditionOnly, + ), + // ch: needle (ConditionOnly - header PHI only, no exit PHI) + CarrierVar::with_role( + parts.needle.clone(), + ch_host, + CarrierRole::ConditionOnly, + ), + ]; + + let carrier_info = CarrierInfo::with_carriers( + parts.loop_var.clone(), // loop_var_name: "i" + i_host, // loop_var_id (LoopState - header PHI + exit PHI) + carriers, // s, ch only (i is handled as loop_var) + ); + + if debug { + trace.debug( + "pattern6/lower", + &format!( + "Phase 255 P0: CarrierInfo with {} carriers (s, ch: ConditionOnly, i: LoopState)", + carrier_info.carrier_count() + ), + ); + } + + // Step 2: Generate join_inputs and host_inputs dynamically + // CRITICAL: Order must match JoinModule main() params: [i, ch, s] (alphabetical) + // Phase 255 P0: CarrierInfo sorts carriers alphabetically, so params must match + // main() is defined with params: vec![i_main_param, ch_main_param, s_main_param] + let mut host_inputs = vec![i_host, ch_host, s_host]; // [i, ch, s] alphabetical + let mut join_inputs = vec![ + ValueId(PARAM_MIN as u32), // i at 100 + ValueId(PARAM_MIN as u32 + 1), // ch at 101 (alphabetically first carrier) + ValueId(PARAM_MIN as u32 + 2), // s at 102 (alphabetically second carrier) + ]; + + // Step 3: Build exit_bindings manually + // CRITICAL: ALL carriers (ConditionOnly + LoopState) must be in exit_bindings + // for latch incoming collection, even though ConditionOnly don't participate in exit PHI + use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; + + let k_exit_func = join_module.require_function("k_exit", "Pattern 6"); + let join_exit_value_i = k_exit_func + .params + .first() + .copied() + .expect("k_exit must have parameter for exit value"); + + let i_exit_binding = LoopExitBinding { + carrier_name: parts.loop_var.clone(), + join_exit_value: join_exit_value_i, + host_slot: i_host, + role: CarrierRole::LoopState, + }; + + // Phase 255 P0: Add ConditionOnly carriers in alphabetical order [ch, s] + // They don't participate in exit PHI, but need latch incoming collection + let ch_exit_binding = LoopExitBinding { + carrier_name: parts.needle.clone(), + join_exit_value: ValueId(0), // Placeholder - not used for ConditionOnly + host_slot: ch_host, + role: CarrierRole::ConditionOnly, + }; + + let s_exit_binding = LoopExitBinding { + carrier_name: parts.haystack.clone(), + join_exit_value: ValueId(0), // Placeholder - not used for ConditionOnly + host_slot: s_host, + role: CarrierRole::ConditionOnly, + }; + + // CRITICAL: Order must match tail call args order: [i, ch, s] (alphabetical) + // Phase 255 P0: loop_step tail call: args = vec![i_plus_1, ch_step_param, s_step_param] + // The loop variable i is first (args[0]), then carriers in alphabetical order (args[1], args[2]) + let exit_bindings = vec![i_exit_binding, ch_exit_binding, s_exit_binding]; + + if debug { + trace.debug( + "pattern6/lower", + &format!("Phase 255 P0: Generated {} exit_bindings", exit_bindings.len()), + ); + } + + // Step 4: Build boundary with carrier_info + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(join_inputs, host_inputs) + .with_exit_bindings(exit_bindings) + .with_loop_var_name(Some(parts.loop_var.clone())) + .with_carrier_info(carrier_info.clone()) // ✅ Key: carrier_info for multi-PHI + .build(); + + // Step 5: Build PostLoopEarlyReturnPlan for exit PHI usage (Phase 255 P1) + // This forces the exit PHI value to be used, preventing DCE from eliminating it + use crate::mir::builder::control_flow::joinir::patterns::policies::post_loop_early_return_plan::PostLoopEarlyReturnPlan; + use crate::ast::Span; + + let post_loop_plan = PostLoopEarlyReturnPlan { + cond: ASTNode::BinaryOp { + operator: BinaryOperator::NotEqual, + left: Box::new(var(&parts.loop_var)), // i + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(parts.not_found_return_lit), // -1 + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ret_expr: var(&parts.loop_var), // return i + }; + + if debug { + trace.debug( + "pattern6/lower", + "Phase 255 P1: Built PostLoopEarlyReturnPlan (cond: i != -1, ret: i)", + ); + } + + // Step 6: Execute JoinIRConversionPipeline + use super::conversion_pipeline::JoinIRConversionPipeline; + let _ = JoinIRConversionPipeline::execute( + self, + join_module, + Some(&boundary), + "pattern6", + debug, + )?; + + // Step 6.5: Emit post-loop early return guard (Phase 255 P1) + // This prevents exit PHI from being DCE'd by using the value + use super::pattern2_steps::post_loop_early_return_step_box::PostLoopEarlyReturnStepBox; + PostLoopEarlyReturnStepBox::maybe_emit(self, Some(&post_loop_plan))?; + + if debug { + trace.debug( + "pattern6/lower", + "Phase 255 P1: Emitted post-loop early return guard (if i != -1 { return i })", + ); + } + + // Note: The post-loop guard ensures exit PHI is used: + // - k_exit with i (found case) + // - k_exit with -1 (not found case) + // The original "return -1" statement after the loop is unreachable + // and will be optimized away by DCE. + + // Step 7: Return Void (loops don't produce values) + let void_val = crate::mir::builder::emission::constant::emit_void(self); + + if debug { + trace.debug( + "pattern6/lower", + &format!("Pattern 6 complete, returning Void {:?}", void_val), + ); + } + + Ok(Some(void_val)) + } +} + +/// Phase 255 P1: Helper function to create Variable ASTNode +fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: crate::ast::Span::unknown(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, BinaryOperator, Literal, Span}; + + #[test] + fn test_contains_methodcall_positive() { + // s.substring(i, i+1) == ch + let method_call = ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: Span::unknown(), + }), + method: "substring".to_string(), + arguments: vec![], + span: Span::unknown(), + }; + + let binary = ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(method_call), + right: Box::new(ASTNode::Variable { + name: "ch".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert!(contains_methodcall(&binary)); + } + + #[test] + fn test_contains_methodcall_negative() { + // i < len + let binary = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "len".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert!(!contains_methodcall(&binary)); + } + + #[test] + fn test_is_const_step_pattern_positive() { + // i + 1 + let value = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: Literal::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert!(is_const_step_pattern(&value)); + } + + #[test] + fn test_is_const_step_pattern_negative() { + // i - 1 + let value = ASTNode::BinaryOp { + operator: BinaryOperator::Subtract, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: Literal::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert!(!is_const_step_pattern(&value)); + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/router.rs b/src/mir/builder/control_flow/joinir/patterns/router.rs index fbd9929f..6c37b8dc 100644 --- a/src/mir/builder/control_flow/joinir/patterns/router.rs +++ b/src/mir/builder/control_flow/joinir/patterns/router.rs @@ -193,6 +193,11 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[ detect: super::pattern4_with_continue::can_lower, lower: super::pattern4_with_continue::lower, }, + LoopPatternEntry { + name: "Pattern6_ScanWithInit", // Phase 254 P0: index_of/find/contains pattern (before P3) + detect: super::pattern6_scan_with_init::can_lower, + lower: super::pattern6_scan_with_init::lower, + }, LoopPatternEntry { name: "Pattern3_WithIfPhi", detect: super::pattern3_with_if_phi::can_lower, diff --git a/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs b/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs index 0600d386..697337b1 100644 --- a/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs +++ b/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs @@ -92,6 +92,7 @@ impl BodyLocalDerivedEmitter { alloc_value, env, Some(body_local_env), + None, // Phase 252: No static box context )?; instructions.extend(escape_cond_insts); @@ -130,7 +131,7 @@ impl BodyLocalDerivedEmitter { let mut env_pre = env.clone(); env_pre.insert(recipe.loop_counter_name.clone(), counter_pre); let (bounds_ok, bounds_insts) = - lower_condition_to_joinir(bounds_ast, alloc_value, &env_pre, Some(body_local_env))?; + lower_condition_to_joinir(bounds_ast, alloc_value, &env_pre, Some(body_local_env), None)?; // Phase 252: No static box context instructions.extend(bounds_insts); let guard = alloc_value(); diff --git a/src/mir/join_ir/lowering/common/conditional_step_emitter.rs b/src/mir/join_ir/lowering/common/conditional_step_emitter.rs index de4ad149..31343094 100644 --- a/src/mir/join_ir/lowering/common/conditional_step_emitter.rs +++ b/src/mir/join_ir/lowering/common/conditional_step_emitter.rs @@ -81,7 +81,7 @@ pub fn emit_conditional_step_update( } // Phase 92 P2-2: Lower the condition expression with body-local support - let (cond_id, cond_insts) = lower_condition_to_joinir(cond_ast, alloc_value, env, body_local_env).map_err(|e| { + let (cond_id, cond_insts) = lower_condition_to_joinir(cond_ast, alloc_value, env, body_local_env, None).map_err(|e| { format!( "ConditionalStep invariant violated: condition must be pure expression for carrier '{}': {}", carrier_name, e diff --git a/src/mir/join_ir/lowering/condition_var_extractor.rs b/src/mir/join_ir/lowering/condition_var_extractor.rs index 1dd8c4d1..1be3e95e 100644 --- a/src/mir/join_ir/lowering/condition_var_extractor.rs +++ b/src/mir/join_ir/lowering/condition_var_extractor.rs @@ -70,6 +70,33 @@ fn collect_variables_recursive(ast: &ASTNode, vars: &mut BTreeSet) { ASTNode::Literal { .. } => { // Literals have no variables } + // Phase 251 Fix: Handle complex condition expressions + ASTNode::MethodCall { object, arguments, .. } => { + // Recurse into object (e.g., 'arr' in 'arr.length()') + collect_variables_recursive(object, vars); + // Recurse into arguments (e.g., 'i' in 'arr.get(i)') + for arg in arguments { + collect_variables_recursive(arg, vars); + } + } + ASTNode::FieldAccess { object, .. } => { + // Recurse into object (e.g., 'obj' in 'obj.field') + collect_variables_recursive(object, vars); + } + ASTNode::Index { target, index, .. } => { + // Recurse into target (e.g., 'arr' in 'arr[i]') + collect_variables_recursive(target, vars); + // Recurse into index (e.g., 'i' in 'arr[i]') + collect_variables_recursive(index, vars); + } + ASTNode::Call { callee, arguments, .. } => { + // Recurse into callee (e.g., function references) + collect_variables_recursive(callee, vars); + // Recurse into arguments + for arg in arguments { + collect_variables_recursive(arg, vars); + } + } _ => { // Other AST nodes not expected in conditions } @@ -192,4 +219,108 @@ mod tests { let vars = extract_condition_variables(&ast, &[]); assert_eq!(vars, vec!["x", "y", "z"]); // 'x' deduplicated } + + #[test] + fn test_extract_method_call() { + // AST: i < arr.length() + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "arr".to_string(), + span: Span::unknown(), + }), + method: "length".to_string(), + arguments: vec![], + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let vars = extract_condition_variables(&ast, &["i".to_string()]); + assert_eq!(vars, vec!["arr"]); // Should extract 'arr' + } + + #[test] + fn test_extract_field_access() { + // AST: i < obj.count + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::FieldAccess { + object: Box::new(ASTNode::Variable { + name: "obj".to_string(), + span: Span::unknown(), + }), + field: "count".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let vars = extract_condition_variables(&ast, &["i".to_string()]); + assert_eq!(vars, vec!["obj"]); + } + + #[test] + fn test_extract_index() { + // AST: i < arr[j] + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Index { + target: Box::new(ASTNode::Variable { + name: "arr".to_string(), + span: Span::unknown(), + }), + index: Box::new(ASTNode::Variable { + name: "j".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let vars = extract_condition_variables(&ast, &["i".to_string()]); + assert_eq!(vars, vec!["arr", "j"]); // Both 'arr' and 'j' (sorted) + } + + #[test] + fn test_extract_complex_method_call_with_args() { + // AST: i < arr.get(j) + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "arr".to_string(), + span: Span::unknown(), + }), + method: "get".to_string(), + arguments: vec![ASTNode::Variable { + name: "j".to_string(), + span: Span::unknown(), + }], + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let vars = extract_condition_variables(&ast, &["i".to_string()]); + assert_eq!(vars, vec!["arr", "j"]); // Both 'arr' and 'j' (sorted) + } } diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index dfac990b..1688df08 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -114,6 +114,8 @@ pub(crate) struct LoopWithBreakLoweringInputs<'a> { pub body_local_derived_recipe: Option<&'a BodyLocalDerivedRecipe>, /// Phase 107: Balanced depth-scan recipe (find_balanced_* family). pub balanced_depth_scan_recipe: Option<&'a BalancedDepthScanRecipe>, + /// Phase 252: Name of the static box being lowered (for this.method(...) in break conditions) + pub current_static_box_name: Option, } /// Lower Pattern 2 (Loop with Conditional Break) to JoinIR @@ -193,6 +195,7 @@ pub(crate) fn lower_loop_with_break_minimal( condition_only_recipe, body_local_derived_recipe, balanced_depth_scan_recipe, + current_static_box_name, // Phase 252 } = inputs; let mut body_local_env = body_local_env; @@ -325,7 +328,7 @@ pub(crate) fn lower_loop_with_break_minimal( ) }); - // Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition + // Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244 / Phase 252: Lower condition let (cond_value, mut cond_instructions) = lower_header_condition( condition, env, @@ -333,6 +336,7 @@ pub(crate) fn lower_loop_with_break_minimal( loop_var_name, i_param, &mut alloc_value, + current_static_box_name.as_deref(), // Phase 252 )?; // After condition lowering, allocate remaining ValueIds @@ -510,7 +514,7 @@ pub(crate) fn lower_loop_with_break_minimal( } // ------------------------------------------------------------------ - // Phase 170-B / Phase 244 / Phase 92 P2-2: Lower break condition + // Phase 170-B / Phase 244 / Phase 92 P2-2 / Phase 252: Lower break condition // ------------------------------------------------------------------ // Phase 92 P2-2: Moved after body-local init to support body-local variable references let (break_cond_value, break_cond_instructions) = lower_break_condition( @@ -521,6 +525,7 @@ pub(crate) fn lower_loop_with_break_minimal( i_param, &mut alloc_value, body_local_env.as_ref().map(|e| &**e), // Phase 92 P2-2: Pass body_local_env + current_static_box_name.as_deref(), // Phase 252 )?; // ------------------------------------------------------------------ diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs index ed8fa49a..db68ec29 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs @@ -28,6 +28,10 @@ fn make_scope_manager<'a>( } /// Lower the header condition. +/// +/// # Phase 252: current_static_box_name Parameter +/// +/// Added to support `this.method(...)` in header conditions for static boxes. pub(crate) fn lower_header_condition( condition: &ASTNode, env: &ConditionEnv, @@ -35,6 +39,7 @@ pub(crate) fn lower_header_condition( loop_var_name: &str, loop_var_id: ValueId, alloc_value: &mut dyn FnMut() -> ValueId, + current_static_box_name: Option<&str>, // Phase 252 ) -> Result<(ValueId, Vec), String> { use crate::mir::join_ir::lowering::condition_lowering_box::ConditionLoweringBox; @@ -59,6 +64,7 @@ pub(crate) fn lower_header_condition( loop_var_id, scope: &scope_manager, alloc_value, + current_static_box_name: current_static_box_name.map(|s| s.to_string()), // Phase 252 }; match expr_lowerer.lower_condition(condition, &mut context) { @@ -94,6 +100,10 @@ pub(crate) fn lower_header_condition( /// /// Added `body_local_env` parameter to support break conditions that reference /// body-local variables (e.g., `ch == '"'` in escape patterns). +/// +/// # Phase 252: current_static_box_name Parameter +/// +/// Added to support `this.method(...)` in break conditions for static boxes. pub(crate) fn lower_break_condition( break_condition: &ASTNode, env: &ConditionEnv, @@ -102,6 +112,7 @@ pub(crate) fn lower_break_condition( loop_var_id: ValueId, alloc_value: &mut dyn FnMut() -> ValueId, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 + current_static_box_name: Option<&str>, // Phase 252 ) -> Result<(ValueId, Vec), String> { use crate::mir::join_ir::lowering::condition_lowering_box::ConditionLoweringBox; @@ -126,6 +137,7 @@ pub(crate) fn lower_break_condition( loop_var_id, scope: &scope_manager, alloc_value, + current_static_box_name: current_static_box_name.map(|s| s.to_string()), // Phase 252 }; let value_id = expr_lowerer diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs index 0acc79f8..e39ad783 100644 --- a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs @@ -286,6 +286,7 @@ pub(crate) fn lower_loop_with_continue_minimal( loop_var_id: i_param, scope: &scope_manager, alloc_value: &mut alloc_value, + current_static_box_name: None, // Phase 252: TODO - plumb through Pattern 3 }; match expr_lowerer.lower_condition(condition, &mut context) { diff --git a/src/mir/join_ir/lowering/method_call_lowerer.rs b/src/mir/join_ir/lowering/method_call_lowerer.rs index 32b09aeb..4a49decb 100644 --- a/src/mir/join_ir/lowering/method_call_lowerer.rs +++ b/src/mir/join_ir/lowering/method_call_lowerer.rs @@ -88,6 +88,7 @@ impl<'a> CascadingArgResolver<'a> { alloc_value, self.cond_env, None, // body-local not used for generic expressions + None, // Phase 252: No static box context for argument lowering instructions, ), } @@ -182,6 +183,7 @@ impl MethodCallLowerer { alloc_value, env, None, // Phase 92 P2-2: No body-local for method call args + None, // Phase 252: No static box context for method call args instructions, )?; lowered_args.push(arg_val); diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index 114d07f8..7d90283e 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -65,6 +65,7 @@ pub(crate) mod loop_view_builder; // Phase 33-23: Loop lowering dispatch pub mod loop_with_break_minimal; // Phase 188-Impl-2: Pattern 2 minimal lowerer pub mod loop_with_continue_minimal; pub mod method_call_lowerer; // Phase 224-B: MethodCall lowering (metadata-driven) +pub mod user_method_policy; // Phase 252: User-defined method policy (SSOT for static box method whitelists) pub mod method_return_hint; // Phase 83: P3-D 既知メソッド戻り値型推論箱 pub mod scope_manager; // Phase 231: Unified variable scope management // Phase 195: Pattern 4 minimal lowerer #[cfg(feature = "normalized_dev")] @@ -74,6 +75,7 @@ pub(crate) mod step_schedule; // Phase 47-A: Generic step scheduler for P2/P3 (r pub mod loop_with_if_phi_if_sum; // Phase 213: Pattern 3 AST-based if-sum lowerer (Phase 242-EX-A: supports complex conditions) pub mod min_loop; pub mod simple_while_minimal; // Phase 188-Impl-1: Pattern 1 minimal lowerer +pub mod scan_with_init_minimal; // Phase 254 P1: Pattern 6 minimal lowerer (index_of/find/contains) pub mod skip_ws; pub mod stage1_using_resolver; pub mod stageb_body; diff --git a/src/mir/join_ir/lowering/scan_with_init_minimal.rs b/src/mir/join_ir/lowering/scan_with_init_minimal.rs new file mode 100644 index 00000000..236bcb6f --- /dev/null +++ b/src/mir/join_ir/lowering/scan_with_init_minimal.rs @@ -0,0 +1,357 @@ +//! Phase 254 P1: Pattern 6 (ScanWithInit) Minimal Lowerer +//! +//! Target: apps/tests/phase254_p0_index_of_min.hako +//! +//! Code: +//! ```nyash +//! static box StringUtils { +//! 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 +//! } +//! } +//! ``` +//! +//! Expected JoinIR: +//! ```text +//! fn main(s, ch, i): +//! result = loop_step(s, ch, i) +//! // Post-loop early return will be inserted by MirBuilder +//! +//! fn loop_step(s, ch, i): +//! // 1. Check exit condition: i >= s.length() +//! len = StringBox.length(s) +//! exit_cond = (i >= len) +//! Jump(k_exit, [-1], cond=exit_cond) // Not found case +//! +//! // 2. Calculate i_plus_1 for substring +//! i_plus_1 = i + 1 +//! +//! // 3. Hoist MethodCall(substring) to init-time BoxCall +//! cur = StringBox.substring(s, i, i_plus_1) +//! +//! // 4. Check match condition +//! match = (cur == ch) +//! Jump(k_exit, [i], cond=match) // Found case +//! +//! // 5. Tail recursion +//! Call(loop_step, [s, ch, i_plus_1]) +//! +//! fn k_exit(i_exit): +//! return i_exit +//! ``` +//! +//! ## Design Notes +//! +//! This is a MINIMAL P0 implementation targeting index_of pattern specifically. +//! Key features: +//! - substring is emitted as BoxCall (init-time, not condition whitelist) +//! - Two Jump instructions to k_exit (not found: -1, found: i) +//! - Step must be 1 (P0 restriction) +//! - not_found_return_lit must be -1 (P0 restriction) + +use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; +use crate::mir::join_ir::{ + BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, +}; + +/// Lower Pattern 6 (ScanWithInit) to JoinIR +/// +/// # Phase 254 P1: Pure JoinIR Fragment Generation +/// +/// This version generates JoinIR using **JoinValueSpace** for unified ValueId allocation. +/// It uses the Param region (100+) for function parameters and Local region (1000+) for +/// temporary values. +/// +/// ## Design Philosophy +/// +/// Following Pattern 1's architecture: +/// - **Pure transformer**: No side effects, only JoinIR generation +/// - **Reusable**: Works in any context with proper boundary +/// - **Testable**: Can test JoinIR independently +/// +/// ## Boundary Contract +/// +/// This function returns a JoinModule with: +/// - **Input slots**: main() params for (s, ch, i) +/// - **Caller responsibility**: Create JoinInlineBoundary to map params to host variables +/// - **Exit binding**: k_exit param receives found index or -1 +/// +/// # Arguments +/// +/// * `join_value_space` - Unified ValueId allocator (Phase 202-A) +/// +/// # Returns +/// +/// * `JoinModule` - Successfully lowered to JoinIR +pub(crate) fn lower_scan_with_init_minimal( + join_value_space: &mut JoinValueSpace, +) -> JoinModule { + let mut join_module = JoinModule::new(); + + // ================================================================== + // Function IDs allocation + // ================================================================== + let main_id = JoinFuncId::new(0); + let loop_step_id = JoinFuncId::new(1); + let k_exit_id = JoinFuncId::new(2); + + // ================================================================== + // ValueId allocation + // ================================================================== + // main() params/locals + // Phase 255 P0: Loop variable MUST be first, then alphabetical order [ch, s] + // (CarrierInfo sorts carriers alphabetically for determinism) + let i_main_param = join_value_space.alloc_param(); // loop index + let ch_main_param = join_value_space.alloc_param(); // needle character (alphabetically first) + let s_main_param = join_value_space.alloc_param(); // haystack string (alphabetically second) + let loop_result = join_value_space.alloc_local(); // result from loop_step + + // loop_step params/locals + // Phase 255 P0: Loop variable MUST be first, then alphabetical order [ch, s] + let i_step_param = join_value_space.alloc_param(); // loop index + let ch_step_param = join_value_space.alloc_param(); // needle (alphabetically first) + let s_step_param = join_value_space.alloc_param(); // haystack (alphabetically second) + let len = join_value_space.alloc_local(); // s.length() + let exit_cond = join_value_space.alloc_local(); // i >= len + let const_minus_1 = join_value_space.alloc_local(); // -1 for not found + let const_1 = join_value_space.alloc_local(); // 1 for increment + let i_plus_1 = join_value_space.alloc_local(); // i + 1 + let cur = join_value_space.alloc_local(); // substring result + let match_cond = join_value_space.alloc_local(); // cur == ch + + // k_exit params + let i_exit_param = join_value_space.alloc_param(); // exit parameter (index or -1) + + // ================================================================== + // main() function + // ================================================================== + let mut main_func = JoinFunction::new( + main_id, + "main".to_string(), + vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical + ); + + // result = loop_step(i, ch, s) // Phase 255 P0: alphabetical order + main_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical + k_next: None, + dst: Some(loop_result), + }); + + // Return loop_result (found index or -1) + main_func.body.push(JoinInst::Ret { value: Some(loop_result) }); + + join_module.add_function(main_func); + + // ================================================================== + // loop_step(i, ch, s) function + // ================================================================== + // Phase 255 P0: Loop variable first, then alphabetical [ch, s] + let mut loop_step_func = JoinFunction::new( + loop_step_id, + "loop_step".to_string(), + vec![i_step_param, ch_step_param, s_step_param], // Phase 255 P0: [i, ch, s] alphabetical + ); + + // 1. len = s.length() + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(len), + box_name: "StringBox".to_string(), + method: "length".to_string(), + args: vec![s_step_param], + })); + + // 2. exit_cond = (i >= len) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Compare { + dst: exit_cond, + op: CompareOp::Ge, + lhs: i_step_param, + rhs: len, + })); + + // 3. const -1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_minus_1, + value: ConstValue::Integer(-1), + })); + + // 4. Jump(k_exit, [-1], cond=exit_cond) - not found case + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![const_minus_1], + cond: Some(exit_cond), + }); + + // 5. i_plus_1 = i + 1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); + + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_plus_1, + op: BinOpKind::Add, + lhs: i_step_param, + rhs: const_1, + })); + + // 6. cur = s.substring(i, i_plus_1) - init-time BoxCall + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(cur), + box_name: "StringBox".to_string(), + method: "substring".to_string(), + args: vec![s_step_param, i_step_param, i_plus_1], + })); + + // 7. match_cond = (cur == ch) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Compare { + dst: match_cond, + op: CompareOp::Eq, + lhs: cur, + rhs: ch_step_param, + })); + + // 8. Jump(k_exit, [i], cond=match_cond) - found case + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![i_step_param], + cond: Some(match_cond), + }); + + // 9. Call(loop_step, [i_plus_1, ch, s]) - tail recursion + // Phase 255 P0: Loop variable first, then alphabetical [ch, s] + loop_step_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_plus_1, ch_step_param, s_step_param], // Phase 255 P0: [i_plus_1, ch, s] alphabetical + k_next: None, // CRITICAL: None for tail call + dst: None, + }); + + join_module.add_function(loop_step_func); + + // ================================================================== + // k_exit(i_exit) function + // ================================================================== + let mut k_exit_func = JoinFunction::new(k_exit_id, "k_exit".to_string(), vec![i_exit_param]); + + // Return i_exit (found index or -1) + k_exit_func.body.push(JoinInst::Ret { + value: Some(i_exit_param), + }); + + join_module.add_function(k_exit_func); + + // Set entry point + join_module.entry = Some(main_id); + + eprintln!("[joinir/pattern6] Generated JoinIR for ScanWithInit Pattern"); + eprintln!("[joinir/pattern6] Functions: main, loop_step, k_exit"); + eprintln!("[joinir/pattern6] BoxCall: substring (init-time, not condition whitelist)"); + + join_module +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lower_scan_with_init_minimal() { + let mut join_value_space = JoinValueSpace::new(); + + let join_module = lower_scan_with_init_minimal(&mut join_value_space); + + // main + loop_step + k_exit の3関数 + assert_eq!(join_module.functions.len(), 3); + + // Entry が main(0) に設定されている + assert_eq!(join_module.entry, Some(JoinFuncId::new(0))); + + // k_exit 関数が取れる + let k_exit_func = join_module + .functions + .get(&JoinFuncId::new(2)) + .expect("k_exit function should exist"); + assert_eq!(k_exit_func.name, "k_exit"); + } + + #[test] + fn test_loop_step_has_substring_box_call() { + let mut join_value_space = JoinValueSpace::new(); + + let join_module = lower_scan_with_init_minimal(&mut join_value_space); + + // loop_step 関数を取得 + let loop_step = join_module + .functions + .get(&JoinFuncId::new(1)) + .expect("loop_step function should exist"); + + // BoxCall(substring) が含まれることを確認 + let has_substring = loop_step.body.iter().any(|inst| { + matches!( + inst, + JoinInst::Compute(MirLikeInst::BoxCall { method, .. }) + if method == "substring" + ) + }); + + assert!( + has_substring, + "loop_step should contain substring BoxCall" + ); + } + + #[test] + fn test_loop_step_has_exit_jumps() { + let mut join_value_space = JoinValueSpace::new(); + + let join_module = lower_scan_with_init_minimal(&mut join_value_space); + + // loop_step 関数を取得 + let loop_step = join_module + .functions + .get(&JoinFuncId::new(1)) + .expect("loop_step function should exist"); + + // Jump(k_exit, ...) が2つ含まれることを確認 + let exit_jump_count = loop_step + .body + .iter() + .filter(|inst| { + matches!( + inst, + JoinInst::Jump { cont, .. } + if *cont == JoinFuncId::new(2).as_cont() + ) + }) + .count(); + + assert_eq!( + exit_jump_count, 2, + "loop_step should have 2 exit jumps" + ); + } +} diff --git a/src/mir/join_ir/lowering/user_method_policy.rs b/src/mir/join_ir/lowering/user_method_policy.rs new file mode 100644 index 00000000..2002368b --- /dev/null +++ b/src/mir/join_ir/lowering/user_method_policy.rs @@ -0,0 +1,304 @@ +//! Phase 252: User-Defined Method Policy Box +//! +//! This box provides a Single Source of Truth (SSOT) for determining whether +//! user-defined static box methods are allowed in JoinIR contexts. +//! +//! ## Design Philosophy +//! +//! **Box-First Design**: UserMethodPolicy is a single-responsibility box that +//! answers one question: "Can this static box method be safely lowered to JoinIR?" +//! +//! **Metadata-Driven**: Uses a policy table to determine allowed methods. +//! NO method name hardcoding in lowering logic - all decisions made here. +//! +//! **Fail-Fast**: If a method is not in the policy table, immediately returns false. +//! No silent fallbacks or guessing. +//! +//! **Future Extension**: This SSOT can be moved to .hako annotations or nyash.toml +//! in the future without breaking lowering logic. +//! +//! ## Supported Static Boxes +//! +//! - **StringUtils**: String utility functions (trim, character checks, etc.) +//! +//! ## Example Usage +//! +//! ```ignore +//! // Check if StringUtils.is_whitespace is allowed in condition +//! if UserMethodPolicy::allowed_in_condition("StringUtils", "is_whitespace") { +//! // Lower this.is_whitespace(...) to JoinIR +//! } +//! ``` + +/// Phase 252: User-Defined Method Policy Box +/// +/// Provides metadata for user-defined static box methods to determine +/// their eligibility for JoinIR lowering in different contexts. +pub struct UserMethodPolicy; + +impl UserMethodPolicy { + /// Check if a user-defined method is allowed in loop condition context + /// + /// # Requirements for Condition Context + /// + /// - Method must be pure (no side effects) + /// - Method should return boolean (for use in conditions) + /// - Method should be deterministic (same inputs → same outputs) + /// + /// # Arguments + /// + /// * `box_name` - Name of the static box (e.g., "StringUtils") + /// * `method_name` - Name of the method (e.g., "is_whitespace") + /// + /// # Returns + /// + /// * `true` - Method is whitelisted for condition context + /// * `false` - Method is not whitelisted or unknown + /// + /// # Example + /// + /// ```ignore + /// // Loop condition: loop(i < n && not this.is_whitespace(ch)) + /// assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_whitespace")); + /// assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "trim_start")); + /// ``` + pub fn allowed_in_condition(box_name: &str, method_name: &str) -> bool { + match box_name { + "StringUtils" => Self::stringutils_allowed_in_condition(method_name), + _ => false, // Unknown static box - fail-fast + } + } + + /// Check if a user-defined method is allowed in LoopBodyLocal init context + /// + /// # Requirements for Init Context + /// + /// - Method must be pure (no side effects) + /// - Method can return any type (strings, integers, etc.) + /// - Method should be deterministic + /// + /// # Arguments + /// + /// * `box_name` - Name of the static box (e.g., "StringUtils") + /// * `method_name` - Name of the method (e.g., "trim_start") + /// + /// # Returns + /// + /// * `true` - Method is whitelisted for init context + /// * `false` - Method is not whitelisted or unknown + /// + /// # Example + /// + /// ```ignore + /// // LoopBodyLocal init: local ch = s.substring(i, i + 1) + /// // (substring is allowed in init but not in condition) + /// assert!(UserMethodPolicy::allowed_in_init("StringUtils", "trim_start")); + /// ``` + pub fn allowed_in_init(box_name: &str, method_name: &str) -> bool { + match box_name { + "StringUtils" => Self::stringutils_allowed_in_init(method_name), + _ => false, // Unknown static box - fail-fast + } + } + + // ======================================================================== + // StringUtils Policy Table + // ======================================================================== + + /// StringUtils methods allowed in condition context + /// + /// All methods here are pure boolean-returning functions suitable for + /// use in loop conditions and conditional expressions. + /// + /// # StringUtils Source + /// + /// See: `apps/lib/json_native/utils/string.hako` + fn stringutils_allowed_in_condition(method_name: &str) -> bool { + matches!( + method_name, + // Character classification (pure boolean functions) + "is_whitespace" // ch == " " or ch == "\t" or ... + | "is_digit" // ch == "0" or ch == "1" or ... + | "is_hex_digit" // is_digit(ch) or ch == "a" or ... + | "is_alpha" // (ch >= "a" and ch <= "z") or ... + | "is_alphanumeric" // is_alpha(ch) or is_digit(ch) + + // String validation (pure boolean functions) + | "is_integer" // Checks if string represents an integer + | "is_empty_or_whitespace" // trim(s).length() == 0 + + // String matching (pure boolean functions) + | "starts_with" // s.substring(0, prefix.length()) == prefix + | "ends_with" // s.substring(s.length() - suffix.length(), ...) == suffix + | "contains" // index_of_string(s, substr) != -1 + ) + } + + /// StringUtils methods allowed in init context + /// + /// All methods here are pure functions but may return non-boolean types + /// (strings, integers). Suitable for LoopBodyLocal initialization. + /// + /// # StringUtils Source + /// + /// See: `apps/lib/json_native/utils/string.hako` + fn stringutils_allowed_in_init(method_name: &str) -> bool { + matches!( + method_name, + // Whitespace handling (pure string functions) + "trim" // s.trim() (VM StringBox method) + | "trim_start" // Remove leading whitespace + | "trim_end" // Remove trailing whitespace + + // String search (pure integer-returning functions) + | "index_of" // First occurrence of character (-1 if not found) + | "last_index_of" // Last occurrence of character (-1 if not found) + | "index_of_string" // First occurrence of substring (-1 if not found) + + // String transformation (pure string functions) + | "to_upper" // Convert string to uppercase + | "to_lower" // Convert string to lowercase + | "char_to_upper" // Convert single character to uppercase + | "char_to_lower" // Convert single character to lowercase + + // String manipulation (pure functions) + | "join" // Join array elements with separator + | "split" // Split string by separator + + // Numeric parsing (pure functions) + | "parse_float" // Parse floating-point number (currently identity) + | "parse_integer" // Parse integer from string + + // Character classification (also allowed in init) + | "is_whitespace" + | "is_digit" + | "is_hex_digit" + | "is_alpha" + | "is_alphanumeric" + | "is_integer" + | "is_empty_or_whitespace" + | "starts_with" + | "ends_with" + | "contains" + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ===== Condition Context Tests ===== + + #[test] + fn test_stringutils_character_classification_in_condition() { + // Pure boolean character classification methods should be allowed + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_whitespace")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_digit")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_hex_digit")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_alpha")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_alphanumeric")); + } + + #[test] + fn test_stringutils_validation_in_condition() { + // Pure boolean validation methods should be allowed + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_integer")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_empty_or_whitespace")); + } + + #[test] + fn test_stringutils_matching_in_condition() { + // Pure boolean matching methods should be allowed + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "starts_with")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "ends_with")); + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "contains")); + } + + #[test] + fn test_stringutils_string_functions_not_in_condition() { + // String-returning functions should NOT be allowed in condition + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "trim")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "trim_start")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "trim_end")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "to_upper")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "to_lower")); + } + + #[test] + fn test_stringutils_search_not_in_condition() { + // Integer-returning search functions should NOT be allowed in condition + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "index_of")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "last_index_of")); + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "index_of_string")); + } + + #[test] + fn test_unknown_static_box_in_condition() { + // Unknown static boxes should fail-fast + assert!(!UserMethodPolicy::allowed_in_condition("UnknownBox", "some_method")); + assert!(!UserMethodPolicy::allowed_in_condition("MathUtils", "abs")); + } + + // ===== Init Context Tests ===== + + #[test] + fn test_stringutils_all_pure_methods_in_init() { + // All pure methods should be allowed in init (more permissive than condition) + // Character classification + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "is_whitespace")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "is_digit")); + + // String manipulation + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "trim")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "trim_start")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "trim_end")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "to_upper")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "to_lower")); + + // String search + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "index_of")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "last_index_of")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "index_of_string")); + + // Numeric parsing + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "parse_integer")); + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "parse_float")); + } + + #[test] + fn test_unknown_static_box_in_init() { + // Unknown static boxes should fail-fast + assert!(!UserMethodPolicy::allowed_in_init("UnknownBox", "some_method")); + assert!(!UserMethodPolicy::allowed_in_init("MathUtils", "sqrt")); + } + + // ===== Real-World Pattern Tests ===== + + #[test] + fn test_trim_end_pattern() { + // Phase 252 P0: StringUtils.trim_end/1 pattern + // loop(i >= 0) { if not this.is_whitespace(s.substring(i, i + 1)) { break } ... } + + // is_whitespace should be allowed in condition (boolean check) + assert!(UserMethodPolicy::allowed_in_condition("StringUtils", "is_whitespace")); + + // trim_end itself should NOT be allowed in condition (string function) + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "trim_end")); + + // But trim_end should be allowed in init + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "trim_end")); + } + + #[test] + fn test_index_of_pattern() { + // Pattern: local pos = this.index_of(s, ch) + // index_of returns integer (-1 or index), not boolean + + // Should NOT be allowed in condition + assert!(!UserMethodPolicy::allowed_in_condition("StringUtils", "index_of")); + + // But should be allowed in init + assert!(UserMethodPolicy::allowed_in_init("StringUtils", "index_of")); + } +} diff --git a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_llvm_exe.sh new file mode 100644 index 00000000..b4c76330 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_llvm_exe.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 254 P0: index_of 形 loop (LLVM backend) +set -euo pipefail + +HAKO_PATH="apps/tests/phase254_p0_index_of_min.hako" + +# Test: "abc".index_of("b") → 1 +EXPECTED_EXIT=1 + +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm "$HAKO_PATH" +actual_exit=$? + +if [[ $actual_exit -eq $EXPECTED_EXIT ]]; then + echo "✅ phase254_p0_index_of_llvm_exe: PASS (exit=$actual_exit)" + exit 0 +else + echo "❌ phase254_p0_index_of_llvm_exe: FAIL (expected=$EXPECTED_EXIT, got=$actual_exit)" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh new file mode 100644 index 00000000..f7a543e0 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 254 P0: index_of 形 loop (VM backend) +set -euo pipefail + +HAKO_PATH="apps/tests/phase254_p0_index_of_min.hako" + +# Test: "abc".index_of("b") → 1 +EXPECTED_EXIT=1 + +$HAKORUNE_BIN --backend vm "$HAKO_PATH" +actual_exit=$? + +if [[ $actual_exit -eq $EXPECTED_EXIT ]]; then + echo "✅ phase254_p0_index_of_vm: PASS (exit=$actual_exit)" + exit 0 +else + echo "❌ phase254_p0_index_of_vm: FAIL (expected=$EXPECTED_EXIT, got=$actual_exit)" + exit 1 +fi