feat(joinir): Phase 256 P1.5-P1.7 contracts and naming SSOT

This commit is contained in:
2025-12-20 06:38:21 +09:00
parent 3d09302832
commit 077657b7a1
24 changed files with 1162 additions and 63 deletions

View File

@ -48,7 +48,14 @@
## 2025-12-19Phase 256StringUtils.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` の導入は完了、Pattern6index_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-19Phase 254index_of loop pattern✅ 完了Blocked by Phase 255

View File

@ -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)

View File

@ -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.8Pattern7 の 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 APattern 7 新設)を推奨。
- Phase 255 で loop_invariants が導入されたが、このケースは invariants 以前の問題(可変ステップ)
- Phase 254-256 の流れで Pattern 6 → Pattern 7 の自然な進化が期待される
- split/tokenization は一般的なパターンなので、汎用的な解決策が望ましい
---
## 進捗P0/P1
### P0: Pattern 7 基本実装(完了)
- Fixture & smokesintegration:
- `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 6index_of回帰: PASSSelect 導入後もインフラは健全)
- `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 が収集/変換/順序のどこかで欠けていないか

View File

@ -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)

View File

@ -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
);
}
}

View File

@ -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<String> (function names)
// No need to convert with join_func_name() - use directly
let continuation_candidates: BTreeSet<String> = 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<String> = mir_module

View File

@ -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;

View File

@ -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,

View File

@ -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
};

View File

@ -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
};

View File

@ -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(),

View File

@ -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 {

View File

@ -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)))
}

View File

@ -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);
}
}

View File

@ -798,14 +798,19 @@ pub struct JoinFragmentMeta {
pub exit_meta: ExitMeta,
/// Phase 132 P1: Continuation contract (SSOT)
/// Phase 256 P1.7: Changed from BTreeSet<JoinFuncId> to BTreeSet<String>
///
/// 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<JoinFuncId>,
/// 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<String>,
}
impl JoinFragmentMeta {

View File

@ -305,13 +305,19 @@ pub struct JoinInlineBoundary {
pub carrier_info: Option<super::carrier_info::CarrierInfo>,
/// Phase 132 P1: Continuation contract (SSOT)
/// Phase 256 P1.7: Changed from BTreeSet<JoinFuncId> to BTreeSet<String>
///
/// 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<JoinFuncId>,
/// 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<String>,
/// 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<JoinFuncId> {
BTreeSet::from([JoinFuncId::new(2)])
pub fn default_continuations() -> BTreeSet<String> {
BTreeSet::from([crate::mir::join_ir::lowering::canonical_names::K_EXIT.to_string()])
}
/// Create a new boundary with input mappings only

View File

@ -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<String>) -> 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"));
}
}

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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],
);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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();