From fe895e8838519d9f6c1acc2f1da9e738bdf58d5e Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sat, 27 Dec 2025 11:05:40 +0900 Subject: [PATCH] refactor(joinir): Phase 287 P2 - Modularize contract_checks (facade pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contract_checks.rs (846行) を facade 化 - 6モジュールへ分割(1 module = 1 contract): - terminator_targets.rs (208行) - Branch/Jump検証 - exit_bindings.rs (35行) - exit_bindings ↔ exit_phis - carrier_inputs.rs (145行) - carrier_inputs完全性 - boundary_creation.rs (160行) - B1/C2不変条件 - entry_params.rs (317行) - Entry param一貫性 - mod.rs (30行) - Facade - Total: 846 → 895行(+49行モジュール境界オーバーヘッド) - 意味論不変: エラータグ/ヒント文すべて保存 - Fail-Fast遵守: silent fallback追加なし - 検証: Build 0 errors / Pattern6 RC=9 / quick 154/154 PASS 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- docs/development/current/main/10-Now.md | 19 +- docs/development/current/main/30-Backlog.md | 7 +- .../P0-BIGFILES-REFACTORING-INSTRUCTIONS.md | 18 +- .../P0-MERGE_MOD_MODULARIZATION_PLAN.md | 10 +- .../P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md | 107 +++ ...RACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md | 110 +++ .../current/main/phases/phase-287/README.md | 49 +- .../joinir/merge/contract_checks.rs | 846 ------------------ .../contract_checks/boundary_creation.rs | 160 ++++ .../merge/contract_checks/carrier_inputs.rs | 145 +++ .../merge/contract_checks/entry_params.rs | 317 +++++++ .../merge/contract_checks/exit_bindings.rs | 35 + .../joinir/merge/contract_checks/mod.rs | 30 + .../contract_checks/terminator_targets.rs | 208 +++++ 14 files changed, 1195 insertions(+), 866 deletions(-) create mode 100644 docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md create mode 100644 docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md delete mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/carrier_inputs.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/entry_params.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/exit_bindings.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs create mode 100644 src/mir/builder/control_flow/joinir/merge/contract_checks/terminator_targets.rs diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index eb33c874..0f077771 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -1,6 +1,6 @@ # Self Current Task — Now (main) -## Current Focus: Post Phase 188.3 (Refactoring Window) +## Current Focus: Phase 287 P2(Contract Checks facade) **2025-12-27: Phase 188.3 完了** ✅ - Pattern6(NestedLoopMinimal): `apps/tests/phase1883_nested_minimal.hako` が RC=9 @@ -8,7 +8,19 @@ - `./tools/smokes/v2/run.sh --profile quick` 154/154 PASS 維持 - 入口: `docs/development/current/main/phases/phase-188.3/README.md` - 次の指示書(refactor挟み込み): `docs/development/current/main/phases/phase-188.3/P2-REFACTORING-INSTRUCTIONS.md` -- 次の指示書(でかいファイル分割): `docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md` + +**2025-12-27: Phase 287 P0 完了** ✅ +- `merge/mod.rs` を modularize(1,555 → 1,053 lines) +- SSOT: `boundary.loop_header_func_name` 優先、`boundary.continuation_func_ids` で除外、`MAIN` 明示除外 +- 検証: Pattern6 RC=9 / quick 154 PASS / 恒常ログ増加なし +- 入口: `docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md` + +**2025-12-27: Phase 287 P1 完了** ✅ +- `ast_feature_extractor.rs` を facade 化(1,148 → 135 lines) +- `pattern_recognizers/`(8 modules)へ分割(1 module = 1 質問) +- 検証: Build 0 errors / Pattern6 RC=9 / quick 154 PASS / 恒常ログ増加なし +- 入口: `docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md` +- 次の指示書(P2): `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` **2025-12-27: Phase 188.2 完了** ✅ - StepTreeの `max_loop_depth` を SSOT に採用(Option A) @@ -101,7 +113,8 @@ - quick smoke 154/154 PASS 維持、Pattern1/4 PoC 両方 PASS **次のステップ**: -1. **Phase 287(optional)**: Hygiene(破壊的変更なし、既定挙動不変) +1. **Phase 287(P2)**: `contract_checks.rs` の facade 化(意味論不変) + - 指示書: `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` 2. (post self-host / docs-first)**Phase 29y**: MIR lifecycle vocab freeze(RC/weak/ABI) - 相談パケット: `docs/development/current/main/investigations/phase-29y-mir-lifecycle-vocab-consult.md` 3. (future design, separate phase)Plan 生成の正規化(相談パケット) diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 04ca1978..d1bcb57a 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -13,10 +13,11 @@ Related: - SSOT: `docs/reference/language/repl.md` - 次: Phase 288.2+(任意): REPL UX improvements(history / multiline / load 等) -- **Phase 287(✅ complete): ビルド/テスト軽量化(quick gate の責務分離)** - - 目的: `tools/smokes/v2/run.sh --profile quick` を 45秒以内(目安)へ戻し、開発サイクルを軽くする +- **Phase 287(active): Big Files Refactoring follow-ups(意味論不変)** + - 状況: P0/P1 ✅ 完了(merge modularize / ast_feature_extractor facade) + - 次(P2): `contract_checks.rs` を facade 化して契約単位で分割 - 入口: `docs/development/current/main/phases/phase-287/README.md` - - 手順: `docs/development/current/main/phases/phase-287/P1-INSTRUCTIONS.md` + - 指示書: `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` - **Phase 284(✅ COMPLETE): Return as ExitKind SSOT(patternに散らさない)** - 目的: `return` を “pattern最適化の個別実装” にせず、`ExitKind` と `compose::*` / `emit_frag()` に収束させる diff --git a/docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md index dc66b7ff..9d7450bb 100644 --- a/docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md +++ b/docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md @@ -1,6 +1,7 @@ # Phase 287 P0: Big Files Refactoring 指示書(意味論不変 / 推定の削減) **Date**: 2025-12-27 +**Status**: Complete ✅ **Scope**: Rust 側の“でかいファイル”を分割して、推定(heuristics)依存を減らす **Non-goals**: 新機能、既定挙動変更、silent fallback 追加、env var 追加 @@ -44,6 +45,11 @@ find src -name '*.rs' -print0 | xargs -0 wc -l | sort -nr | head -50 実行用の詳細プラン: - `docs/development/current/main/phases/phase-287/P0-MERGE_MOD_MODULARIZATION_PLAN.md` +結果(実装済み): +- `merge/mod.rs`: 1,555 → 1,053 lines +- 新規モジュール: `entry_selector.rs`, `header_phi_prebuild.rs`, `value_remapper.rs`, `boundary_logging.rs` +- heuristic 排除: continuation 判定は `boundary.continuation_func_ids` を SSOT に採用(文字列一致を撤去) + #### 目標構造(案) ``` @@ -52,11 +58,13 @@ src/mir/builder/control_flow/joinir/merge/ ├── entry_selector.rs # loop header / entry block 選定(SSOT) ├── header_phi_prebuild.rs # LoopHeaderPhiBuilder 呼び出し(SSOT) ├── boundary_logging.rs # verbose/debug のみ(trace統一) -└── verification/ # “契約検証” をまとめる - ├── mod.rs - └── ... (既存 contract_checks.rs を移す/薄くする) +└── debug_assertions.rs # merge 内の fail-fast / 契約検証(debug で固定) ``` +注: +- Phase 287 P0 では `verification/` の新設ではなく、既存の “契約検証” を `debug_assertions.rs` に統合した(意味論不変)。 +- 次(P2)で `contract_checks.rs` を facade 化し、契約単位のモジュール分割を行う(`contract_checks/` を新設予定)。 + #### SSOT(ここで削る推定) - loop header の推定を boundary に寄せる: @@ -95,6 +103,10 @@ src/mir/builder/control_flow/joinir/merge/ 現状: 複数の “検出器” が同居。純粋関数なので物理分割が安全。 方針: `pattern_recognizers/` を作って“1 recognizer = 1 質問”にする。 +結果(実装済み): +- `ast_feature_extractor.rs`: 1,148 → 135 lines(facade) +- `pattern_recognizers/`: 8 modules(1 module = 1 質問) + #### 目標構造(案) ``` diff --git a/docs/development/current/main/phases/phase-287/P0-MERGE_MOD_MODULARIZATION_PLAN.md b/docs/development/current/main/phases/phase-287/P0-MERGE_MOD_MODULARIZATION_PLAN.md index fe82559b..777a0d2d 100644 --- a/docs/development/current/main/phases/phase-287/P0-MERGE_MOD_MODULARIZATION_PLAN.md +++ b/docs/development/current/main/phases/phase-287/P0-MERGE_MOD_MODULARIZATION_PLAN.md @@ -1,12 +1,19 @@ # Phase 287 P0: `merge/mod.rs` Modularization Plan(意味論不変) **Date**: 2025-12-27 -**Status**: Planning Complete(Ready for Implementation) +**Status**: Historical(Implemented) ✅ **Parent**: Phase 287 (Big Files Refactoring) **Goal**: `src/mir/builder/control_flow/joinir/merge/mod.rs` を“配線だけ”に寄せる(意味論不変) --- +## Note + +この文書は実装前のプラン。Phase 287 P0 は既に完了しているので、現状の入口は以下: + +- 入口(完了ログ): `docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md` +- 次(P2): `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` + ## 前提(直近の SSOT) - Pattern6 の merge/latch 事故は SSOT 化済み @@ -120,4 +127,3 @@ cargo build --release ## リスクメモ - `merge/mod.rs` と `merge/instruction_rewriter.rs` は両方で “entry 選定” を持ちがちなので、SSOT を二重にしない(可能なら selector を共用、ただし大きく動かさない)。 - diff --git a/docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md new file mode 100644 index 00000000..ddc75f36 --- /dev/null +++ b/docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md @@ -0,0 +1,107 @@ +# Phase 287 P1: `ast_feature_extractor.rs` 分割指示書(意味論不変) + +**Date**: 2025-12-27 +**Status**: Complete ✅ +**Scope**: `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`(1,148行)を“recognizer単位”に分割 +**Non-goals**: ルーティング条件の変更、検出仕様の変更、silent fallback 追加、`merge/instruction_rewriter.rs` の大改造 + +実装: +- commit: `de1cd1fea` +- Phase hub: `docs/development/current/main/phases/phase-287/README.md` + +--- + +## 目的(SSOT) + +- “推定(heuristics)で決めているところ”を増やさず、既存検出の **契約を保ったまま** 構造(フォルダ/モジュール)で責務分離する。 +- `ast_feature_extractor.rs` を **facade**(re-export + glue)へ寄せて、個別検出は “1ファイル=1質問” にする。 + +--- + +## 背景(現状) + +- `ast_feature_extractor.rs` は純粋関数中心だが、複数の検出器が同居して巨大化している。 +- `escape_pattern_recognizer.rs` のように、既に分割できることは確認済み。 + +--- + +## 目標の構造(案) + +``` +src/mir/builder/control_flow/joinir/patterns/ +├── ast_feature_extractor.rs # facade(公開関数の入口) +└── pattern_recognizers/ # NEW + ├── mod.rs + ├── continue_break.rs # continue/break/return の単純探索 + ├── infinite_loop.rs # condition==true の判定 + ├── if_else_phi.rs # if-else phi 検出 + ├── carrier_count.rs # carrier count(代入ベース) + ├── parse_number.rs # parse_number 系の検出 + ├── parse_string.rs # parse_string 系の検出 + └── skip_whitespace.rs # skip_ws 系の検出 +``` + +注: +- 既存の `escape_pattern_recognizer.rs` はそのままでも良い(P1で無理に統合しない)。 +- “recognizer名” は **検出の質問** を表す(例: `detect_infinite_loop`、`detect_continue_pattern` など)。 + +--- + +## 実績(実装済み) + +- `ast_feature_extractor.rs` は facade 化され、既存 import の互換を維持したまま `pattern_recognizers/` へ分割済み。 +- 恒常ログ増加なし、build/quick/Pattern6 の回帰なし(意味論不変)。 + +--- + +## 進め方(安全な順序) + +### Step 1: facade化の準備(最小差分) + +- `pattern_recognizers/` を追加し、`mod.rs` を置く(空でもよい)。 +- `ast_feature_extractor.rs` から、新フォルダの関数を `pub(crate)` で re-export できる形にする。 + +### Step 2: “依存が少ない” 検出器から移す + +優先(低依存): +- `detect_continue_in_body` / `detect_break_in_body` / `detect_return_in_body` +- `detect_infinite_loop` + +ルール: +- 公開関数シグネチャは維持(呼び出し側の差分最小)。 +- `pub(crate)` API の入口は **ast_feature_extractor.rs に残す**(外部参照の破壊を避ける)。 + +### Step 3: 中依存(helper多め)を移す + +- if-else phi 系 +- carrier count 系 + +### Step 4: 個別パターン recognizer を移す(必要なら) + +- parse_number / parse_string / skip_whitespace など +- 既に抽出済みの recognizer と重複する場合は、P1では **統合しない**(P2以降で整理)。 + +--- + +## テスト(仕様固定) + +P1 は意味論不変が主目的なので “薄く” で良い: + +- 既存の unit tests があるなら移設に合わせて位置だけ更新 +- 新規追加するなら、各 recognizer に 1本まで(代表ケースのみ) + +--- + +## 検証手順(受け入れ基準) + +```bash +cargo build --release +./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako # RC=9 +./tools/smokes/v2/run.sh --profile quick +``` + +受け入れ: +- ビルド 0 errors +- quick 154/154 PASS +- Pattern6 RC=9 維持 +- 恒常ログ増加なし diff --git a/docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md new file mode 100644 index 00000000..ffd6a1da --- /dev/null +++ b/docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md @@ -0,0 +1,110 @@ +# Phase 287 P2: `merge/contract_checks.rs` 分割指示書(意味論不変) + +**Date**: 2025-12-27 +**Status**: Ready(next) +**Scope**: `src/mir/builder/control_flow/joinir/merge/contract_checks.rs`(~846行)を facade 化し、契約検証を “1 module = 1 契約” に分割 +**Non-goals**: エラータグ変更、検証条件の追加/緩和、`merge/instruction_rewriter.rs` の分割、silent fallback 追加 + +--- + +## 目的(SSOT) + +- merge の “Fail-Fast 契約” を **構造で見える化**する。 +- `contract_checks.rs` を facade(re-export + glue)に寄せ、検証ロジックを責務分離する。 +- 既存の呼び出し点を壊さない(public 関数名/シグネチャを基本維持)。 + +--- + +## 現状 + +`src/mir/builder/control_flow/joinir/merge/contract_checks.rs` は複数契約を同居させており、読む側が “どの契約がどこにあるか” を追いにくい。 + +代表的な契約(例): +- terminator target existence +- exit_bindings ↔ exit_phis +- carrier_inputs completeness +- boundary contract at creation(B1/C2) +- entry params consistency + +--- + +## 目標の構造(案) + +``` +src/mir/builder/control_flow/joinir/merge/ +├── contract_checks.rs # facade(旧名維持 / re-export) +└── contract_checks/ # NEW + ├── mod.rs + ├── terminator_targets.rs # verify_all_terminator_targets_exist + ├── exit_bindings.rs # verify_exit_bindings_have_exit_phis + ├── carrier_inputs.rs # verify_carrier_inputs_complete + ├── boundary_creation.rs # verify_boundary_contract_at_creation(B1/C2) + └── entry_params.rs # verify_boundary_entry_params +``` + +ルール: +- `contract_checks.rs` は **呼び出し側互換のための facade** に徹する。 +- 新規の “総合入口” を作るなら `contract_checks::run_all_pipeline_checks()` のみ(既存があるなら整理だけ)。 + +--- + +## 手順(安全な順序) + +### Step 1: `contract_checks/` を追加し facade を作る + +- `contract_checks/mod.rs` に各 module を `pub(super)` で生やす +- `src/mir/builder/control_flow/joinir/merge/contract_checks.rs` から `pub(super) use ...` で既存 API を re-export + +### Step 2: 低依存の契約から移す + +優先: +- `verify_all_terminator_targets_exist()`(依存が少ない) +- `verify_exit_bindings_have_exit_phis()` + +### Step 3: `verify_carrier_inputs_complete()` を移す + +- ここは “エラータグ/ヒント文” が契約なので、文言変更は避ける。 + +### Step 4: boundary creation(B1/C2)を移す + +- `verify_boundary_contract_at_creation()` は “入口で fail-fast する” という意味で重要なので、移設後も呼び出し位置を変えない。 + +### Step 5: entry params を移す + +- `verify_boundary_entry_params()` は “param 順序” の SSOT なので、テストがあるなら位置だけ追従させる。 + +--- + +## テスト(仕様固定) + +P2 は意味論不変が主目的なので、原則 “既存テストを壊さない” を優先する。 + +- 既存の `#[cfg(test)]` が `contract_checks.rs` にある場合: + - 最小差分で `contract_checks/` の該当 module へ移動 + - もしくは facade 側に残して import だけ更新(どちらでも可) + +新規テストは原則不要(既存で十分)。 + +--- + +## 検証手順(受け入れ基準) + +```bash +cargo build --release +./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako # RC=9 +./tools/smokes/v2/run.sh --profile quick +``` + +受け入れ: +- Build: 0 errors +- quick: 154/154 PASS +- Pattern6: RC=9 維持 +- 恒常ログ増加なし + +--- + +## Out of Scope(重要) + +- `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs` の物理分割 +- エラータグの変更、契約の意味変更、条件追加による挙動変化 +- “便利な fallback” の追加(Fail-Fast 原則に反するため) diff --git a/docs/development/current/main/phases/phase-287/README.md b/docs/development/current/main/phases/phase-287/README.md index c47750dd..04340801 100644 --- a/docs/development/current/main/phases/phase-287/README.md +++ b/docs/development/current/main/phases/phase-287/README.md @@ -1,14 +1,45 @@ -# Phase 287: Normalizer Hygiene(正規化器整理) +# Phase 287: Developer Hygiene(big files / smoke / normalizer) -**Status**: Planning -**Date**: 2025-12-26 +**Status**: In progress (P0/P1 complete, P2 next) +**Date**: 2025-12-27 **Previous**: Phase 286 (Plan Line完全運用化) -## 概要 +## 概要(SSOT) -Phase 286 でPlan lineへの移行が完了したため、legacy Pattern5 削除とnormalizer.rs の整理を実施。 +Phase 287 は「開発導線の整備(意味論不変)」を優先して、巨大ファイルの責務分離(big files refactoring)と、既存の hygiene(smoke/normalizer)を扱う。 -## Phase 286 完了作業(本セッション) +## 2025-12-27 Update: Big Files Refactoring(P0)✅ + +`merge/mod.rs` を modularize(意味論不変)し、SSOT(boundary-first / continuation SSOT)を強化した。 + +- 入口: `docs/development/current/main/phases/phase-287/P0-BIGFILES-REFACTORING-INSTRUCTIONS.md` + +## 2025-12-27 Update: AST Feature Extractor modularization(P1)✅ + +`ast_feature_extractor.rs` を facade にして、`pattern_recognizers/` 配下へ recognizer 単位で分割した(意味論不変)。 + +- 入口: `docs/development/current/main/phases/phase-287/P1-AST_FEATURE_EXTRACTOR-INSTRUCTIONS.md` +- 次(P2): `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` + +--- + +## Next (P2) + +- `contract_checks.rs` を facade 化して、契約検証を “1 module = 1 契約” に分割する(意味論不変)。 + - 指示書: `docs/development/current/main/phases/phase-287/P2-CONTRACT_CHECKS-MODULARIZATION-INSTRUCTIONS.md` + +--- + +## Legacy / Historical (2025-12-26 plan) + +以下は「Phase 287 を hygiene として計画していた時期のログ」。今後の候補として残すが、P0/P1(big files)とは別系統。 + +### Legacy docs(smoke quick) + +- P1(legacy): quick 軽量化(~45s 目標): `docs/development/current/main/phases/phase-287/P1-INSTRUCTIONS.md` +- P2(legacy, optional): quick をさらに 45s へ寄せる: `docs/development/current/main/phases/phase-287/P2-INSTRUCTIONS.md` + +## Phase 286 完了作業(historical) ### ✅ Legacy Pattern5 削除(488行) @@ -78,9 +109,9 @@ Pattern4 → Pattern8 → Pattern9 → Pattern3 → Pattern1 → Pattern2 **注**: Pattern5/6/7 は Plan line 経由で処理(`PLAN_EXTRACTORS` テーブル) -## Phase 287 計画(後回し) +## Legacy backlog (post-2025-12-27) -### P0: normalizer.rs 分割計画 +### (legacy) normalizer.rs 分割計画 **現状**: `src/mir/builder/control_flow/plan/normalizer.rs` が大きすぎる(推定 1,500+ 行) @@ -103,7 +134,7 @@ Pattern4 → Pattern8 → Pattern9 → Pattern3 → Pattern1 → Pattern2 - テスト分離(Pattern5 正規化のみをユニットテスト) - 責任分離(SRP原則) -### P1: LOOP_PATTERNS テーブル完全削除 +### (legacy) LOOP_PATTERNS テーブル完全削除 **背景**: 全Pattern が Plan line 経由になれば、`LOOP_PATTERNS` テーブルは不要 diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs deleted file mode 100644 index 45f4103b..00000000 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ /dev/null @@ -1,846 +0,0 @@ -use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; -use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; -use std::collections::BTreeMap; - -use super::merge_result::MergeContracts; - -/// Contract check (Fail-Fast): every Branch/Jump target must exist in the function. -/// -/// This prevents latent runtime failures like: -/// - "Invalid basic block: bb not found" -/// -/// Typical root causes: -/// - Jumping to a continuation function entry block whose blocks were intentionally skipped -/// - Allocating a block ID but forgetting to insert the block -/// -/// Phase 131 P1 Task 1: Now accepts MergeContracts instead of raw slice for SSOT visibility. -pub(super) fn verify_all_terminator_targets_exist( - func: &MirFunction, - contracts: &MergeContracts, -) -> Result<(), String> { - use crate::mir::join_ir::lowering::error_tags; - - for (block_id, block) in &func.blocks { - let Some(term) = &block.terminator else { continue }; - - match term { - MirInstruction::Jump { target, .. } => { - if !func.blocks.contains_key(target) - && !contracts.allowed_missing_jump_targets.contains(target) - { - return Err(error_tags::freeze_with_hint( - "joinir/merge/contract/missing_jump_target", - &format!( - "Jump target {:?} not found in function '{}' (from block {:?})", - target, func.signature.name, block_id - ), - "ensure merge inserts all remapped blocks and does not Jump to skipped continuation blocks (k_exit must Jump to exit_block_id)", - )); - } - } - MirInstruction::Branch { - then_bb, else_bb, .. - } => { - if !func.blocks.contains_key(then_bb) - && !contracts.allowed_missing_jump_targets.contains(then_bb) - { - return Err(error_tags::freeze_with_hint( - "joinir/merge/contract/missing_branch_target", - &format!( - "Branch then_bb {:?} not found in function '{}' (from block {:?})", - then_bb, func.signature.name, block_id - ), - "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", - )); - } - if !func.blocks.contains_key(else_bb) - && !contracts.allowed_missing_jump_targets.contains(else_bb) - { - return Err(error_tags::freeze_with_hint( - "joinir/merge/contract/missing_branch_target", - &format!( - "Branch else_bb {:?} not found in function '{}' (from block {:?})", - else_bb, func.signature.name, block_id - ), - "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", - )); - } - } - _ => {} - } - } - - Ok(()) -} - -/// Phase 118 P2: Contract check (Fail-Fast) - exit_bindings carriers must have exit PHI dsts. -/// -/// This prevents latent "Carrier '' not found in carrier_phis" failures later in -/// ExprResultResolver and ExitLine reconnection. -pub(super) fn verify_exit_bindings_have_exit_phis( - boundary: &JoinInlineBoundary, - exit_carrier_phis: &BTreeMap, -) -> Result<(), String> { - use crate::mir::join_ir::lowering::carrier_info::CarrierRole; - use crate::mir::join_ir::lowering::error_tags; - - for binding in &boundary.exit_bindings { - if binding.role == CarrierRole::ConditionOnly { - continue; - } - if exit_carrier_phis.contains_key(&binding.carrier_name) { - continue; - } - - return Err(error_tags::freeze_with_hint( - "phase118/exit_phi/missing_carrier_phi", - &format!( - "exit_bindings carrier '{}' is missing from exit_carrier_phis", - binding.carrier_name - ), - "exit_bindings carriers must be included in exit_phi_builder inputs; check carrier_inputs derivation from jump_args", - )); - } - - Ok(()) -} - -/// Phase 286C-4.2: Contract check (Fail-Fast) - carrier_inputs must include all exit_bindings. -/// -/// # Purpose -/// -/// Validates that carrier_inputs collected during plan stage contains entries for all -/// exit_bindings that require PHI generation (non-ConditionOnly carriers). -/// -/// This catches bugs where: -/// - CarrierInputsCollector fails to add a carrier due to missing header PHI -/// - plan_rewrites skips a carrier mistakenly -/// - exit_phi_builder receives incomplete carrier_inputs -/// -/// # Contract -/// -/// For each non-ConditionOnly exit_binding: -/// - `carrier_inputs[carrier_name]` must exist -/// -/// # Returns -/// - `Ok(())`: All non-ConditionOnly carriers present in carrier_inputs -/// - `Err(String)`: Contract violation with [joinir/contract:C4] tag -pub(super) fn verify_carrier_inputs_complete( - boundary: &JoinInlineBoundary, - carrier_inputs: &BTreeMap>, -) -> Result<(), String> { - use crate::mir::join_ir::lowering::carrier_info::CarrierRole; - use crate::mir::join_ir::lowering::error_tags; - - for binding in &boundary.exit_bindings { - // Skip ConditionOnly carriers (no PHI required) - if binding.role == CarrierRole::ConditionOnly { - continue; - } - - // Check carrier_inputs has entry for this binding - if !carrier_inputs.contains_key(&binding.carrier_name) { - return Err(error_tags::freeze_with_hint( - "joinir/contract:C4", - &format!( - "exit_binding carrier '{}' (role={:?}) is missing from carrier_inputs", - binding.carrier_name, binding.role - ), - "ensure CarrierInputsCollector successfully collected from header PHI or DirectValue mode; check loop_header_phi_info has PHI dst for this carrier", - )); - } - } - - Ok(()) -} - -/// Phase 286 P1: Boundary contract validation (B1/C2 invariants) -/// -/// Validates boundary structure invariants BEFORE merge begins. -/// This catches boundary construction bugs early with clear diagnostics. -/// -/// # Checks -/// - **B1**: join_inputs ValueIds are in non-colliding region (Param: 100-999) -/// - **C2**: condition_bindings join_values are in Param region -/// -/// # Returns -/// - `Ok(())`: All invariants satisfied -/// - `Err(String)`: Contract violation with [joinir/contract:B*] tag -pub(in crate::mir::builder::control_flow::joinir) fn verify_boundary_contract_at_creation( - boundary: &JoinInlineBoundary, - context: &str, -) -> Result<(), String> { - use crate::mir::join_ir::lowering::error_tags; - use crate::mir::join_ir::lowering::join_value_space::{PARAM_MIN, PARAM_MAX}; - - // Debug logging (HAKO_JOINIR_DEBUG=1) - if crate::config::env::is_joinir_debug() { - eprintln!("[joinir/boundary-contract] Validating boundary:"); - eprintln!(" join_inputs: {:?}", boundary.join_inputs); - eprintln!(" condition_bindings: {} bindings", boundary.condition_bindings.len()); - eprintln!(" exit_bindings: {} bindings", boundary.exit_bindings.len()); - - // Debug: Print file/line info - eprintln!(" [DEBUG] verify_boundary_contract_at_creation called from:"); - eprintln!(" (This should help identify which pattern is causing the issue)"); - } - - // B1: join_inputs in Param region (100-999) - for (i, join_id) in boundary.join_inputs.iter().enumerate() { - if !(PARAM_MIN..=PARAM_MAX).contains(&join_id.0) { - // Debug: Print backtrace to identify which pattern caused this - if crate::config::env::is_joinir_debug() { - eprintln!("[joinir/contract/B1] FAILED - join_inputs[{}] = ValueId({}) outside Param region", i, join_id.0); - eprintln!(" [DEBUG] This likely means alloc_local() was used instead of alloc_param() for function parameters"); - eprintln!(" [DEBUG] Check pattern lowering code for function parameter allocations"); - } - - return Err(error_tags::freeze_with_hint( - "joinir/contract/B1", - &format!( - "[{}] join_inputs[{}] = {:?} outside Param region (expected 100-999)", - context, i, join_id - ), - "use alloc_join_param() for function parameters", - )); - } - } - - // C2: condition_bindings join_values in Param region - for binding in &boundary.condition_bindings { - if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_value.0) { - return Err(error_tags::freeze_with_hint( - "joinir/contract/C2", - &format!( - "[{}] condition_binding '{}' join_value {:?} outside Param region", - context, binding.name, binding.join_value - ), - "use ConditionContext.alloc_value() for JoinIR ValueIds", - )); - } - } - - Ok(()) -} - -/// 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::*; - use crate::mir::{ - BasicBlock, BasicBlockId, FunctionSignature, MirFunction, MirInstruction, MirType, ValueId, - }; - - /// Helper: Create a minimal test function with given blocks - fn create_test_function(blocks: Vec<(BasicBlockId, Option)>) -> MirFunction { - use crate::mir::EffectMask; - - let entry_block = blocks.first().map(|(id, _)| *id).unwrap_or(BasicBlockId(0)); - - let mut func = MirFunction::new( - FunctionSignature { - name: "test_func".to_string(), - params: vec![], - return_type: MirType::Void, - effects: EffectMask::default(), - }, - entry_block, - ); - - // Remove the entry block that was auto-created - func.blocks.clear(); - - for (block_id, terminator) in blocks { - let mut block = BasicBlock::new(block_id); - block.terminator = terminator; - func.add_block(block); - } - - func - } - - #[test] - fn test_verify_all_terminator_targets_exist_all_present() { - // case 1: すべてのターゲットが存在 → OK - let bb0 = BasicBlockId(0); - let bb1 = BasicBlockId(1); - let bb2 = BasicBlockId(2); - - let func = create_test_function(vec![ - ( - bb0, - Some(MirInstruction::Jump { - target: bb1, - edge_args: None, - }), - ), - ( - bb1, - Some(MirInstruction::Branch { - condition: ValueId(0), - then_bb: bb2, - else_bb: bb2, - then_edge_args: None, - else_edge_args: None, - }), - ), - (bb2, Some(MirInstruction::Return { value: None })), - ]); - - let contracts = MergeContracts { - allowed_missing_jump_targets: vec![], - }; - - let result = verify_all_terminator_targets_exist(&func, &contracts); - assert!(result.is_ok(), "All targets exist, should pass"); - } - - #[test] - fn test_verify_all_terminator_targets_exist_missing_disallowed() { - // case 2: 許可ターゲット以外が missing → FAIL - let bb0 = BasicBlockId(0); - let bb99 = BasicBlockId(99); // Missing block - - let func = create_test_function(vec![( - bb0, - Some(MirInstruction::Jump { - target: bb99, - edge_args: None, - }), - )]); - - let contracts = MergeContracts { - allowed_missing_jump_targets: vec![], - }; - - let result = verify_all_terminator_targets_exist(&func, &contracts); - assert!(result.is_err(), "Missing disallowed target should fail"); - assert!( - result.unwrap_err().contains("Jump target"), - "Error should mention Jump target" - ); - } - - #[test] - fn test_verify_all_terminator_targets_exist_missing_allowed() { - // case 3: 許可ターゲットが missing → OK(許可) - let bb0 = BasicBlockId(0); - let bb_exit = BasicBlockId(100); // Missing but allowed - - let func = create_test_function(vec![( - bb0, - Some(MirInstruction::Jump { - target: bb_exit, - edge_args: None, - }), - )]); - - let contracts = MergeContracts { - allowed_missing_jump_targets: vec![bb_exit], - }; - - let result = verify_all_terminator_targets_exist(&func, &contracts); - assert!( - result.is_ok(), - "Missing allowed target should pass: {:?}", - result - ); - } - - #[test] - fn test_merge_contracts_creation() { - // MergeContracts の生成と値確認 - let exit_block = BasicBlockId(42); - let contracts = MergeContracts { - allowed_missing_jump_targets: vec![exit_block], - }; - - 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 - ); - } - - // ============================================================ - // Phase 286 P1: Boundary Contract Tests (B1/C2 invariants) - // ============================================================ - - #[test] - fn test_verify_boundary_contract_at_creation_valid() { - // Case 1: Valid boundary with all ValueIds in Param region → OK - let boundary = JoinInlineBoundary::new_inputs_only( - vec![ValueId(100), ValueId(101), ValueId(102)], // Valid Param region - vec![ValueId(4), ValueId(5), ValueId(6)], - ); - - let result = verify_boundary_contract_at_creation(&boundary, "test_valid"); - assert!( - result.is_ok(), - "Valid boundary should pass: {:?}", - result - ); - } - - #[test] - fn test_verify_boundary_contract_at_creation_invalid_b1_too_low() { - // Case 2: join_inputs with ValueId below Param region → FAIL with B1 tag - let mut boundary = JoinInlineBoundary::new_inputs_only( - vec![ValueId(5)], // INVALID: Below Param region (100-999) - vec![ValueId(5)], - ); - // Override join_inputs to invalid ValueId - boundary.join_inputs = vec![ValueId(5)]; - - let result = verify_boundary_contract_at_creation(&boundary, "test_b1_too_low"); - assert!(result.is_err(), "Invalid join_inputs should fail"); - - let err = result.unwrap_err(); - assert!( - err.contains("[joinir/contract/B1]"), - "Error should have B1 tag: {}", - err - ); - assert!( - err.contains("[test_b1_too_low]"), - "Error should include context: {}", - err - ); - assert!( - err.contains("outside Param region"), - "Error should explain the issue: {}", - err - ); - assert!( - err.contains("ValueId(5)"), - "Error should show invalid ValueId: {}", - err - ); - } - - #[test] - fn test_verify_boundary_contract_at_creation_invalid_b1_too_high() { - // Case 3: join_inputs with ValueId above Param region → FAIL with B1 tag - let mut boundary = JoinInlineBoundary::new_inputs_only( - vec![ValueId(1000)], // INVALID: Above Param region (100-999) - vec![ValueId(4)], - ); - boundary.join_inputs = vec![ValueId(1000)]; - - let result = verify_boundary_contract_at_creation(&boundary, "test_b1_too_high"); - assert!(result.is_err(), "Invalid join_inputs should fail"); - - let err = result.unwrap_err(); - assert!( - err.contains("[joinir/contract/B1]"), - "Error should have B1 tag: {}", - err - ); - assert!( - err.contains("[test_b1_too_high]"), - "Error should include context: {}", - err - ); - } -} - -// Phase 286C-4.2: Test helper for JoinInlineBoundary construction -#[cfg(test)] -fn make_boundary( - exit_bindings: Vec, -) -> JoinInlineBoundary { - use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode; - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; - - let mut boundary = JoinInlineBoundaryBuilder::new() - .with_inputs(vec![], vec![]) - .with_exit_bindings(exit_bindings) - .build(); - - boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; - boundary -} - -#[cfg(test)] -mod carrier_inputs_tests { - use super::*; - use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; - use crate::mir::join_ir::lowering::carrier_info::CarrierRole; - - #[test] - fn test_verify_carrier_inputs_complete_missing_carrier() { - // Setup: boundary with Accumulator carrier - let boundary = make_boundary(vec![ - LoopExitBinding { - carrier_name: "sum".to_string(), - host_slot: ValueId(10), - join_exit_value: ValueId(100), - role: CarrierRole::LoopState, - }, - ]); - - // Empty carrier_inputs (欠落シミュレート) - let carrier_inputs = BTreeMap::new(); - - // Verify: should fail - let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!(err_msg.contains("joinir/contract:C4")); - assert!(err_msg.contains("sum")); - } - - #[test] - fn test_verify_carrier_inputs_complete_condition_only_skipped() { - // Setup: ConditionOnly carrier (should be skipped) - let boundary = make_boundary(vec![ - LoopExitBinding { - carrier_name: "is_found".to_string(), - role: CarrierRole::ConditionOnly, - host_slot: ValueId(20), - join_exit_value: ValueId(101), - }, - ]); - - // Empty carrier_inputs (but OK because ConditionOnly) - let carrier_inputs = BTreeMap::new(); - - // Verify: should succeed - let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); - assert!(result.is_ok()); - } - - #[test] - fn test_verify_carrier_inputs_complete_valid() { - // Setup: LoopState carrier with inputs - let boundary = make_boundary(vec![ - LoopExitBinding { - carrier_name: "count".to_string(), - host_slot: ValueId(30), - join_exit_value: ValueId(102), - role: CarrierRole::LoopState, - }, - ]); - - let mut carrier_inputs = BTreeMap::new(); - carrier_inputs.insert( - "count".to_string(), - vec![ - (BasicBlockId(1), ValueId(100)), - (BasicBlockId(2), ValueId(200)), - ], - ); - - // Verify: should succeed - let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); - assert!(result.is_ok()); - } -} diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs new file mode 100644 index 00000000..991e41c6 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs @@ -0,0 +1,160 @@ +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; + +/// Phase 286 P1: Boundary contract validation (B1/C2 invariants) +/// +/// Validates boundary structure invariants BEFORE merge begins. +/// This catches boundary construction bugs early with clear diagnostics. +/// +/// # Checks +/// - **B1**: join_inputs ValueIds are in non-colliding region (Param: 100-999) +/// - **C2**: condition_bindings join_values are in Param region +/// +/// # Returns +/// - `Ok(())`: All invariants satisfied +/// - `Err(String)`: Contract violation with [joinir/contract:B*] tag +pub(in crate::mir::builder::control_flow::joinir) fn verify_boundary_contract_at_creation( + boundary: &JoinInlineBoundary, + context: &str, +) -> Result<(), String> { + use crate::mir::join_ir::lowering::error_tags; + use crate::mir::join_ir::lowering::join_value_space::{PARAM_MIN, PARAM_MAX}; + + // Debug logging (HAKO_JOINIR_DEBUG=1) + if crate::config::env::is_joinir_debug() { + eprintln!("[joinir/boundary-contract] Validating boundary:"); + eprintln!(" join_inputs: {:?}", boundary.join_inputs); + eprintln!(" condition_bindings: {} bindings", boundary.condition_bindings.len()); + eprintln!(" exit_bindings: {} bindings", boundary.exit_bindings.len()); + + // Debug: Print file/line info + eprintln!(" [DEBUG] verify_boundary_contract_at_creation called from:"); + eprintln!(" (This should help identify which pattern is causing the issue)"); + } + + // B1: join_inputs in Param region (100-999) + for (i, join_id) in boundary.join_inputs.iter().enumerate() { + if !(PARAM_MIN..=PARAM_MAX).contains(&join_id.0) { + // Debug: Print backtrace to identify which pattern caused this + if crate::config::env::is_joinir_debug() { + eprintln!("[joinir/contract/B1] FAILED - join_inputs[{}] = ValueId({}) outside Param region", i, join_id.0); + eprintln!(" [DEBUG] This likely means alloc_local() was used instead of alloc_param() for function parameters"); + eprintln!(" [DEBUG] Check pattern lowering code for function parameter allocations"); + } + + return Err(error_tags::freeze_with_hint( + "joinir/contract/B1", + &format!( + "[{}] join_inputs[{}] = {:?} outside Param region (expected 100-999)", + context, i, join_id + ), + "use alloc_join_param() for function parameters", + )); + } + } + + // C2: condition_bindings join_values in Param region + for binding in &boundary.condition_bindings { + if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_value.0) { + return Err(error_tags::freeze_with_hint( + "joinir/contract/C2", + &format!( + "[{}] condition_binding '{}' join_value {:?} outside Param region", + context, binding.name, binding.join_value + ), + "use ConditionContext.alloc_value() for JoinIR ValueIds", + )); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + use crate::mir::ValueId; + + #[test] + fn test_verify_boundary_contract_at_creation_valid() { + // Case 1: Valid boundary with all ValueIds in Param region → OK + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(100), ValueId(101), ValueId(102)], // Valid Param region + vec![ValueId(4), ValueId(5), ValueId(6)], + ) + .build(); + + let result = verify_boundary_contract_at_creation(&boundary, "test_valid"); + assert!( + result.is_ok(), + "Valid boundary should pass: {:?}", + result + ); + } + + #[test] + fn test_verify_boundary_contract_at_creation_invalid_b1_too_low() { + // Case 2: join_inputs with ValueId below Param region → FAIL with B1 tag + let mut boundary = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(5)], // INVALID: Below Param region (100-999) + vec![ValueId(5)], + ) + .build(); + // Override join_inputs to invalid ValueId + boundary.join_inputs = vec![ValueId(5)]; + + let result = verify_boundary_contract_at_creation(&boundary, "test_b1_too_low"); + assert!(result.is_err(), "Invalid join_inputs should fail"); + + let err = result.unwrap_err(); + assert!( + err.contains("[joinir/contract/B1]"), + "Error should have B1 tag: {}", + err + ); + assert!( + err.contains("[test_b1_too_low]"), + "Error should include context: {}", + err + ); + assert!( + err.contains("outside Param region"), + "Error should explain the issue: {}", + err + ); + assert!( + err.contains("ValueId(5)"), + "Error should show invalid ValueId: {}", + err + ); + } + + #[test] + fn test_verify_boundary_contract_at_creation_invalid_b1_too_high() { + // Case 3: join_inputs with ValueId above Param region → FAIL with B1 tag + let mut boundary = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(1000)], // INVALID: Above Param region (100-999) + vec![ValueId(4)], + ) + .build(); + boundary.join_inputs = vec![ValueId(1000)]; + + let result = verify_boundary_contract_at_creation(&boundary, "test_b1_too_high"); + assert!(result.is_err(), "Invalid join_inputs should fail"); + + let err = result.unwrap_err(); + assert!( + err.contains("[joinir/contract/B1]"), + "Error should have B1 tag: {}", + err + ); + assert!( + err.contains("[test_b1_too_high]"), + "Error should include context: {}", + err + ); + } +} diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/carrier_inputs.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/carrier_inputs.rs new file mode 100644 index 00000000..9f0ce6ae --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/carrier_inputs.rs @@ -0,0 +1,145 @@ +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; +use crate::mir::{BasicBlockId, ValueId}; +use std::collections::BTreeMap; + +/// Phase 286C-4.2: Contract check (Fail-Fast) - carrier_inputs must include all exit_bindings. +/// +/// # Purpose +/// +/// Validates that carrier_inputs collected during plan stage contains entries for all +/// exit_bindings that require PHI generation (non-ConditionOnly carriers). +/// +/// This catches bugs where: +/// - CarrierInputsCollector fails to add a carrier due to missing header PHI +/// - plan_rewrites skips a carrier mistakenly +/// - exit_phi_builder receives incomplete carrier_inputs +/// +/// # Contract +/// +/// For each non-ConditionOnly exit_binding: +/// - `carrier_inputs[carrier_name]` must exist +/// +/// # Returns +/// - `Ok(())`: All non-ConditionOnly carriers present in carrier_inputs +/// - `Err(String)`: Contract violation with [joinir/contract:C4] tag +pub(in crate::mir::builder::control_flow::joinir::merge) fn verify_carrier_inputs_complete( + boundary: &JoinInlineBoundary, + carrier_inputs: &BTreeMap>, +) -> Result<(), String> { + use crate::mir::join_ir::lowering::carrier_info::CarrierRole; + use crate::mir::join_ir::lowering::error_tags; + + for binding in &boundary.exit_bindings { + // Skip ConditionOnly carriers (no PHI required) + if binding.role == CarrierRole::ConditionOnly { + continue; + } + + // Check carrier_inputs has entry for this binding + if !carrier_inputs.contains_key(&binding.carrier_name) { + return Err(error_tags::freeze_with_hint( + "joinir/contract:C4", + &format!( + "exit_binding carrier '{}' (role={:?}) is missing from carrier_inputs", + binding.carrier_name, binding.role + ), + "ensure CarrierInputsCollector successfully collected from header PHI or DirectValue mode; check loop_header_phi_info has PHI dst for this carrier", + )); + } + } + + Ok(()) +} + +// Phase 286C-4.2: Test helper for JoinInlineBoundary construction +#[cfg(test)] +fn make_boundary( + exit_bindings: Vec, +) -> JoinInlineBoundary { + use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + + let mut boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(vec![], vec![]) + .with_exit_bindings(exit_bindings) + .build(); + + boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; + boundary +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; + use crate::mir::join_ir::lowering::carrier_info::CarrierRole; + + #[test] + fn test_verify_carrier_inputs_complete_missing_carrier() { + // Setup: boundary with Accumulator carrier + let boundary = make_boundary(vec![ + LoopExitBinding { + carrier_name: "sum".to_string(), + host_slot: ValueId(10), + join_exit_value: ValueId(100), + role: CarrierRole::LoopState, + }, + ]); + + // Empty carrier_inputs (欠落シミュレート) + let carrier_inputs = BTreeMap::new(); + + // Verify: should fail + let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("joinir/contract:C4")); + assert!(err_msg.contains("sum")); + } + + #[test] + fn test_verify_carrier_inputs_complete_condition_only_skipped() { + // Setup: ConditionOnly carrier (should be skipped) + let boundary = make_boundary(vec![ + LoopExitBinding { + carrier_name: "is_found".to_string(), + role: CarrierRole::ConditionOnly, + host_slot: ValueId(20), + join_exit_value: ValueId(101), + }, + ]); + + // Empty carrier_inputs (but OK because ConditionOnly) + let carrier_inputs = BTreeMap::new(); + + // Verify: should succeed + let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); + assert!(result.is_ok()); + } + + #[test] + fn test_verify_carrier_inputs_complete_valid() { + // Setup: LoopState carrier with inputs + let boundary = make_boundary(vec![ + LoopExitBinding { + carrier_name: "count".to_string(), + host_slot: ValueId(30), + join_exit_value: ValueId(102), + role: CarrierRole::LoopState, + }, + ]); + + let mut carrier_inputs = BTreeMap::new(); + carrier_inputs.insert( + "count".to_string(), + vec![ + (BasicBlockId(1), ValueId(100)), + (BasicBlockId(2), ValueId(200)), + ], + ); + + // Verify: should succeed + let result = verify_carrier_inputs_complete(&boundary, &carrier_inputs); + assert!(result.is_ok()); + } +} diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_params.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_params.rs new file mode 100644 index 00000000..e37fff27 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_params.rs @@ -0,0 +1,317 @@ +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; + +/// 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 +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::*; + use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinModule}; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + use crate::mir::ValueId; + + #[test] + fn test_verify_boundary_entry_params_matches() { + // Case 1: All parameters match → OK + 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 = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(100), ValueId(101), ValueId(102)], + vec![ValueId(4), ValueId(5), ValueId(6)], + ) + .build(); + + 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 + 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 = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(100), ValueId(102), ValueId(101)], // WRONG ORDER + vec![ValueId(4), ValueId(6), ValueId(5)], + ) + .build(); + + 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 + 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 = JoinInlineBoundaryBuilder::new() + .with_inputs( + vec![ValueId(100), ValueId(101)], // Only 2 inputs + vec![ValueId(4), ValueId(5)], + ) + .build(); + + 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/contract_checks/exit_bindings.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/exit_bindings.rs new file mode 100644 index 00000000..7fa8d0be --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/exit_bindings.rs @@ -0,0 +1,35 @@ +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +/// Phase 118 P2: Contract check (Fail-Fast) - exit_bindings carriers must have exit PHI dsts. +/// +/// This prevents latent "Carrier '' not found in carrier_phis" failures later in +/// ExprResultResolver and ExitLine reconnection. +pub(in crate::mir::builder::control_flow::joinir::merge) fn verify_exit_bindings_have_exit_phis( + boundary: &JoinInlineBoundary, + exit_carrier_phis: &BTreeMap, +) -> Result<(), String> { + use crate::mir::join_ir::lowering::carrier_info::CarrierRole; + use crate::mir::join_ir::lowering::error_tags; + + for binding in &boundary.exit_bindings { + if binding.role == CarrierRole::ConditionOnly { + continue; + } + if exit_carrier_phis.contains_key(&binding.carrier_name) { + continue; + } + + return Err(error_tags::freeze_with_hint( + "phase118/exit_phi/missing_carrier_phi", + &format!( + "exit_bindings carrier '{}' is missing from exit_carrier_phis", + binding.carrier_name + ), + "exit_bindings carriers must be included in exit_phi_builder inputs; check carrier_inputs derivation from jump_args", + )); + } + + Ok(()) +} diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs new file mode 100644 index 00000000..f77fe430 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs @@ -0,0 +1,30 @@ +// Phase 287 P2: Contract checks modularization +// +// This module provides contract verification functions organized by responsibility. +// Each contract is implemented in its own module for clear separation of concerns. +// +// Design principle: 1 module = 1 contract +// - terminator_targets: Branch/Jump target existence +// - exit_bindings: exit_bindings ↔ exit_phis consistency +// - carrier_inputs: carrier_inputs completeness +// - boundary_creation: B1/C2 invariants at boundary creation +// - entry_params: Entry function parameter consistency + +mod terminator_targets; +mod exit_bindings; +mod carrier_inputs; +mod boundary_creation; +mod entry_params; + +// Re-export public functions +pub(super) use terminator_targets::verify_all_terminator_targets_exist; +pub(super) use exit_bindings::verify_exit_bindings_have_exit_phis; +pub(super) use carrier_inputs::verify_carrier_inputs_complete; +pub(in crate::mir::builder::control_flow::joinir) use boundary_creation::verify_boundary_contract_at_creation; +pub(in crate::mir::builder::control_flow::joinir) use entry_params::{ + verify_boundary_entry_params, + run_all_pipeline_checks, +}; + +// Note: get_entry_function is kept internal to entry_params module +// Patterns use the version from patterns/common/joinir_helpers.rs instead diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/terminator_targets.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/terminator_targets.rs new file mode 100644 index 00000000..046a7aad --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/terminator_targets.rs @@ -0,0 +1,208 @@ +use crate::mir::{BasicBlockId, MirFunction, MirInstruction}; + +use super::super::merge_result::MergeContracts; + +/// Contract check (Fail-Fast): every Branch/Jump target must exist in the function. +/// +/// This prevents latent runtime failures like: +/// - "Invalid basic block: bb not found" +/// +/// Typical root causes: +/// - Jumping to a continuation function entry block whose blocks were intentionally skipped +/// - Allocating a block ID but forgetting to insert the block +/// +/// Phase 131 P1 Task 1: Now accepts MergeContracts instead of raw slice for SSOT visibility. +pub(in crate::mir::builder::control_flow::joinir::merge) fn verify_all_terminator_targets_exist( + func: &MirFunction, + contracts: &MergeContracts, +) -> Result<(), String> { + use crate::mir::join_ir::lowering::error_tags; + + for (block_id, block) in &func.blocks { + let Some(term) = &block.terminator else { continue }; + + match term { + MirInstruction::Jump { target, .. } => { + if !func.blocks.contains_key(target) + && !contracts.allowed_missing_jump_targets.contains(target) + { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_jump_target", + &format!( + "Jump target {:?} not found in function '{}' (from block {:?})", + target, func.signature.name, block_id + ), + "ensure merge inserts all remapped blocks and does not Jump to skipped continuation blocks (k_exit must Jump to exit_block_id)", + )); + } + } + MirInstruction::Branch { + then_bb, else_bb, .. + } => { + if !func.blocks.contains_key(then_bb) + && !contracts.allowed_missing_jump_targets.contains(then_bb) + { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_branch_target", + &format!( + "Branch then_bb {:?} not found in function '{}' (from block {:?})", + then_bb, func.signature.name, block_id + ), + "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", + )); + } + if !func.blocks.contains_key(else_bb) + && !contracts.allowed_missing_jump_targets.contains(else_bb) + { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_branch_target", + &format!( + "Branch else_bb {:?} not found in function '{}' (from block {:?})", + else_bb, func.signature.name, block_id + ), + "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", + )); + } + } + _ => {} + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::{ + BasicBlock, BasicBlockId, FunctionSignature, MirFunction, MirInstruction, MirType, ValueId, + }; + + /// Helper: Create a minimal test function with given blocks + fn create_test_function(blocks: Vec<(BasicBlockId, Option)>) -> MirFunction { + use crate::mir::EffectMask; + + let entry_block = blocks.first().map(|(id, _)| *id).unwrap_or(BasicBlockId(0)); + + let mut func = MirFunction::new( + FunctionSignature { + name: "test_func".to_string(), + params: vec![], + return_type: MirType::Void, + effects: EffectMask::default(), + }, + entry_block, + ); + + // Remove the entry block that was auto-created + func.blocks.clear(); + + for (block_id, terminator) in blocks { + let mut block = BasicBlock::new(block_id); + block.terminator = terminator; + func.add_block(block); + } + + func + } + + #[test] + fn test_verify_all_terminator_targets_exist_all_present() { + // case 1: すべてのターゲットが存在 → OK + let bb0 = BasicBlockId(0); + let bb1 = BasicBlockId(1); + let bb2 = BasicBlockId(2); + + let func = create_test_function(vec![ + ( + bb0, + Some(MirInstruction::Jump { + target: bb1, + edge_args: None, + }), + ), + ( + bb1, + Some(MirInstruction::Branch { + condition: ValueId(0), + then_bb: bb2, + else_bb: bb2, + then_edge_args: None, + else_edge_args: None, + }), + ), + (bb2, Some(MirInstruction::Return { value: None })), + ]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!(result.is_ok(), "All targets exist, should pass"); + } + + #[test] + fn test_verify_all_terminator_targets_exist_missing_disallowed() { + // case 2: 許可ターゲット以外が missing → FAIL + let bb0 = BasicBlockId(0); + let bb99 = BasicBlockId(99); // Missing block + + let func = create_test_function(vec![( + bb0, + Some(MirInstruction::Jump { + target: bb99, + edge_args: None, + }), + )]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!(result.is_err(), "Missing disallowed target should fail"); + assert!( + result.unwrap_err().contains("Jump target"), + "Error should mention Jump target" + ); + } + + #[test] + fn test_verify_all_terminator_targets_exist_missing_allowed() { + // case 3: 許可ターゲットが missing → OK(許可) + let bb0 = BasicBlockId(0); + let bb_exit = BasicBlockId(100); // Missing but allowed + + let func = create_test_function(vec![( + bb0, + Some(MirInstruction::Jump { + target: bb_exit, + edge_args: None, + }), + )]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![bb_exit], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!( + result.is_ok(), + "Missing allowed target should pass: {:?}", + result + ); + } + + #[test] + fn test_merge_contracts_creation() { + // MergeContracts の生成と値確認 + let exit_block = BasicBlockId(42); + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![exit_block], + }; + + assert_eq!(contracts.allowed_missing_jump_targets.len(), 1); + assert_eq!(contracts.allowed_missing_jump_targets[0], exit_block); + } +}