From 077657b7a1ab3e8c04b1f718bdf66d1c621c112a Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sat, 20 Dec 2025 06:38:21 +0900 Subject: [PATCH] feat(joinir): Phase 256 P1.5-P1.7 contracts and naming SSOT --- docs/development/current/main/10-Now.md | 9 +- .../main/investigations/legacy-candidates.md | 150 +++++++++ .../current/main/phases/phase-256/README.md | 253 +++++++++++++- src/mir/builder.rs | 2 +- .../joinir/merge/contract_checks.rs | 312 ++++++++++++++++++ .../joinir/merge/instruction_rewriter.rs | 12 +- .../builder/control_flow/joinir/merge/mod.rs | 2 +- .../joinir/patterns/conversion_pipeline.rs | 20 ++ .../joinir/patterns/exit_binding.rs | 3 +- .../patterns/exit_binding_applicator.rs | 3 +- .../patterns/pattern6_scan_with_init.rs | 20 +- .../joinir/patterns/pattern7_split_scan.rs | 13 +- .../normalized_shadow/loop_true_break_once.rs | 10 +- src/mir/join_ir/lowering/canonical_names.rs | 100 ++++++ src/mir/join_ir/lowering/carrier_info.rs | 13 +- src/mir/join_ir/lowering/inline_boundary.rs | 29 +- .../lowering/inline_boundary_builder.rs | 105 ++++++ src/mir/join_ir/lowering/mod.rs | 1 + .../lowering/scan_with_init_minimal.rs | 7 +- .../join_ir/lowering/simple_while_minimal.rs | 5 +- .../join_ir/lowering/split_scan_minimal.rs | 14 +- src/tests/mod.rs | 6 + src/tests/phase256_select_minimal_test.rs | 132 ++++++++ src/tests/phase40_array_ext_filter_test.rs | 4 +- 24 files changed, 1162 insertions(+), 63 deletions(-) create mode 100644 docs/development/current/main/investigations/legacy-candidates.md create mode 100644 src/mir/join_ir/lowering/canonical_names.rs create mode 100644 src/tests/phase256_select_minimal_test.rs diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index df61230b..9d9e1dd8 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -48,7 +48,14 @@ ## 2025-12-19:Phase 256(StringUtils.split/2 可変 step ループ)🔜 - Phase 256 README: `docs/development/current/main/phases/phase-256/README.md` -- Current first FAIL: `StringUtils.split/2`(Missing caps: `ConstStep` → freeze) +- Current first FAIL: `StringUtils.split/2`(`jump_args length mismatch` → `Carrier 'i' has no latch incoming set`) +- 状況: + - `MirInstruction::Select` の導入は完了、Pattern6(index_of)は PASS 維持。 + - `ValueId(57)` undefined は根治(原因は `const_1` 未初期化)。 + - SSA undef(`%49/%67`)は P1.7 で根治(continuation 関数名の SSOT 不一致)。 + - 残りは Pattern7 の carrier PHI 配線(ExitLine / jump_args 契約)を直す段階。 + - P1.5-DBG: boundary entry params の契約チェックを追加(VM実行前 fail-fast)。 + - P1.6: 契約チェックの薄い集約 `run_all_pipeline_checks()` を導入(pipeline の責務を縮退)。 ## 2025-12-19:Phase 254(index_of loop pattern)✅ 完了(Blocked by Phase 255) diff --git a/docs/development/current/main/investigations/legacy-candidates.md b/docs/development/current/main/investigations/legacy-candidates.md new file mode 100644 index 00000000..e76c0547 --- /dev/null +++ b/docs/development/current/main/investigations/legacy-candidates.md @@ -0,0 +1,150 @@ +# Legacy 候補一覧 + +Phase 256+ で特定された、将来削除可能な legacy コード候補を記録する。 + +--- + +## Phase 256 P1.7 で特定された legacy + +### `join_func_name()` (src/mir/join_ir_vm_bridge/mod.rs) + +**状態**: 未使用になった可能性あり(要精査) + +**理由**: +- Phase 256 P1.7 で `continuation_func_ids` が `String` ベースに変更 +- `join_func_name()` による `JoinFuncId → String` 変換が不要になった可能性 +- JoinFunction の `name` フィールドが直接利用可能 + +**残利用箇所**: 11箇所 + 定義1箇所 = 計12箇所 + +#### 実利用箇所(10箇所) + +1. **src/mir/join_ir_vm_bridge/runner.rs:55** + ```rust + let entry_name = join_func_name(entry_func); + ``` + - 用途: VM実行時のエントリーポイント関数名取得 + - 状態: 実利用(Phase 27-shortterm S-4.3) + +2. **src/mir/join_ir_vm_bridge/joinir_block_converter.rs:162** + ```rust + let func_name = join_func_name(*func); + ``` + - 用途: JoinIR ブロック変換時の関数名取得 + - 状態: 実利用(Phase 190) + +3. **src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs:94** + ```rust + name: join_func_name(JoinFuncId(func.id.0)), + ``` + - 用途: Normalized → MIR 変換時の関数署名生成 + - 状態: 実利用(Phase 141+) + - 条件: `#[cfg(feature = "normalized_dev")]` + +4. **src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs:182** + ```rust + let func_name = join_func_name(JoinFuncId(target.0)); + ``` + - 用途: Normalized 関数ターゲット名取得 + - 状態: 実利用(Phase 141+) + - 条件: `#[cfg(feature = "normalized_dev")]` + +5. **src/mir/join_ir_vm_bridge/joinir_function_converter.rs:39** + ```rust + .insert(join_func_name(*func_id), mir_func); + ``` + - 用途: MIR モジュールへの関数登録 + - 状態: 実利用(Phase 190) + +6. **src/mir/builder/control_flow/joinir/merge/tail_call_lowering_policy.rs:100** + ```rust + let k_exit_name = join_func_name(JoinFuncId::new(2)); + ``` + - 用途: テストコード内での k_exit 関数名生成 + - 状態: テスト専用(`#[cfg(test)]`) + +7. **src/mir/control_tree/normalized_shadow/loop_true_break_once.rs:63** + ```rust + use crate::mir::join_ir_vm_bridge::join_func_name; + ``` + - 用途: テストコード内での関数名取得 + - 状態: テスト専用(`#[cfg(test)]`) + +#### コメント参照箇所(3箇所) + +8. **src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs** + ```rust + // No need to convert with join_func_name() - use directly + ``` + - 用途: コメント(変換不要の説明) + - 状態: ドキュメント + +9. **src/mir/join_ir_vm_bridge/meta.rs** + ```rust + // Phase 256 P1.7: Use actual function name instead of join_func_name() + // join_func_name() produces "join_func_{id}" but JoinFunction.name contains + ``` + - 用途: コメント(Phase 256 P1.7 の設計変更説明) + - 状態: ドキュメント + +10. **src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs** + ```rust + // The bridge uses JoinFunction.name as the MirModule function key, not join_func_name(id) + ``` + - 用途: コメント(Pattern 7 の設計説明) + - 状態: ドキュメント + +#### 定義箇所(1箇所) + +11. **src/mir/join_ir_vm_bridge/mod.rs** + ```rust + pub(crate) fn join_func_name(id: JoinFuncId) -> String { + ``` + - 用途: 関数定義 + - 状態: pub(crate) で公開 + +--- + +## 推奨アクション + +### 短期(Phase 256 P1.7 スコープ外) +- ✅ このドキュメントに記録完了 +- 実装変更は行わない(記録のみ) + +### 中期(Phase 257+ で検討) +1. **段階的移行案**: + - Step 1: 新規コードでは `JoinFunction.name` を直接使用 + - Step 2: 既存の10実利用箇所を段階的に置換 + - Step 3: テストコード(2箇所)を置換 + - Step 4: コメント更新(3箇所) + - Step 5: `join_func_name()` 削除 + +2. **影響範囲**: + - Bridge 層: 7箇所(実利用6 + テスト1) + - Normalized 層: 2箇所(feature gated) + - Control flow builder: 1箇所(テストのみ) + - Control tree: 1箇所(テストのみ) + +3. **リスク評価**: + - 低: テストコードのみ(2箇所) + - 中: Bridge 層の実利用(7箇所) + - 高: Normalized 層(feature gated, 現在開発中) + +4. **優先度**: 低(現在問題なし、技術的負債削減として) + +--- + +## メンテナンス + +- **作成日**: 2025-12-20 +- **最終更新**: 2025-12-20 +- **関連 Phase**: 256 P1.7 +- **関連コミット**: (Phase 256 P1.7 完了後に記入) + +--- + +## 参考リンク + +- Phase 256 P1.7 設計: [phase-256/README.md](../phases/phase-256/README.md) +- JoinIR 設計マップ: [design/joinir-design-map.md](../design/joinir-design-map.md) +- JoinIR アーキテクチャ: [joinir-architecture-overview.md](../joinir-architecture-overview.md) diff --git a/docs/development/current/main/phases/phase-256/README.md b/docs/development/current/main/phases/phase-256/README.md index e6f4734a..7e1cf030 100644 --- a/docs/development/current/main/phases/phase-256/README.md +++ b/docs/development/current/main/phases/phase-256/README.md @@ -6,13 +6,28 @@ Related: - Phase 255 完了(loop_invariants 導入、Pattern 6 完成) - Phase 254 完了(Pattern 6 index_of 実装) -## 失敗詳細 +## Current Status (SSOT) + +- Current first FAIL: `StringUtils.split/2`(`jump_args length mismatch` → `Carrier 'i' has no latch incoming set`) +- Pattern6 は PASS 維持 +- 直近の完了: + - P1.7: SSA undef(`%49/%67`)根治(continuation 関数名の SSOT 不一致) + - P1.6: pipeline contract checks を `run_all_pipeline_checks()` に集約 +- 次の作業: P1.8(Pattern7 の carrier PHI wiring / ExitLine + jump_args 契約の修正) + +--- + +## Background (P0 Archive) + +このセクションは初期の失敗詳細とP0設計の記録。現状の作業は上記の Current Status を参照。 + +### 失敗詳細 **テスト**: json_lint_vm (quick profile) **エラー**: `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern` **関数**: `StringUtils.split/2` -### エラーメッセージ全体 +#### エラーメッセージ全体 ``` [trace:dev] loop_canonicalizer: Decision: FAIL_FAST @@ -25,15 +40,15 @@ Function: StringUtils.split/2 Hint: This loop pattern is not supported. All loops must use JoinIR lowering. ``` -## 期待される動作 +### 期待される動作 `StringUtils.split(s, separator)` が正常にコンパイルされ、文字列分割が動作すること。 -## 実際の動作 +### 実際の動作 Loop canonicalizer が `ConstStep` を要求しているが、このループはステップが複雑で定数ではない。 -## 最小再現コード +### 最小再現コード ```nyash split(s, separator) { @@ -68,9 +83,9 @@ split(s, separator) { } ``` -## 分析 +### 分析 -### ループ構造 +#### ループ構造 1. **条件**: `i <= s.length() - separator.length()` 2. **ボディ**: @@ -85,7 +100,7 @@ split(s, separator) { - **複数キャリア**: `i`, `start`, `result` を更新 - **MethodCall**: `substring()`, `push()`, `length()` を使用 -### Canonicalizer の問題 +#### Canonicalizer の問題 ``` Missing caps: [ConstStep] @@ -95,9 +110,9 @@ Missing caps: [ConstStep] - このループは条件分岐で異なるステップ幅を使う - Pattern 2 (balanced_depth_scan) に近いが、可変ステップがネック -## 実装計画 +### 実装計画 -### Option A: Pattern 7 - Split/Tokenization Pattern +#### Option A: Pattern 7 - Split/Tokenization Pattern **新しいパターン追加**: - 可変ステップサポート @@ -111,20 +126,20 @@ Missing caps: [ConstStep] - Else: `i = i + 1` (定数ステップ) 3. Accumulator への追加操作 (`push` など) -### Option B: Pattern 2 拡張 +#### Option B: Pattern 2 拡張 **既存 Pattern 2 を拡張**: - ConstStep 要件を緩和 - If-else で異なるステップ幅を許可 - balanced_depth_scan_policy を拡張 -### Option C: Normalization 経路 +#### Option C: Normalization 経路 **ループ正規化で対応**: - 可変ステップを定数ステップに変換 - Carrier 追加で状態管理 -## 次のステップ +### 次のステップ(P0時点の初期計画) 1. **StepTree 詳細解析**: split ループの完全な AST 構造確認 2. **類似パターン調査**: 他の可変ステップループ(indexOf, contains など) @@ -133,7 +148,7 @@ Missing caps: [ConstStep] --- -## Phase 256 指示書(P0) +## Phase 256 指示書(P0 / 完了済み) ### 目標 @@ -184,3 +199,213 @@ Option A(Pattern 7 新設)を推奨。 - Phase 255 で loop_invariants が導入されたが、このケースは invariants 以前の問題(可変ステップ) - Phase 254-256 の流れで Pattern 6 → Pattern 7 の自然な進化が期待される - split/tokenization は一般的なパターンなので、汎用的な解決策が望ましい + +--- + +## 進捗(P0/P1) + +### P0: Pattern 7 基本実装(完了) + +- Fixture & smokes(integration): + - `apps/tests/phase256_p0_split_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh` + - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.sh` +- Pattern 7: + - Detector / Extractor / JoinIR lowerer / MirBuilder 統合まで実装 + - JoinIR lowerer は可変 step を `JoinInst::Select` で表現 + +### P1: Carrier/param 配線の整流(完了) + +- “Carriers-first” の順序を SSOT として固定し、latch incoming 不足を解消 +- exit bindings を明示的に構築(`i`, `start`) + +### P1.5: JoinIR→MIR 変換の根治(進行中) + +背景: +- Pattern 7 は `JoinInst::Select` を使用するが、JoinIR→MIR 変換経路で Select が未対応だったため、 + “dst が定義されない ValueId” が発生し得る。 + +対応(完了): +- boundary の伝播を bridge 経路全体へ追加 +- `MirInstruction::Select` の追加、および JoinIR→MIR 変換 / remap / value collection を実装 + +現状(ブロッカー): +- integration VM が still FAIL: + - `use of undefined value ValueId(57)`(`StringUtils.split/2`) + +#### 追加の事実(2025-12-20) + +- Pattern 6(index_of)回帰: PASS(Select 導入後もインフラは健全) +- `ValueId(57)` の正体: + - JoinIR `ValueId(1004)` の remap 先(JoinIR→Host の remap は成功している) + - `sep_len = sep.length()` のローカル値(loop_step 内で `BoxCall("length")` で定義される想定) +- つまり「remap の欠落」ではなく、「定義命令(dst を持つ BoxCall)が MIR に落ちていない/順序が壊れている」問題が濃厚 + +#### 次(P1.5 Task 3): sep_len 定義トレース(SSOT) + +目的: `sep_len` の「定義(dst)」が MIR に存在し、かつ use より前に配置されることを確認し、欠落しているなら根治する。 + +1) MIR ダンプで「%57 の定義があるか」を確認 + - `./target/release/hakorune --backend vm --dump-mir --mir-verbose apps/tests/phase256_p0_split_min.hako > /tmp/split.mir` + - `rg -n \"\\bValueId\\(57\\)\\b|%57\" /tmp/split.mir` + - 期待: `BoxCall` かそれに相当する命令で `dst=%57` が use より前に出る + +2) 定義が無い場合: JoinIR→MIR converter を疑う + - `JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(sep_len), method: \"length\", .. })` + が `MirInstruction::BoxCall { dst: Some(remapped), .. }` として出力されているかを点検する + - もし `dst: None` になっている/命令自体が落ちているなら、converter か value-collector の取りこぼしを修正する + +3) 定義があるが use より後の場合: ブロック内の命令順の生成/マージ規約を疑う + - joinir→mir の「生成順」か merge の「挿入位置」がおかしい + - 期待: “def-before-use” が各 BasicBlock 内で成立する + +4) 受け入れ基準(P1.5) + - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh` が PASS + - `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_llvm_exe.sh` が PASS +- 既存: `tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh` が PASS 維持 + +#### 診断アップデート(2025-12-20) + +`ValueId(57)`(= `sep_len`)について、Step 2 の結果で「Case A」が確定した: + +- JoinIR には定義が存在する: + - `JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(sep_len), method: "length", args: [sep] })` +- しかし最終 MIR(`--dump-mir`)には `%57 = ...` 相当の def が存在しない(use のみ) +- remap 自体は成功している: + - JoinIR `ValueId(1004)` → Host `ValueId(57)` + +結論: +- 「remap が壊れている」ではなく、 + **`JoinInst::Compute(MirLikeInst::BoxCall)` が JoinIR→MIR 変換/マージ経路のどこかで落ちている**。 + +次の実装タスク(P1.5 Task 3.1): +- JoinIR→MIR の中間生成物(bridge 側の MirModule)をダンプして、 + - `dst: Some(1004)` の `MirInstruction::BoxCall` が生成されているか(生成されていないなら converter バグ) + - 生成されているのに最終 MIR から消えるなら merge/DCE バグ + を二分探索で確定する。 + +#### Task 3.1 結果(2025-12-20) + +`ValueId(57)` undefined は根治できた(def-before-use 不変条件が回復)。 + +- 最終原因: + - `split_scan_minimal.rs` 内で `const_1`(`JoinValueSpace` のローカル ValueId)が **初期化されないまま** `i + const_1` に使用されていた。 + - これが remap 後に `ValueId(57)` となり、最終 MIR で「use のみ / def が無い」になっていた。 +- 修正: + - `const_1 = 1` を `JoinInst::Compute(MirLikeInst::Const { .. })` で use より前に挿入。 +- 付随: + - bridge 生成物 MirModule を `/tmp/joinir_bridge_split.mir` へダンプできるようにした(`HAKO_JOINIR_DEBUG=1` ガード)。 + +⚠️ 注記: +- これにより「ValueId(57) = sep_len」仮説は撤回する。 + - 以降は、各ローカル ValueId の意味は **bridge dump / join module dump を SSOT** として確定すること。 + +#### 次のブロッカー(P1.5 Task 3.2) + +`ValueId(57)` は直ったが、Pattern7(split) はまだ PASS していない。新たに以下が露出: + +- SSA: + - `Undefined value %49 used in block bb10` + - `Undefined value %67 used in block bb10` +- 型: + - `unsupported compare Le on BoxRef(ArrayBox) and Integer(...)` + +次のタスク(P1.5 Task 3.2): +- まず `--verify` と `--dump-mir` で **%49 / %67 が何の値で、どの命令で定義されるべきか**を確定し、 + - (A) joinir lowerer が def を吐いていないのか + - (B) bridge が落としているのか + - (C) merge/optimizer が落としているのか + を二分探索する。 + +#### 追記(P1.7完了後) + +- SSA undef(`%49/%67`)は P1.7 で根治済み +- 現在の first FAIL は carrier PHI 配線へ移動: + - `[joinir/exit-line] jump_args length mismatch: expected 3 or 4 but got 5` + - `Phase 33-16: Carrier 'i' has no latch incoming set` + +--- + +## 進捗(P1.5-DBG) + +### P1.5-DBG: Boundary Entry Parameter Contract Check(完了) + +目的: +- `boundary.join_inputs` と JoinIR entry function の `params` の対応(個数/順序/ValueId)を **VM 実行前**に fail-fast で検出する。 +- これにより、Pattern6 で起きた `loop_invariants` 順序バグ(例: `[s, ch]` ↔ `[ch, s]`)のような問題を「実行時の undef」ではなく「構造エラー」として落とせる。 + +実装(SSOT): +- `src/mir/builder/control_flow/joinir/merge/contract_checks.rs` + - `verify_boundary_entry_params()`(個数/順序/ValueId の検証) + - `get_entry_function()`(`join_module.entry` → `"main"` へのフォールバック) + - unit tests を追加(正常/順序ミスマッチ/個数ミスマッチ) +- `src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs` + - JoinIR→MIR 変換直前に検証を追加 + - `is_joinir_debug()` 時にログ出力 + +運用: +- `HAKO_JOINIR_DEBUG=1` で `[joinir/boundary-contract] ...` を出す(既存トグルのみ、env 追加なし)。 + +### P1.6: Pipeline Contract Checks の薄い集約(完了) + +目的: +- `conversion_pipeline.rs` から契約チェック呼び出しと debug ログの詳細を排除し、 + 契約チェックの SSOT を `contract_checks.rs` に集約する(dyn trait など過剰な箱化はしない)。 + +実装(SSOT): +- `src/mir/builder/control_flow/joinir/merge/contract_checks.rs` + - `run_all_pipeline_checks()`(薄い集約エントリ) + - `debug_log_boundary_contract()` を同ファイルへ移設(`is_joinir_debug()` ガード) +- `src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs` + - `run_all_pipeline_checks()` の 1 呼び出しに縮退 + +効果: +- pipeline 側の責務を「パイプラインの制御」に戻し、契約チェックは `contract_checks.rs` に閉じ込めた。 + 今後チェック項目を増やす場合も `run_all_pipeline_checks()` に追記するだけで済む。 + +--- + +## 進捗(P1.7) + +### P1.7: SSA undef(`%49/%67`)根治(完了) + +症状: +- `Undefined value %49 used in block bb10` +- `Undefined value %67 used in block bb10` + +根本原因: +- JoinIR→MIR bridge が `JoinFunction.name` をそのまま MirModule の関数名にしていた(例: `"k_exit"`, `"loop_step"`) +- merge 側が `join_func_name(id)`(例: `"join_func_2"`)で関数を探索していた +- その結果、continuation 関数が見つからず inline/merge がスキップされ、SSA undef が露出した + +修正方針: +- continuation 関数の識別を「関数ID→暗黙変換」に依存させず、MirModule 上の関数名(String)で SSOT 化する + +結果: +- `./target/release/hakorune --backend vm --verify apps/tests/phase256_p0_split_min.hako` で SSA undef は消滅 + +### リファクタリング方針(P1.6候補 / 先送り推奨) + +現時点(split がまだ FAIL)では、箱化のための箱化で複雑さが増えやすいので、以下を推奨する: + +- ✅ 先にやってよい(低リスク / 価値が高い) + - `contract_checks.rs` 内で「チェックの実行順」を `run_all_*()` のような薄い関数にまとめる(dyn trait は不要) + - debug ログは既存の仕組み(`is_joinir_debug()` / `DebugOutputBox`)に寄せる(新 logger box を増やさない) + - テストの JoinModule 構築は、重複が 3 箇所以上になった時点で共通化 + +- ⛔ 先送り(split を PASS してから) + - `ContractCheckerBox`(trait object)化:柔軟だが、ここでは過剰になりやすい + - `JoinIRDebugLoggerBox` 新設:既存の DebugOutputBox と二重化しやすい + - MIR 命令 dst 抽出の広域統一:既存の `MirInstruction` helper との重複が出やすいので要調査の上で + +### 小さな整理(今後の予定 / P1.8以降) + +- JoinIR の関数名は `src/mir/join_ir/lowering/canonical_names.rs` を SSOT とする + - `"k_exit"` / `"loop_step"` / `"main"` の直書きは段階的に `canonical_names::*` へ置換 + - 正規化 shadow の `"join_func_2"` は `canonical_names::K_EXIT_LEGACY` として隔離し、統一は Phase 256 完了後に検討 +- legacy 掃除候補: + - `join_func_name(id)` の利用箇所を棚卸しし、「structured JoinIR では使用禁止 / normalized shadow だけで使用」など境界を明文化 + +次(P1.5 Task 3): +- `ValueId(57)` が「何の JoinIR 値の remap 結果か」を確定し、定義側(dst)が MIR に落ちているかを追う + - 例: `sep_len = sep.length()` の BoxCall dst が収集/変換/順序のどこかで欠けていないか diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 885bc100..7aa9041d 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -58,7 +58,7 @@ mod exprs_peek; // peek expression mod exprs_qmark; // ?-propagate mod fields; // field access/assignment lowering split mod if_form; -mod joinir_id_remapper; // Phase 189: JoinIR ID remapping (ValueId/BlockId translation) +pub mod joinir_id_remapper; // Phase 189: JoinIR ID remapping (ValueId/BlockId translation) - Public for tests mod joinir_inline_boundary_injector; // Phase 189: JoinInlineBoundary Copy instruction injector mod lifecycle; mod loop_frontend_binding; // Phase 50: Loop Frontend Binding (JoinIR variable mapping) diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs index 8d401ccd..6c6936a3 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs @@ -498,6 +498,214 @@ pub(super) fn verify_header_phi_dsts_not_redefined( } } +/// Phase 256 P1.5-DBG: Contract check - Entry function parameters match boundary join_inputs exactly +/// +/// # Purpose +/// +/// Validates that `boundary.join_inputs` and `JoinModule.entry.params` have the same order, +/// count, and ValueId mapping. This prevents ordering bugs like the Pattern6 loop_invariants +/// issue where `[s, ch]` → `[ch, s]` required manual debugging. +/// +/// # Example Valid (Pattern6): +/// ```text +/// JoinModule.main.params: [ValueId(100), ValueId(101), ValueId(102)] (i, ch, s) +/// boundary.join_inputs: [ValueId(100), ValueId(101), ValueId(102)] +/// Check: params[0]==join_inputs[0] ✓, params[1]==join_inputs[1] ✓, etc. +/// ``` +/// +/// # Example Invalid (ordering bug): +/// ```text +/// JoinModule.main.params: [ValueId(100), ValueId(101), ValueId(102)] (i, ch, s) +/// boundary.join_inputs: [ValueId(100), ValueId(102), ValueId(101)] (i, s, ch - WRONG!) +/// Error: "Entry param[1] in 'main': expected ValueId(101), but boundary.join_inputs[1] = ValueId(102)" +/// ``` +/// +/// # Contract +/// +/// 1. Entry function params count must match `boundary.join_inputs` count +/// 2. Each `entry.params[i]` must equal `boundary.join_inputs[i]` (order + ValueId match) +/// 3. Optional: `join_inputs.len() == host_inputs.len()` (already asserted in constructor) +/// +/// # Returns +/// +/// - `Ok(())`: All parameters match correctly +/// - `Err(String)`: Mismatch found with clear diagnostic message +pub(in crate::mir::builder::control_flow::joinir) fn verify_boundary_entry_params( + join_module: &crate::mir::join_ir::JoinModule, + boundary: &JoinInlineBoundary, +) -> Result<(), String> { + use crate::mir::join_ir::lowering::error_tags; + + // Get entry function (priority: join_module.entry → fallback to "main") + let entry = get_entry_function(join_module)?; + + // Check 1: Count must match + if entry.params.len() != boundary.join_inputs.len() { + return Err(error_tags::freeze_with_hint( + "phase1.5/boundary/entry_param_count", + &format!( + "Entry function '{}' has {} params, but boundary has {} join_inputs", + entry.name, + entry.params.len(), + boundary.join_inputs.len() + ), + "ensure pattern lowerer sets boundary.join_inputs with one entry per parameter", + )); + } + + // Check 2: Each param must match in order + for (i, (entry_param, join_input)) in entry + .params + .iter() + .zip(boundary.join_inputs.iter()) + .enumerate() + { + if entry_param != join_input { + return Err(error_tags::freeze_with_hint( + "phase1.5/boundary/entry_param_mismatch", + &format!( + "Entry param[{}] in '{}': expected {:?}, but boundary.join_inputs[{}] = {:?}", + i, entry.name, entry_param, i, join_input + ), + "parameter ValueId mismatch indicates boundary.join_inputs constructed in wrong order", + )); + } + } + + // Check 3: Verify join_inputs.len() == host_inputs.len() (belt-and-suspenders) + // (Already asserted in JoinInlineBoundary constructor, but reconfirm for safety) + if boundary.join_inputs.len() != boundary.host_inputs.len() { + return Err(error_tags::freeze_with_hint( + "phase1.5/boundary/input_count_mismatch", + &format!( + "boundary.join_inputs ({}) and host_inputs ({}) have different lengths", + boundary.join_inputs.len(), + boundary.host_inputs.len() + ), + "BoundaryBuilder should prevent this - indicates constructor invariant violation", + )); + } + + Ok(()) +} + +/// Helper: Get entry function from JoinModule +/// +/// Priority: +/// 1. Use `join_module.entry` if Some +/// 2. Fallback to function named "main" +/// 3. Fail with descriptive error if neither exists +pub(super) fn get_entry_function( + join_module: &crate::mir::join_ir::JoinModule, +) -> Result<&crate::mir::join_ir::JoinFunction, String> { + use crate::mir::join_ir::lowering::error_tags; + + if let Some(entry_id) = join_module.entry { + return join_module.functions.get(&entry_id).ok_or_else(|| { + error_tags::freeze_with_hint( + "phase1.5/boundary/entry_not_found", + &format!("Entry function ID {:?} not found in module", entry_id), + "ensure JoinModule.entry points to valid JoinFuncId", + ) + }); + } + + // Fallback to "main" + join_module + .get_function_by_name(crate::mir::join_ir::lowering::canonical_names::MAIN) + .ok_or_else(|| { + error_tags::freeze_with_hint( + "phase1.5/boundary/no_entry_function", + "no entry function found (entry=None and no 'main' function)", + "pattern lowerer must set join_module.entry OR create 'main' function", + ) + }) +} + +/// Phase 256 P1.6: Run all JoinIR conversion pipeline contract checks +/// +/// Thin aggregation function that runs all pre-conversion contract checks. +/// This simplifies the conversion_pipeline.rs call site and makes it easy to add new checks. +/// +/// # Checks included: +/// - Phase 256 P1.5-DBG: Boundary entry parameter contract +/// - (Future checks can be added here) +/// +/// # Debug logging: +/// - Enabled when `is_joinir_debug()` is true (HAKO_JOINIR_DEBUG=1) +pub(in crate::mir::builder::control_flow::joinir) fn run_all_pipeline_checks( + join_module: &crate::mir::join_ir::JoinModule, + boundary: &JoinInlineBoundary, +) -> Result<(), String> { + // Phase 256 P1.5-DBG: Boundary entry parameter contract + verify_boundary_entry_params(join_module, boundary)?; + + // Debug logging (is_joinir_debug() only) + if crate::config::env::is_joinir_debug() { + debug_log_boundary_contract(join_module, boundary); + } + + Ok(()) +} + +/// Debug logging for boundary entry parameter contract validation +/// +/// Only enabled when `is_joinir_debug()` is true (HAKO_JOINIR_DEBUG=1). +/// +/// Outputs each parameter with OK/MISMATCH status for easy diagnosis. +/// +/// # Example Output +/// +/// ```text +/// [joinir/boundary-contract] Entry function 'main' params: +/// [0] entry=ValueId(100) join_input=ValueId(100) OK +/// [1] entry=ValueId(101) join_input=ValueId(101) OK +/// [2] entry=ValueId(102) join_input=ValueId(102) OK +/// ``` +#[cfg(debug_assertions)] +fn debug_log_boundary_contract( + join_module: &crate::mir::join_ir::JoinModule, + boundary: &JoinInlineBoundary, +) { + // Get entry function (priority: join_module.entry → fallback to "main") + let entry = if let Some(entry_id) = join_module.entry { + join_module.functions.get(&entry_id) + } else { + join_module.get_function_by_name("main") + }; + + if let Some(entry) = entry { + eprintln!( + "[joinir/boundary-contract] Entry function '{}' params:", + entry.name + ); + for (i, (entry_param, join_input)) in entry + .params + .iter() + .zip(boundary.join_inputs.iter()) + .enumerate() + { + let status = if entry_param == join_input { + "OK" + } else { + "MISMATCH" + }; + eprintln!( + " [{}] entry={:?} join_input={:?} {}", + i, entry_param, join_input, status + ); + } + } +} + +#[cfg(not(debug_assertions))] +fn debug_log_boundary_contract( + _join_module: &crate::mir::join_ir::JoinModule, + _boundary: &JoinInlineBoundary, +) { + // No-op in release mode +} + #[cfg(test)] mod tests { use super::*; @@ -612,4 +820,108 @@ mod tests { assert_eq!(contracts.allowed_missing_jump_targets.len(), 1); assert_eq!(contracts.allowed_missing_jump_targets[0], exit_block); } + + // ============================================================ + // Phase 256 P1.5-DBG: Boundary Entry Parameter Contract Tests + // ============================================================ + + #[test] + fn test_verify_boundary_entry_params_matches() { + // Case 1: All parameters match → OK + use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinModule}; + + let mut join_module = JoinModule::new(); + let main_func = JoinFunction::new( + JoinFuncId::new(0), + "main".to_string(), + vec![ValueId(100), ValueId(101), ValueId(102)], + ); + join_module.add_function(main_func); + join_module.entry = Some(JoinFuncId::new(0)); + + let boundary = JoinInlineBoundary::new_inputs_only( + vec![ValueId(100), ValueId(101), ValueId(102)], + vec![ValueId(4), ValueId(5), ValueId(6)], + ); + + let result = verify_boundary_entry_params(&join_module, &boundary); + assert!(result.is_ok(), "Matching params should pass: {:?}", result); + } + + #[test] + fn test_verify_boundary_entry_params_order_mismatch() { + // Case 2: Parameters in wrong order → FAIL with specific error + use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinModule}; + + let mut join_module = JoinModule::new(); + let main_func = JoinFunction::new( + JoinFuncId::new(0), + "main".to_string(), + vec![ValueId(100), ValueId(101), ValueId(102)], // i, ch, s + ); + join_module.add_function(main_func); + join_module.entry = Some(JoinFuncId::new(0)); + + // Wrong order: [i, s, ch] instead of [i, ch, s] + let boundary = JoinInlineBoundary::new_inputs_only( + vec![ValueId(100), ValueId(102), ValueId(101)], // WRONG ORDER + vec![ValueId(4), ValueId(6), ValueId(5)], + ); + + let result = verify_boundary_entry_params(&join_module, &boundary); + assert!(result.is_err(), "Order mismatch should fail"); + + let err = result.unwrap_err(); + assert!( + err.contains("param[1]"), + "Error should mention param[1]: {}", + err + ); + assert!( + err.contains("expected ValueId(101)"), + "Error should show expected ValueId(101): {}", + err + ); + assert!( + err.contains("ValueId(102)"), + "Error should show actual ValueId(102): {}", + err + ); + } + + #[test] + fn test_verify_boundary_entry_params_count_mismatch() { + // Case 3: Different count of parameters → FAIL + use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinModule}; + + let mut join_module = JoinModule::new(); + let main_func = JoinFunction::new( + JoinFuncId::new(0), + "main".to_string(), + vec![ValueId(100), ValueId(101), ValueId(102)], // 3 params + ); + join_module.add_function(main_func); + join_module.entry = Some(JoinFuncId::new(0)); + + // Boundary has only 2 inputs (count mismatch) + let boundary = JoinInlineBoundary::new_inputs_only( + vec![ValueId(100), ValueId(101)], // Only 2 inputs + vec![ValueId(4), ValueId(5)], + ); + + let result = verify_boundary_entry_params(&join_module, &boundary); + assert!(result.is_err(), "Count mismatch should fail"); + + let err = result.unwrap_err(); + assert!( + err.contains("has 3 params"), + "Error should mention 3 params: {}", + err + ); + assert!( + err.contains("2 join_inputs"), + "Error should mention 2 join_inputs: {}", + err + ); + } } diff --git a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 40001bcd..8029502e 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -17,7 +17,7 @@ use super::super::trace; use crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper; use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; -use crate::mir::join_ir_vm_bridge::join_func_name; +// Phase 256 P1.7: Removed join_func_name import - no longer needed use crate::mir::types::ConstValue; use crate::mir::{BasicBlock, BasicBlockId, MirFunction, MirInstruction, MirModule, MirType, ValueId}; use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism @@ -156,14 +156,10 @@ pub(super) fn merge_and_rewrite( }; } + // Phase 256 P1.7: continuation_func_ids is now BTreeSet (function names) + // No need to convert with join_func_name() - use directly let continuation_candidates: BTreeSet = boundary - .map(|b| { - b.continuation_func_ids - .iter() - .copied() - .map(join_func_name) - .collect() - }) + .map(|b| b.continuation_func_ids.clone()) .unwrap_or_default(); let skippable_continuation_func_names: BTreeSet = mir_module diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index c50274b0..8c34256e 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -14,7 +14,7 @@ mod block_allocator; mod carrier_init_builder; -mod contract_checks; +pub(super) mod contract_checks; // Phase 256 P1.5-DBG: Exposed for patterns to access verify_boundary_entry_params pub mod exit_args_collector; // Phase 118: Exit args collection box pub mod exit_line; mod exit_phi_builder; diff --git a/src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs b/src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs index 8147841d..cb4f0c77 100644 --- a/src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs +++ b/src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs @@ -93,12 +93,32 @@ impl JoinIRConversionPipeline { join_module.functions.values().map(|f| f.body.len()).sum(), ); + // Step 1.5: Run all pipeline contract checks (Phase 256 P1.5-DBG + P1.6) + if let Some(boundary) = boundary { + use super::super::merge::contract_checks::run_all_pipeline_checks; + run_all_pipeline_checks(&join_module, boundary)?; + } + // Step 2: JoinModule → MirModule conversion // Phase 256 P1.5: Pass boundary to bridge for ValueId remapping let empty_meta: JoinFuncMetaMap = BTreeMap::new(); let mir_module = bridge_joinir_to_mir_with_meta(&join_module, &empty_meta, boundary) .map_err(|e| format!("[{}/pipeline] MIR conversion failed: {:?}", pattern_name, e))?; + // Task 3.1-2: Dump bridge output for diagnosis (dev-only) + if crate::config::env::is_joinir_debug() { + use crate::mir::printer::MirPrinter; + use std::io::Write; + + let mir_text = MirPrinter::new().print_module(&mir_module); + if let Ok(mut file) = std::fs::File::create("/tmp/joinir_bridge_split.mir") { + let _ = writeln!(file, "; Bridge output for {}", pattern_name); + let _ = writeln!(file, "; JoinIR → MIR conversion (before merge)\n"); + let _ = write!(file, "{}", mir_text); + eprintln!("[trace:bridge] Dumped bridge MIR to /tmp/joinir_bridge_split.mir"); + } + } + // Step 3: Log MIR stats (functions and blocks) trace::trace().joinir_stats( pattern_name, diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs index 755f7844..5cecf3a9 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs @@ -383,8 +383,9 @@ mod tests { expr_result: None, // Phase 33-14: Add missing field loop_var_name: None, // Phase 33-16: Add missing field carrier_info: None, // Phase 228: Add missing field + loop_invariants: vec![], // Phase 255 P2: Add missing field continuation_func_ids: std::collections::BTreeSet::from([ - crate::mir::join_ir::JoinFuncId::new(2), + "k_exit".to_string(), // Phase 256 P1.7: Use String instead of JoinFuncId ]), exit_reconnect_mode: crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::default(), // Phase 131 P1.5 }; diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs index cf9bf4db..8361ebc7 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs @@ -137,8 +137,9 @@ mod tests { expr_result: None, // Phase 33-14: Add missing field loop_var_name: None, // Phase 33-16: Add missing field carrier_info: None, // Phase 228: Add missing field + loop_invariants: vec![], // Phase 255 P2: Add missing field continuation_func_ids: std::collections::BTreeSet::from([ - crate::mir::join_ir::JoinFuncId::new(2), + "k_exit".to_string(), // Phase 256 P1.7: Use String instead of JoinFuncId ]), exit_reconnect_mode: crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::default(), // Phase 131 P1.5 }; 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 index cffeadff..ae62c446 100644 --- 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 @@ -425,10 +425,13 @@ impl MirBuilder { vec![], // Empty carriers - only loop_var ); - // Phase 255 P2: Create loop_invariants for s and ch + // Phase 255 P2: Create loop_invariants for ch and s + // CRITICAL: Order MUST match JoinModule loop_step params: [i, ch, s] + // carrier_order is built as: [loop_var] + loop_invariants + // So loop_invariants order determines param-to-PHI mapping for invariants! let loop_invariants = vec![ - (parts.haystack.clone(), s_host), // s: haystack - (parts.needle.clone(), ch_host), // ch: needle + (parts.needle.clone(), ch_host), // ch: needle (JoinIR param 1) + (parts.haystack.clone(), s_host), // s: haystack (JoinIR param 2) ]; if debug { @@ -457,7 +460,10 @@ impl MirBuilder { // Loop invariants (s, ch) do NOT need exit bindings use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; - let k_exit_func = join_module.require_function("k_exit", "Pattern 6"); + let k_exit_func = join_module.require_function( + crate::mir::join_ir::lowering::canonical_names::K_EXIT, + "Pattern 6", + ); let join_exit_value_i = k_exit_func .params .first() @@ -563,7 +569,7 @@ impl MirBuilder { #[cfg(test)] mod tests { use super::*; - use crate::ast::{ASTNode, BinaryOperator, Literal, Span}; + use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; #[test] fn test_contains_methodcall_positive() { @@ -620,7 +626,7 @@ mod tests { span: Span::unknown(), }), right: Box::new(ASTNode::Literal { - value: Literal::Integer(1), + value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), @@ -639,7 +645,7 @@ mod tests { span: Span::unknown(), }), right: Box::new(ASTNode::Literal { - value: Literal::Integer(1), + value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs index 60980835..12834276 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs @@ -471,12 +471,15 @@ impl MirBuilder { )], ); - // Phase 255 P2: Create loop_invariants for s, sep, result + // Phase 255 P2: Create loop_invariants for result, s, sep + // CRITICAL: Order MUST match JoinModule loop_step params: [i, start, result, s, sep] + // carrier_order is built as: [loop_var (i), carriers (start)] + loop_invariants + // So loop_invariants order must be [result, s, sep] to match param indices 2, 3, 4! // Phase 256 P1.5: result needs to be in BOTH loop_invariants (for initial value) AND exit_bindings (for return) let loop_invariants = vec![ - (parts.s_var.clone(), s_host), // s: haystack (read-only) - (parts.sep_var.clone(), sep_host), // sep: separator (read-only) - (parts.result_var.clone(), result_host), // result: also needs initial value for post-loop code + (parts.result_var.clone(), result_host), // result: JoinIR param 2 + (parts.s_var.clone(), s_host), // s: JoinIR param 3 (haystack, read-only) + (parts.sep_var.clone(), sep_host), // sep: JoinIR param 4 (separator, read-only) ]; if debug { @@ -573,6 +576,7 @@ impl MirBuilder { // Step 6: Build boundary with carrier_info and loop_invariants // Phase 256 P1.5: Set expr_result to result_exit_param so the loop expression returns the result + // Phase 256 P1.7: Register k_exit as continuation function for proper merging let boundary = JoinInlineBoundaryBuilder::new() .with_inputs(join_inputs, host_inputs) .with_loop_invariants(loop_invariants) // Phase 255 P2: Add loop invariants @@ -580,6 +584,7 @@ impl MirBuilder { .with_expr_result(Some(join_exit_value_result)) // Phase 256 P1.5: Loop expression returns result .with_loop_var_name(Some(parts.i_var.clone())) .with_carrier_info(carrier_info.clone()) // ✅ Key: carrier_info for multi-PHI + .with_k_exit_continuation() // Phase 256 P1.7: Convenience API for k_exit registration .build(); if debug { diff --git a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs index 1d4e0634..80a23437 100644 --- a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs +++ b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs @@ -348,10 +348,12 @@ impl LoopTrueBreakOnceBuilderBox { // k_exit(env): handle post-loop or return // Phase 143 fix: reuse Param region IDs for all functions + // Phase 256 P1.7: Use canonical name from SSOT (legacy variant for normalized shadow) + use crate::mir::join_ir::lowering::canonical_names as cn; let k_exit_params = main_params.clone(); let env_k_exit = NormalizedHelperBox::build_env_map(&env_fields, &k_exit_params); let mut k_exit_func = - JoinFunction::new(k_exit_id, "join_func_2".to_string(), k_exit_params); + JoinFunction::new(k_exit_id, cn::K_EXIT_LEGACY.to_string(), k_exit_params); if has_post_computation { // Phase 132-P4/133-P0: k_exit → TailCall(post_k, env) @@ -455,7 +457,8 @@ impl LoopTrueBreakOnceBuilderBox { exit_values: exit_values_for_meta, }; let mut meta = JoinFragmentMeta::carrier_only(exit_meta); - meta.continuation_funcs.insert(k_exit_id); + // Phase 256 P1.7: Use canonical name from SSOT (legacy variant for normalized shadow) + meta.continuation_funcs.insert(cn::K_EXIT_LEGACY.to_string()); return Ok(Some((module, meta))); } @@ -524,7 +527,8 @@ impl LoopTrueBreakOnceBuilderBox { exit_values: exit_values_for_meta, }; let mut meta = JoinFragmentMeta::carrier_only(exit_meta); - meta.continuation_funcs.insert(k_exit_id); + // Phase 256 P1.7: Use canonical name from SSOT (legacy variant for normalized shadow) + meta.continuation_funcs.insert(cn::K_EXIT_LEGACY.to_string()); Ok(Some((module, meta))) } diff --git a/src/mir/join_ir/lowering/canonical_names.rs b/src/mir/join_ir/lowering/canonical_names.rs new file mode 100644 index 00000000..423c319f --- /dev/null +++ b/src/mir/join_ir/lowering/canonical_names.rs @@ -0,0 +1,100 @@ +//! Phase 256 P1.7: Canonical JoinIR Function Names (SSOT) +//! +//! This module provides a Single Source of Truth (SSOT) for JoinIR function names +//! that are used throughout the codebase. By centralizing these names here, we: +//! +//! 1. **Eliminate magic strings**: No more scattered "k_exit", "loop_step" literals +//! 2. **Ensure consistency**: All code uses the same canonical names +//! 3. **Simplify refactoring**: Change the name in one place, not dozens +//! 4. **Improve readability**: Clear intent with named constants +//! +//! ## Usage +//! +//! ```rust +//! use crate::mir::join_ir::lowering::canonical_names as cn; +//! +//! // Instead of: +//! let func_name = "k_exit".to_string(); +//! +//! // Use: +//! let func_name = cn::K_EXIT.to_string(); +//! ``` +//! +//! ## Design Note +//! +//! These names represent the canonical function names used in JoinModule. +//! The bridge uses `JoinFunction.name` as the MirModule function key, +//! not `join_func_name(id)`. This SSOT ensures all components agree on +//! the exact spelling of these critical function names. + +/// Canonical name for loop exit/continuation function +/// +/// Used in: +/// - Pattern 2, 3, 4, 5, 6, 7 (loop patterns with exit continuations) +/// - JoinInlineBoundary.continuation_funcs +/// - ExitLine/ExitMeta handling +/// +/// Historical note: Some normalized shadow code uses "join_func_2" instead. +/// See K_EXIT_LEGACY for compatibility. +pub const K_EXIT: &str = "k_exit"; + +/// Legacy canonical name for k_exit in normalized shadow code +/// +/// Used in: +/// - normalized_shadow/loop_true_break_once.rs (line 354, 460, 531) +/// +/// TODO (Phase 256 P1.7): Unify with K_EXIT or keep as separate const +/// if semantic difference exists. +pub const K_EXIT_LEGACY: &str = "join_func_2"; + +/// Canonical name for loop step/body function +/// +/// Used in: +/// - Pattern 1, 2, 3, 4, 5, 6, 7 (all loop patterns) +/// - LoopScopeShape inspection +/// - Normalized JoinIR validation +pub const LOOP_STEP: &str = "loop_step"; + +/// Canonical name for main entry function +/// +/// Used in: +/// - Entry point detection +/// - JoinIR module main function naming +/// - MIR builder entry selection +pub const MAIN: &str = "main"; + +/// Canonical name for post-continuation function (if variant) +/// +/// Used in: +/// - Pattern 3 with post-if computation (Phase 132-P4/133-P0) +/// - Normalized shadow exit routing +pub const POST_K: &str = "post_k"; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_canonical_names_are_not_empty() { + assert!(!K_EXIT.is_empty()); + assert!(!K_EXIT_LEGACY.is_empty()); + assert!(!LOOP_STEP.is_empty()); + assert!(!MAIN.is_empty()); + assert!(!POST_K.is_empty()); + } + + #[test] + fn test_canonical_names_have_expected_values() { + assert_eq!(K_EXIT, "k_exit"); + assert_eq!(K_EXIT_LEGACY, "join_func_2"); + assert_eq!(LOOP_STEP, "loop_step"); + assert_eq!(MAIN, "main"); + assert_eq!(POST_K, "post_k"); + } + + #[test] + fn test_k_exit_variants_differ() { + // These should be different (historical reasons) + assert_ne!(K_EXIT, K_EXIT_LEGACY); + } +} diff --git a/src/mir/join_ir/lowering/carrier_info.rs b/src/mir/join_ir/lowering/carrier_info.rs index 9d03b03d..2e327e4b 100644 --- a/src/mir/join_ir/lowering/carrier_info.rs +++ b/src/mir/join_ir/lowering/carrier_info.rs @@ -798,14 +798,19 @@ pub struct JoinFragmentMeta { pub exit_meta: ExitMeta, /// Phase 132 P1: Continuation contract (SSOT) + /// Phase 256 P1.7: Changed from BTreeSet to BTreeSet /// - /// JoinIR merge must NOT “guess” continuation functions by name/ID. - /// Normalized shadow (and other frontends) must explicitly declare which JoinFuncIds + /// JoinIR merge must NOT "guess" continuation functions by name. + /// Normalized shadow (and other frontends) must explicitly declare which function names /// are continuations for the fragment, and merge must follow this contract. /// /// Merge may still choose to *skip* some continuation functions if and only if they - /// are structurally “skippable” (pure exit stubs). See merge/instruction_rewriter.rs. - pub continuation_funcs: BTreeSet, + /// are structurally "skippable" (pure exit stubs). See merge/instruction_rewriter.rs. + /// + /// **Why Strings instead of JoinFuncIds**: The bridge uses JoinFunction.name as the + /// MirModule function key (e.g., "k_exit"), not "join_func_{id}". The merge code + /// looks up functions by name, so we must use actual function names here. + pub continuation_funcs: BTreeSet, } impl JoinFragmentMeta { diff --git a/src/mir/join_ir/lowering/inline_boundary.rs b/src/mir/join_ir/lowering/inline_boundary.rs index ff063ffa..7fd431eb 100644 --- a/src/mir/join_ir/lowering/inline_boundary.rs +++ b/src/mir/join_ir/lowering/inline_boundary.rs @@ -305,13 +305,19 @@ pub struct JoinInlineBoundary { pub carrier_info: Option, /// Phase 132 P1: Continuation contract (SSOT) + /// Phase 256 P1.7: Changed from BTreeSet to BTreeSet /// /// JoinIR merge must not infer/guess continuation functions. The router/lowerer - /// must declare continuation JoinFuncIds here. + /// must declare continuation function names here. /// /// Merge may still choose to *skip* a continuation function if it is a pure - /// exit stub (structural check), but it must never skip based on name/ID alone. - pub continuation_func_ids: BTreeSet, + /// exit stub (structural check), but it must never skip based on name alone. + /// + /// **Why Strings instead of JoinFuncIds**: The MirModule after bridge conversion + /// uses JoinFunction.name (e.g., "k_exit") as the key, not "join_func_{id}". + /// The merge code looks up functions by name in MirModule.functions, so we must + /// use the actual function names here. + pub continuation_func_ids: BTreeSet, /// Phase 131 P1.5: Exit reconnection mode /// @@ -324,17 +330,24 @@ pub struct JoinInlineBoundary { } impl JoinInlineBoundary { - /// Phase 132-R0 Task 1: SSOT for default continuation function IDs + /// Phase 132-R0 Task 1: SSOT for default continuation function names + /// Phase 256 P1.7: Changed from JoinFuncIds to function names (Strings) /// - /// Returns the default set of continuation functions (k_exit = join_func_2). + /// Returns the default set of continuation functions (k_exit). /// This is the single source of truth for continuation function identification. /// /// # Rationale /// /// - Router/lowerer must declare continuation functions explicitly - /// - Merge must NOT infer continuations by name or ID + /// - Merge must NOT infer continuations by name alone /// - This method centralizes the default continuation contract /// + /// # Why Strings instead of JoinFuncIds + /// + /// The bridge uses JoinFunction.name as the MirModule function key (e.g., "k_exit"), + /// not "join_func_{id}". The merge code looks up functions by name in MirModule.functions, + /// so we must use actual function names here. + /// /// # Usage /// /// Use this method when constructing JoinInlineBoundary objects: @@ -346,8 +359,8 @@ impl JoinInlineBoundary { /// // ... /// } /// ``` - pub fn default_continuations() -> BTreeSet { - BTreeSet::from([JoinFuncId::new(2)]) + pub fn default_continuations() -> BTreeSet { + BTreeSet::from([crate::mir::join_ir::lowering::canonical_names::K_EXIT.to_string()]) } /// Create a new boundary with input mappings only diff --git a/src/mir/join_ir/lowering/inline_boundary_builder.rs b/src/mir/join_ir/lowering/inline_boundary_builder.rs index d3d5c18b..087b9221 100644 --- a/src/mir/join_ir/lowering/inline_boundary_builder.rs +++ b/src/mir/join_ir/lowering/inline_boundary_builder.rs @@ -310,6 +310,67 @@ impl JoinInlineBoundaryBuilder { self.boundary.carrier_info = Some(carrier_info); self } + + /// Set continuation function names (Phase 256 P1.7) + /// + /// Continuation functions (e.g., k_exit) are functions that should be merged + /// into the host function. This method registers which JoinIR functions are + /// continuations, enabling proper merge behavior. + /// + /// # Arguments + /// + /// * `func_names` - Set of function names (Strings) representing continuation functions + /// + /// # Example + /// + /// ```ignore + /// use std::collections::BTreeSet; + /// use crate::mir::join_ir::lowering::canonical_names as cn; + /// let boundary = JoinInlineBoundaryBuilder::new() + /// .with_inputs(join_inputs, host_inputs) + /// .with_continuation_funcs(BTreeSet::from([cn::K_EXIT.to_string()])) + /// .build(); + /// ``` + /// + /// # Why Strings instead of JoinFuncIds + /// + /// The MirModule after bridge conversion uses JoinFunction.name as the function key, + /// not "join_func_{id}". The merge code looks up functions by name, so we must use + /// actual function names here. + pub fn with_continuation_funcs(mut self, func_names: std::collections::BTreeSet) -> Self { + self.boundary.continuation_func_ids = func_names; + self + } + + /// Phase 256 P1.7: Register k_exit as continuation (convenience method) + /// + /// This is a convenience method for the common case of registering "k_exit" as a + /// continuation function. It's equivalent to: + /// ```ignore + /// use crate::mir::join_ir::lowering::canonical_names as cn; + /// .with_continuation_funcs(BTreeSet::from([cn::K_EXIT.to_string()])) + /// ``` + /// + /// # Example + /// + /// ```ignore + /// let boundary = JoinInlineBoundaryBuilder::new() + /// .with_inputs(join_inputs, host_inputs) + /// .with_k_exit_continuation() + /// .build(); + /// ``` + /// + /// # Note + /// + /// For multiple continuations or custom function names, use `with_continuation_funcs()` + /// instead. This method is specifically for the "k_exit" pattern. + pub fn with_k_exit_continuation(mut self) -> Self { + use super::canonical_names as cn; + self.boundary + .continuation_func_ids + .insert(cn::K_EXIT.to_string()); + self + } } impl Default for JoinInlineBoundaryBuilder { @@ -479,4 +540,48 @@ mod tests { assert_eq!(boundary.host_inputs.len(), 1); assert_eq!(boundary.host_inputs[0], ValueId(101)); } + + #[test] + fn test_with_k_exit_continuation() { + // Phase 256 P1.7: Test convenience method for k_exit registration + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(vec![ValueId(0)], vec![ValueId(100)]) + .with_k_exit_continuation() + .build(); + + assert_eq!(boundary.continuation_func_ids.len(), 1); + assert!(boundary.continuation_func_ids.contains("k_exit")); + } + + #[test] + fn test_with_continuation_funcs_manual() { + // Phase 256 P1.7: Test manual continuation registration (should be same as with_k_exit_continuation) + use std::collections::BTreeSet; + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(vec![ValueId(0)], vec![ValueId(100)]) + .with_continuation_funcs(BTreeSet::from(["k_exit".to_string()])) + .build(); + + assert_eq!(boundary.continuation_func_ids.len(), 1); + assert!(boundary.continuation_func_ids.contains("k_exit")); + } + + #[test] + fn test_with_k_exit_and_additional_continuation() { + // Phase 256 P1.7: Test combining convenience method with additional continuations + use std::collections::BTreeSet; + let mut continuations = BTreeSet::new(); + continuations.insert("post_k".to_string()); + + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(vec![ValueId(0)], vec![ValueId(100)]) + .with_k_exit_continuation() + .with_continuation_funcs(continuations) + .build(); + + // with_continuation_funcs replaces the set, so only post_k should be present + assert_eq!(boundary.continuation_func_ids.len(), 1); + assert!(boundary.continuation_func_ids.contains("post_k")); + assert!(!boundary.continuation_func_ids.contains("k_exit")); + } } diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index d6b9f6fb..3164eb8f 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -20,6 +20,7 @@ //! - `loop_pattern_router.rs`: Phase 33-12 Loop pattern routing (Pattern 1-4 dispatcher) pub(crate) mod bool_expr_lowerer; // Phase 168: Boolean expression lowering (unused - candidate for removal) +pub mod canonical_names; // Phase 256 P1.7: SSOT for JoinIR function names (k_exit, loop_step, main) pub mod carrier_binding_assigner; // Phase 78: BindingId assignment for promoted carriers (dev-only) pub mod carrier_info; // Phase 196: Carrier metadata for loop lowering pub(crate) mod carrier_update_emitter; // Phase 179: Carrier update instruction emission diff --git a/src/mir/join_ir/lowering/scan_with_init_minimal.rs b/src/mir/join_ir/lowering/scan_with_init_minimal.rs index 236bcb6f..99803c2d 100644 --- a/src/mir/join_ir/lowering/scan_with_init_minimal.rs +++ b/src/mir/join_ir/lowering/scan_with_init_minimal.rs @@ -56,6 +56,7 @@ //! - Step must be 1 (P0 restriction) //! - not_found_return_lit must be -1 (P0 restriction) +use crate::mir::join_ir::lowering::canonical_names as cn; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::{ BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, @@ -157,7 +158,7 @@ pub(crate) fn lower_scan_with_init_minimal( // Phase 255 P0: Loop variable first, then alphabetical [ch, s] let mut loop_step_func = JoinFunction::new( loop_step_id, - "loop_step".to_string(), + cn::LOOP_STEP.to_string(), vec![i_step_param, ch_step_param, s_step_param], // Phase 255 P0: [i, ch, s] alphabetical ); @@ -254,7 +255,7 @@ pub(crate) fn lower_scan_with_init_minimal( // ================================================================== // k_exit(i_exit) function // ================================================================== - let mut k_exit_func = JoinFunction::new(k_exit_id, "k_exit".to_string(), vec![i_exit_param]); + let mut k_exit_func = JoinFunction::new(k_exit_id, cn::K_EXIT.to_string(), vec![i_exit_param]); // Return i_exit (found index or -1) k_exit_func.body.push(JoinInst::Ret { @@ -294,7 +295,7 @@ mod tests { .functions .get(&JoinFuncId::new(2)) .expect("k_exit function should exist"); - assert_eq!(k_exit_func.name, "k_exit"); + assert_eq!(k_exit_func.name, cn::K_EXIT); } #[test] diff --git a/src/mir/join_ir/lowering/simple_while_minimal.rs b/src/mir/join_ir/lowering/simple_while_minimal.rs index 8165f33e..018cfd6e 100644 --- a/src/mir/join_ir/lowering/simple_while_minimal.rs +++ b/src/mir/join_ir/lowering/simple_while_minimal.rs @@ -42,6 +42,7 @@ //! //! Following the "80/20 rule" from CLAUDE.md - get it working first, generalize later. +use crate::mir::join_ir::lowering::canonical_names as cn; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; use crate::mir::join_ir::{ @@ -151,7 +152,7 @@ pub(crate) fn lower_simple_while_minimal( // loop_step(i) function // ================================================================== let mut loop_step_func = - JoinFunction::new(loop_step_id, "loop_step".to_string(), vec![i_step_param]); + JoinFunction::new(loop_step_id, cn::LOOP_STEP.to_string(), vec![i_step_param]); // exit_cond = !(i < 3) // Step 1: const 3 @@ -229,7 +230,7 @@ pub(crate) fn lower_simple_while_minimal( // ================================================================== // k_exit(i_exit) function - Phase 132: receives loop variable // ================================================================== - let mut k_exit_func = JoinFunction::new(k_exit_id, "k_exit".to_string(), vec![i_exit_param]); + let mut k_exit_func = JoinFunction::new(k_exit_id, cn::K_EXIT.to_string(), vec![i_exit_param]); // Phase 132: return i_exit (loop variable at exit) // This ensures VM/LLVM parity for `return i` after loop diff --git a/src/mir/join_ir/lowering/split_scan_minimal.rs b/src/mir/join_ir/lowering/split_scan_minimal.rs index 69b69aaf..c86d563b 100644 --- a/src/mir/join_ir/lowering/split_scan_minimal.rs +++ b/src/mir/join_ir/lowering/split_scan_minimal.rs @@ -162,7 +162,7 @@ pub(crate) fn lower_split_scan_minimal( // ================================================================== let mut main_func = JoinFunction::new( main_id, - "main".to_string(), + crate::mir::join_ir::lowering::canonical_names::MAIN.to_string(), vec![i_main_param, start_main_param, result_main_param, s_main_param, sep_main_param], ); @@ -182,7 +182,7 @@ pub(crate) fn lower_split_scan_minimal( // ================================================================== let mut loop_step_func = JoinFunction::new( loop_step_id, - "loop_step".to_string(), + crate::mir::join_ir::lowering::canonical_names::LOOP_STEP.to_string(), vec![i_step_param, start_step_param, result_step_param, s_step_param, sep_step_param], ); @@ -282,6 +282,14 @@ pub(crate) fn lower_split_scan_minimal( // i_next_if = start_next_if (same as i_plus_sep) let i_next_if_actual = start_next_if_actual; // Reuse i_plus_sep + // Task 3.1-3 FIX: Initialize const_1 = 1 before use + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); + // 8. No-match case: i_next_else = i + 1 loop_step_func .body @@ -332,7 +340,7 @@ pub(crate) fn lower_split_scan_minimal( let mut k_exit_func = JoinFunction::new( k_exit_id, - "k_exit".to_string(), + crate::mir::join_ir::lowering::canonical_names::K_EXIT.to_string(), vec![i_exit_param, start_exit_param, result_exit_param, s_exit_param], ); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0bc3f380..715df585 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -38,3 +38,9 @@ pub mod vtable; // Phase 34-2: JoinIR Frontend (AST→JoinIR) and related components pub mod joinir; + +// Phase 40-3: array_ext.filter A/B test +pub mod phase40_array_ext_filter_test; + +// Phase 256 P1.5: Select instruction minimal test +pub mod phase256_select_minimal_test; diff --git a/src/tests/phase256_select_minimal_test.rs b/src/tests/phase256_select_minimal_test.rs new file mode 100644 index 00000000..3556e8c3 --- /dev/null +++ b/src/tests/phase256_select_minimal_test.rs @@ -0,0 +1,132 @@ +//! Phase 256 P1.5: Minimal Select instruction unit test +//! +//! Verifies that JoinInst::Select remapping and collection work correctly. +//! Focuses on low-level ValueId handling independent of bridge/conversion code. + +#[cfg(test)] +mod select_minimal_test { + use crate::mir::{ + MirInstruction, ValueId, + }; + use crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper; + + /// Test: Select instruction ValueId consistency + /// + /// Verifies that a Select instruction can be created with all required fields + /// and that ValueIds are properly stored and retrievable. + #[test] + fn test_select_instruction_creation() { + // Create Select instruction: %4 = select %3 ? %1 : %2 + let select_inst = MirInstruction::Select { + dst: ValueId::new(4), + cond: ValueId::new(3), + then_val: ValueId::new(1), + else_val: ValueId::new(2), + }; + + // Verify instruction can be matched and ValueIds extracted + match select_inst { + MirInstruction::Select { dst, cond, then_val, else_val } => { + assert_eq!(dst.0, 4, "dst ValueId should be 4"); + assert_eq!(cond.0, 3, "cond ValueId should be 3"); + assert_eq!(then_val.0, 1, "then_val ValueId should be 1"); + assert_eq!(else_val.0, 2, "else_val ValueId should be 2"); + + eprintln!("[test] ✅ Select instruction created and verified:"); + eprintln!("[test] dst: %{}, cond: %{}, then_val: %{}, else_val: %{}", dst.0, cond.0, then_val.0, else_val.0); + } + _ => panic!("Expected Select instruction"), + } + } + + /// Test: Remapper handles Select instruction + /// + /// Verifies that JoinIrIdRemapper.remap_instruction() properly remaps + /// all ValueIds in Select instruction. + #[test] + fn test_remapper_handles_select() { + let mut remapper = JoinIrIdRemapper::new(); + + // Setup: Map ValueIds from JoinIR local range (1000+) to host range (<1000) + remapper.set_value(ValueId::new(1001), ValueId::new(51)); // cond + remapper.set_value(ValueId::new(1002), ValueId::new(52)); // then_val + remapper.set_value(ValueId::new(1003), ValueId::new(53)); // else_val + remapper.set_value(ValueId::new(1004), ValueId::new(54)); // dst + + // Create Select instruction with JoinIR-local ValueIds + let select_inst = MirInstruction::Select { + dst: ValueId::new(1004), + cond: ValueId::new(1001), + then_val: ValueId::new(1002), + else_val: ValueId::new(1003), + }; + + // Remap the instruction + let remapped = remapper.remap_instruction(&select_inst); + + // Verify: all ValueIds are remapped + match remapped { + MirInstruction::Select { dst, cond, then_val, else_val } => { + assert_eq!(dst.0, 54, "dst should be remapped to 54"); + assert_eq!(cond.0, 51, "cond should be remapped to 51"); + assert_eq!(then_val.0, 52, "then_val should be remapped to 52"); + assert_eq!(else_val.0, 53, "else_val should be remapped to 53"); + + eprintln!("[test] ✅ Remapper correctly remaps Select instruction:"); + eprintln!("[test] dst: 1004 → {}", dst.0); + eprintln!("[test] cond: 1001 → {}", cond.0); + eprintln!("[test] then_val: 1002 → {}", then_val.0); + eprintln!("[test] else_val: 1003 → {}", else_val.0); + } + _ => panic!("Expected Select instruction after remapping"), + } + } + + /// Test: ValueId collection includes Select + /// + /// Verifies that JoinIrIdRemapper.collect_values_in_instruction() + /// properly collects all ValueIds from Select instruction. + #[test] + fn test_collector_handles_select() { + let remapper = JoinIrIdRemapper::new(); + + // Create Select instruction + let select_inst = MirInstruction::Select { + dst: ValueId::new(100), + cond: ValueId::new(101), + then_val: ValueId::new(102), + else_val: ValueId::new(103), + }; + + // Collect ValueIds + let collected = remapper.collect_values_in_instruction(&select_inst); + + // Verify: all 4 ValueIds are collected + assert_eq!( + collected.len(), + 4, + "Should collect 4 ValueIds, got {}", + collected.len() + ); + + assert!( + collected.contains(&ValueId::new(100)), + "dst should be collected" + ); + assert!( + collected.contains(&ValueId::new(101)), + "cond should be collected" + ); + assert!( + collected.contains(&ValueId::new(102)), + "then_val should be collected" + ); + assert!( + collected.contains(&ValueId::new(103)), + "else_val should be collected" + ); + + eprintln!("[test] ✅ Collector properly collects Select ValueIds:"); + eprintln!("[test] Collected: {:?}", collected); + } +} diff --git a/src/tests/phase40_array_ext_filter_test.rs b/src/tests/phase40_array_ext_filter_test.rs index b0ad788e..7485ee74 100644 --- a/src/tests/phase40_array_ext_filter_test.rs +++ b/src/tests/phase40_array_ext_filter_test.rs @@ -78,7 +78,7 @@ fn phase40_mir_conversion_with_empty_meta() { let meta = JoinFuncMetaMap::new(); // Should not panic - let result = convert_join_module_to_mir_with_meta(&module, &meta); + let result = convert_join_module_to_mir_with_meta(&module, &meta, None); assert!(result.is_ok(), "Empty metadata should not cause errors"); let mir_module = result.unwrap(); @@ -121,7 +121,7 @@ fn phase40_mir_conversion_with_meta() { ); // Should not panic, metadata is logged but not used for PHI generation yet - let result = convert_join_module_to_mir_with_meta(&module, &meta); + let result = convert_join_module_to_mir_with_meta(&module, &meta, None); assert!(result.is_ok(), "Metadata should not cause errors"); let mir_module = result.unwrap();