diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 7798bc0e..4ad6c5b8 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -17,7 +17,7 @@ Pattern6(1-level nested loop)の JoinIR→bridge→merge 経路で発生し - SSOT(mergeの契約): - latch_incoming を記録してよいのは `TailCallKind::BackEdge` のみ(LoopEntry は上書き禁止) - - `LoopEntry` 判定は “JoinIR main の entry block のみ” を entry-like とする(誤分類で latch が壊れるのを防止) + - entry-like 判定は “JoinIR MAIN のみ” を対象にする(block-id 推測はしない): `src/mir/builder/control_flow/joinir/merge/contract_checks/entry_like_policy.rs` - latch 二重設定は `debug_assert!` で fail-fast(回帰検知) - 変更箇所: - `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs` diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 539666a8..ee059f4f 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -1,6 +1,12 @@ # Self Current Task — Now (main) -## Current Focus: Phase 29ae (JoinIR Regression Pack) +## Current Focus: Phase 29af (Pattern2 Boundary Hygiene) + +**2025-12-29: Phase 29af P0 完了** ✅ +- 目的: Pattern2 の boundary 情報の歪みを SSOT 化し、exit/header/latch の責務境界を固定(仕様不変) +- 入口: `docs/development/current/main/phases/phase-29af/README.md` +- 変更: exit_bindings は LoopState のみ(ConditionOnly/LoopLocalZero は carrier_info→header PHI) +- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"` / `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"` PASS **2025-12-28: Phase 29ae P1 完了** ✅ - 目的: Merge/Phi Contract SSOT + 回帰パック完全固定 diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 4152a60c..0dbdbf45 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -8,6 +8,9 @@ Related: ## 直近(JoinIR/selfhost) +- **Phase 29af P0(in progress): Pattern2 Boundary Hygiene(SSOT固定)** + - 入口: `docs/development/current/main/phases/phase-29af/README.md` + - **Phase 29ae P1(✅ COMPLETE): JoinIR Regression Pack (SSOT固定)** - 入口: `docs/development/current/main/phases/phase-29ae/README.md` diff --git a/docs/development/current/main/phases/phase-29af/P1-BOUNDARY-HYGIENE-CONTRACT-CHECKS-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29af/P1-BOUNDARY-HYGIENE-CONTRACT-CHECKS-INSTRUCTIONS.md new file mode 100644 index 00000000..8a921221 --- /dev/null +++ b/docs/development/current/main/phases/phase-29af/P1-BOUNDARY-HYGIENE-CONTRACT-CHECKS-INSTRUCTIONS.md @@ -0,0 +1,66 @@ +# Phase 29af P1: Boundary Hygiene Contract Checks — Instructions + +Status: Ready for execution +Scope: JoinIR merge の contract_checks に boundary hygiene を集約(仕様不変) + +## Goal + +Phase 29af P0 で確定した boundary hygiene(Pattern2)を、merge の `contract_checks` 側でも検証できる形に収束する。 + +- upstream(Pattern2 lowerer 側)だけに Fail-Fast が散らばらないようにする +- future pattern / future refactor で boundary 構築の歪みが再発したときに、merge 入口で検知できるようにする + +## Non-goals + +- 挙動変更(release ビルド既定挙動の変更) +- env var の追加 +- fixture/smoke の増加(必要性が明確になった場合のみ) + +## Contract (SSOT) + +SSOT: `docs/development/current/main/phases/phase-29af/README.md` + +- Exit reconnection (`boundary.exit_bindings`) は LoopState のみ +- Header PHI の対象は `boundary.carrier_info` の carriers(LoopState + ConditionOnly + LoopLocalZero) +- `CarrierInit::FromHost` の `host_id=0` は契約違反(Fail-Fast) +- `exit_bindings.carrier_name` の重複は禁止(Fail-Fast) + +## Implementation Steps + +1) **contract_checks に boundary hygiene チェックを追加** + - 追加ファイル案: `src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_hygiene.rs` + - 関数案: + - `verify_boundary_hygiene(boundary: &JoinInlineBoundary) -> Result<(), String>` + - 検証項目: + - `exit_bindings` の `role` が全て LoopState であること + - `exit_bindings` の `carrier_name` が重複しないこと + - `carrier_info` がある場合: + - `CarrierInit::FromHost` の carrier は `host_id != ValueId(0)` であること + - `exit_bindings` の carrier が `carrier_info` と整合すること(最低限: 同名が存在すること) + +2) **merge 入口のチェックに配線** + - `src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs` + - boundary の概要ログの直後(Fail-Fast 位置)で `verify_boundary_hygiene()` を呼ぶ + - `src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs` + - module 宣言と re-export を追加 + +3) **docs を更新(入口と責務の明文化)** + - `docs/development/current/main/phases/phase-29af/README.md` + - P1: contract_checks に集約した旨を追記 + - `docs/development/current/main/10-Now.md` + - Phase 29af P1 の進捗(Started/Complete)に反映 + - `docs/development/current/main/30-Backlog.md` + - Phase 29af のステータスを更新 + +## Verification + +- `cargo build --release` +- `./tools/smokes/v2/run.sh --profile quick` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"` + +## Acceptance Criteria + +- 既存 smokes(quick / phase29ab_pattern2_ / phase1883_)が PASS のまま +- release ビルドでの挙動は不変(Fail-Fast は debug/strict のみ) +- boundary hygiene の契約が “merge 入口の SSOT(contract_checks)” でも検証される diff --git a/docs/development/current/main/phases/phase-29af/README.md b/docs/development/current/main/phases/phase-29af/README.md new file mode 100644 index 00000000..1ede5336 --- /dev/null +++ b/docs/development/current/main/phases/phase-29af/README.md @@ -0,0 +1,35 @@ +# Phase 29af P0: Pattern2 Boundary Hygiene (SSOT) + +Goal: Pattern2 の boundary 情報の歪みを SSOT で整理し、将来の回帰を防ぐ(仕様不変)。 + +## Boundary Contract (SSOT) + +- Header PHI 対象: + - `carrier_info` の carriers(LoopState + ConditionOnly + LoopLocalZero) +- Exit reconnection 対象: + - LoopState のみ(ConditionOnly は exit_bindings に入れない) +- Host binding 対象: + - `CarrierInit::FromHost` のみ(BoolConst / LoopLocalZero は host slot 不要) + +## Fail-Fast Rules + +- exit_bindings の `carrier_name` 重複は禁止(debug_assert) +- `CarrierInit::FromHost` の `host_id=0` は Fail-Fast + +## Entry Points + +- boundary 構築: `src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs` +- header PHI 事前構築: `src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs` +- exit_bindings 収集: `src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs` +- latch 記録: `src/mir/builder/control_flow/joinir/merge/rewriter/{tail_call_policy,latch_incoming_recorder}.rs` + +## Verification + +- `cargo build --release` +- `./tools/smokes/v2/run.sh --profile quick` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"` + +## Notes + +- Merge 側の Header PHI Entry/Latch contract は Phase 29ae で SSOT 化済み: `docs/development/current/main/phases/phase-29ae/README.md` diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_like_policy.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_like_policy.rs new file mode 100644 index 00000000..718ac68d --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/entry_like_policy.rs @@ -0,0 +1,12 @@ +//! Entry-like policy (SSOT) +//! +//! Contract: +//! - "entry-like" は MAIN のみを対象にする(by-name 以外の推測は禁止) + +use crate::mir::join_ir::lowering::canonical_names; + +pub(in crate::mir::builder::control_flow::joinir::merge) fn is_entry_like_source( + func_name: &str, +) -> bool { + func_name == canonical_names::MAIN +} 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 index 3f4ede23..3b1a2c59 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs @@ -15,11 +15,13 @@ mod exit_bindings; mod carrier_inputs; mod boundary_creation; mod entry_params; +mod entry_like_policy; // 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(super) use entry_like_policy::is_entry_like_source; 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::run_all_pipeline_checks; diff --git a/src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs b/src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs index 4dfbe103..8abd5592 100644 --- a/src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs +++ b/src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs @@ -8,7 +8,6 @@ use crate::mir::builder::MirBuilder; use crate::mir::join_ir::lowering::carrier_info::ExitMeta; use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; -use crate::mir::ValueId; // Phase 228-8: For ConditionOnly placeholder /// ExitMetaCollector: A Box that builds exit_bindings from ExitMeta /// @@ -59,17 +58,15 @@ impl ExitMetaCollector { /// 2. Create LoopExitBinding with carrier_name, join_exit_value, host_slot /// 3. Collect into Vec /// - /// # Phase 228-8: ConditionOnly carrier handling + /// # Phase 29af: Boundary hygiene /// - /// ConditionOnly carriers are included in exit_bindings even if they're not - /// in variable_ctx.variable_map, because they need latch incoming values for header PHI. - /// The host_slot is set to ValueId(0) as a placeholder since ConditionOnly - /// carriers don't participate in exit PHI. + /// ConditionOnly / LoopLocalZero carriers do not participate in exit reconnection. + /// They are excluded from exit_bindings and are handled by header PHIs via carrier_info. /// /// # Skipped carriers /// - /// Carriers not found in variable_ctx.variable_map AND not in carrier_info are silently skipped. - /// This is intentional: some carriers may not be relevant to the current pattern. + /// - ConditionOnly / LoopLocalZero carriers are excluded from exit_bindings. + /// - Other carriers not found in variable_ctx.variable_map are skipped (or strict-fail). /// /// # Logging /// @@ -148,8 +145,6 @@ impl ExitMetaCollector { bindings.push(binding); } else { - // Phase 228-8: Check if this is a ConditionOnly carrier - // Phase 247-EX: Also check if this is a FromHost carrier (e.g., digit_value) use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; let carrier_meta = if let Some(ci) = carrier_info { ci.carriers @@ -161,80 +156,34 @@ impl ExitMetaCollector { }; match carrier_meta { - Some((CarrierRole::ConditionOnly, _)) => { - // Phase 228-8: Include ConditionOnly carrier in exit_bindings - // (needed for latch incoming, not for exit PHI) - let binding = LoopExitBinding { - carrier_name: carrier_name.clone(), - join_exit_value: *join_exit_value, - host_slot: ValueId(0), // Placeholder - not used for ConditionOnly - role: CarrierRole::ConditionOnly, - }; - + Some((CarrierRole::ConditionOnly, _)) + | Some((CarrierRole::LoopState, CarrierInit::LoopLocalZero)) => { if verbose { trace.emit_if( "exit-line", "collector", &format!( - "collected ConditionOnly carrier '{}' JoinIR {:?} (not in variable_ctx.variable_map)", + "skipping non-exit carrier '{}' JoinIR {:?} (ConditionOnly/LoopLocalZero)", carrier_name, join_exit_value ), true, ); } - - bindings.push(binding); } Some((CarrierRole::LoopState, CarrierInit::FromHost)) => { - // Phase 247-EX: Include FromHost carrier in exit_bindings - // (needed for latch incoming, not for exit PHI or variable_ctx.variable_map) - let binding = LoopExitBinding { - carrier_name: carrier_name.clone(), - join_exit_value: *join_exit_value, - host_slot: ValueId(0), // Placeholder - not used for FromHost - role: CarrierRole::LoopState, - }; - - if verbose { - trace.emit_if( - "exit-line", - "collector", - &format!( - "collected FromHost carrier '{}' JoinIR {:?} (not in variable_ctx.variable_map)", - carrier_name, join_exit_value - ), - true, - ); + let msg = format!( + "[joinir/exit-line] carrier '{}' missing host slot for FromHost", + carrier_name + ); + if strict { + panic!("{}", msg); + } else if verbose { + trace.emit_if("exit-line", "collector", &msg, true); } - - bindings.push(binding); - } - Some((CarrierRole::LoopState, CarrierInit::LoopLocalZero)) => { - // Loop-local derived carrier: include binding with placeholder host slot - let binding = LoopExitBinding { - carrier_name: carrier_name.clone(), - join_exit_value: *join_exit_value, - host_slot: ValueId(0), // No host slot; used for latch/PHI only - role: CarrierRole::LoopState, - }; - - if verbose { - trace.emit_if( - "exit-line", - "collector", - &format!( - "collected loop-local carrier '{}' JoinIR {:?} (no host slot)", - carrier_name, join_exit_value - ), - true, - ); - } - - bindings.push(binding); } _ => { let msg = format!( - "[joinir/exit-line] carrier '{}' not in variable_ctx.variable_map and not ConditionOnly/FromHost (skip)", + "[joinir/exit-line] carrier '{}' not in variable_ctx.variable_map (skip)", carrier_name ); if strict { diff --git a/src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs b/src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs index e26fdd27..1904c258 100644 --- a/src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs +++ b/src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs @@ -14,7 +14,7 @@ use super::{entry_selector, LoopHeaderPhiBuilder, LoopHeaderPhiInfo}; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; use crate::mir::{BasicBlockId, MirModule, ValueId}; -use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::collections::{BTreeMap, HashSet}; /// Pre-build loop header PHIs and reserve ValueIds /// @@ -81,12 +81,6 @@ pub(super) fn prebuild_header_phis( }; // Extract carriers with their initialization strategy - let exit_carrier_names: BTreeSet<&str> = boundary - .exit_bindings - .iter() - .map(|b| b.carrier_name.as_str()) - .collect(); - let other_carriers: Vec<( String, ValueId, @@ -97,7 +91,6 @@ pub(super) fn prebuild_header_phis( .carriers .iter() .filter(|c| c.name != *loop_var_name) - .filter(|c| exit_carrier_names.contains(c.name.as_str())) .map(|c| (c.name.clone(), c.host_id, c.init, c.role)) .collect() } else { diff --git a/src/mir/builder/control_flow/joinir/merge/rewriter/latch_incoming_recorder.rs b/src/mir/builder/control_flow/joinir/merge/rewriter/latch_incoming_recorder.rs index 3254d47a..77967018 100644 --- a/src/mir/builder/control_flow/joinir/merge/rewriter/latch_incoming_recorder.rs +++ b/src/mir/builder/control_flow/joinir/merge/rewriter/latch_incoming_recorder.rs @@ -62,16 +62,28 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge( // Other carriers (excluding loop_var) let mut carrier_arg_idx = if boundary.loop_var_name.is_some() { 1 } else { 0 }; - for binding in boundary.exit_bindings.iter() { - if let Some(ref loop_var) = boundary.loop_var_name { - if &binding.carrier_name == loop_var { + if let Some(ref carrier_info) = boundary.carrier_info { + for carrier in carrier_info.carriers.iter() { + if boundary.loop_var_name.as_deref() == Some(carrier.name.as_str()) { continue; } + if let Some(&latch_value) = args.get(carrier_arg_idx) { + loop_header_phi_info.set_latch_incoming(&carrier.name, new_block_id, latch_value); + carrier_arg_idx += 1; + } } + } else { + for binding in boundary.exit_bindings.iter() { + if let Some(ref loop_var) = boundary.loop_var_name { + if &binding.carrier_name == loop_var { + continue; + } + } - if let Some(&latch_value) = args.get(carrier_arg_idx) { - loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value); - carrier_arg_idx += 1; + if let Some(&latch_value) = args.get(carrier_arg_idx) { + loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value); + carrier_arg_idx += 1; + } } } diff --git a/src/mir/builder/control_flow/joinir/merge/rewriter/tail_call_policy.rs b/src/mir/builder/control_flow/joinir/merge/rewriter/tail_call_policy.rs index 343bea2e..b6183c5e 100644 --- a/src/mir/builder/control_flow/joinir/merge/rewriter/tail_call_policy.rs +++ b/src/mir/builder/control_flow/joinir/merge/rewriter/tail_call_policy.rs @@ -4,20 +4,20 @@ //! - Identify entry-like source blocks (LoopEntry vs BackEdge) //! - Record latch incoming in one place for BackEdge +use crate::mir::builder::control_flow::joinir::merge::contract_checks::is_entry_like_source; use crate::mir::builder::control_flow::joinir::merge::loop_header_phi_info::LoopHeaderPhiInfo; use crate::mir::builder::control_flow::joinir::merge::rewriter::latch_incoming_recorder; use crate::mir::builder::control_flow::joinir::merge::tail_call_classifier::TailCallKind; -use crate::mir::join_ir::lowering::canonical_names; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; use crate::mir::{BasicBlock, BasicBlockId, MirInstruction, ValueId}; -/// Entry-like source is only MAIN's entry block. +/// Entry-like source is MAIN's blocks. pub(super) fn is_loop_entry_source( func_name: &str, - old_block_id: BasicBlockId, - func_entry_block: BasicBlockId, + _old_block_id: BasicBlockId, + _func_entry_block: BasicBlockId, ) -> bool { - func_name == canonical_names::MAIN && old_block_id == func_entry_block + is_entry_like_source(func_name) } /// Record latch incoming for BackEdge using copies in the rewritten block. @@ -38,7 +38,31 @@ pub(super) fn record_latch_incoming_if_backedge( let mut latch_args: Vec = Vec::new(); let mut loop_var_updated = false; - for (idx, carrier_name) in loop_header_phi_info.carrier_order.iter().enumerate() { + let mut ordered_carriers: Vec<&str> = Vec::new(); + let mut other_phi_dsts: std::collections::BTreeSet = + std::collections::BTreeSet::new(); + if let Some(loop_var) = boundary.loop_var_name.as_deref() { + ordered_carriers.push(loop_var); + for (name, entry) in loop_header_phi_info.carrier_phis.iter() { + if name.as_str() != loop_var { + other_phi_dsts.insert(entry.phi_dst); + } + } + } + if let Some(ref carrier_info) = boundary.carrier_info { + for carrier in carrier_info.carriers.iter() { + ordered_carriers.push(carrier.name.as_str()); + } + } else { + for binding in boundary.exit_bindings.iter() { + if boundary.loop_var_name.as_deref() == Some(binding.carrier_name.as_str()) { + continue; + } + ordered_carriers.push(binding.carrier_name.as_str()); + } + } + + for (idx, carrier_name) in ordered_carriers.iter().enumerate() { let phi_dst = match loop_header_phi_info.get_carrier_phi(carrier_name) { Some(dst) => dst, None => continue, @@ -50,8 +74,8 @@ pub(super) fn record_latch_incoming_if_backedge( if *dst == phi_dst { chosen = Some(*src); if *src != *dst { - if boundary.loop_var_name.as_deref() == Some(carrier_name) { - loop_var_updated = true; + if boundary.loop_var_name.as_deref() == Some(*carrier_name) { + loop_var_updated = !other_phi_dsts.contains(src); } break; } @@ -62,7 +86,10 @@ pub(super) fn record_latch_incoming_if_backedge( if let Some(val) = chosen { latch_args.push(val); } else if let Some(arg) = args.get(idx) { - if boundary.loop_var_name.as_deref() == Some(carrier_name) && *arg != phi_dst { + if boundary.loop_var_name.as_deref() == Some(*carrier_name) + && *arg != phi_dst + && !other_phi_dsts.contains(arg) + { loop_var_updated = true; } latch_args.push(*arg); diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md b/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md index 0d119448..2a83ba29 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md @@ -27,6 +27,14 @@ - `CarrierInit::BoolConst(_)` / `CarrierInit::LoopLocalZero` -> host binding is skipped - ConditionOnly carriers must not use `FromHost` +## Boundary hygiene (Phase 29af) +- Header PHI 対象: `carrier_info` の carriers(LoopState + ConditionOnly + LoopLocalZero) +- Exit reconnection 対象: LoopState のみ(ConditionOnly は exit_bindings に入れない) +- Host binding 対象: `CarrierInit::FromHost` のみ(BoolConst / LoopLocalZero は host slot 不要) +- Fail-Fast: exit_bindings の `carrier_name` 重複は禁止(debug_assert) +- Fail-Fast: `CarrierInit::FromHost` が `host_id=0` の場合は Err +- SSOT: `docs/development/current/main/phases/phase-29af/README.md` + ## Out of scope - multiple breaks / continue / return in the loop body - reassigned LoopBodyLocal outside the derived-slot shape diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs index c7b8007f..4b969af5 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs @@ -66,7 +66,25 @@ impl EmitJoinIRStepBox { let exit_meta = &fragment_meta.exit_meta; use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector; - let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug); + let mut exit_bindings = + ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug); + // Phase 29af P0: Exit reconnection targets LoopState only. + exit_bindings.retain(|binding| { + binding.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState + }); + // Phase 29af P0: Reject duplicate carrier_name in exit_bindings. + #[cfg(debug_assertions)] + { + use std::collections::HashSet; + let mut seen = HashSet::new(); + for binding in &exit_bindings { + debug_assert!( + seen.insert(&binding.carrier_name), + "Phase 29af Fail-Fast: duplicate exit_binding carrier '{}'", + binding.carrier_name + ); + } + } // Phase 256.8.5: Use JoinModule.entry.params as SSOT (no hardcoded ValueIds) use super::super::common::get_entry_function; @@ -78,7 +96,22 @@ impl EmitJoinIRStepBox { // Build host_input_values in same order (loop_var + carriers) let mut host_input_values = vec![inputs.loop_var_id]; for carrier in inputs.carrier_info.carriers.iter() { - host_input_values.push(carrier.host_id); + use super::super::common::{decide_carrier_binding_policy, CarrierBindingPolicy}; + match decide_carrier_binding_policy(carrier) { + CarrierBindingPolicy::BindFromHost => { + if carrier.host_id == crate::mir::ValueId(0) { + return Err(format!( + "[emit_joinir] Phase 29af Fail-Fast: FromHost carrier '{}' has host_id=0", + carrier.name + )); + } + host_input_values.push(carrier.host_id); + } + CarrierBindingPolicy::SkipBinding => { + // Placeholder: SkipBinding does not require a host slot. + host_input_values.push(crate::mir::ValueId(0)); + } + } } // Verify count consistency (fail-fast) diff --git a/src/mir/builder/joinir_inline_boundary_injector.rs b/src/mir/builder/joinir_inline_boundary_injector.rs index aaeae4b2..74612d02 100644 --- a/src/mir/builder/joinir_inline_boundary_injector.rs +++ b/src/mir/builder/joinir_inline_boundary_injector.rs @@ -36,8 +36,8 @@ impl BoundaryInjector { /// /// # Phase 33-20: All Carriers Header PHI Support /// - /// When `boundary.loop_var_name` is set, ALL carriers (loop var + other carriers - /// from exit_bindings) are handled by header PHIs. We skip ALL join_inputs + /// When `boundary.loop_var_name` is set, ALL carriers (loop var + carrier_info carriers) + /// are handled by header PHIs. We skip ALL join_inputs /// Copy instructions to avoid overwriting the PHI results. /// /// # Phase 177-3: PHI Collision Avoidance (Option B) @@ -62,7 +62,7 @@ impl BoundaryInjector { ) -> Result, String> { // Phase 222.5-E: HashMap → BTreeMap for determinism // Phase 33-20: When loop_var_name is set, ALL join_inputs are handled by header PHIs - // This includes the loop variable AND all other carriers from exit_bindings. + // This includes the loop variable AND all other carriers from carrier_info. // We skip ALL join_inputs Copy instructions, only condition_bindings remain. let skip_all_join_inputs = boundary.loop_var_name.is_some();