diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs index 20a7bc97..f6cd6b7e 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs @@ -1,5 +1,5 @@ use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; -use crate::mir::{MirFunction, MirInstruction, ValueId}; +use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; use std::collections::BTreeMap; use super::merge_result::MergeContracts; @@ -7,8 +7,6 @@ use super::merge_result::MergeContracts; #[cfg(debug_assertions)] use super::LoopHeaderPhiInfo; #[cfg(debug_assertions)] -use crate::mir::BasicBlockId; -#[cfg(debug_assertions)] use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN}; #[cfg(debug_assertions)] use std::collections::HashMap; @@ -114,6 +112,55 @@ pub(super) fn verify_exit_bindings_have_exit_phis( 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(()) +} + #[cfg(debug_assertions)] pub(super) fn verify_loop_header_phis( func: &MirFunction, @@ -1095,3 +1142,98 @@ mod tests { ); } } + +// 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; + + JoinInlineBoundary { + join_inputs: vec![], + host_inputs: vec![], + join_outputs: vec![], + #[allow(deprecated)] + host_outputs: vec![], + loop_invariants: vec![], + exit_bindings, + loop_var_name: None, + continuation_function_ids: vec![], + exit_reconnect_mode: ExitReconnectMode::DirectValue, + } +} + +#[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(), + role: CarrierRole::Accumulator, + host_slot: ValueId(10), + join_exit_value: ValueId(100), + }, + ]); + + // 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: Accumulator carrier with inputs + let boundary = make_boundary(vec![ + LoopExitBinding { + carrier_name: "count".to_string(), + role: CarrierRole::Accumulator, + host_slot: ValueId(30), + join_exit_value: ValueId(102), + }, + ]); + + 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/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 0050bf4a..0e77166d 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -90,6 +90,8 @@ use super::rewriter::scan_box::{RewritePlan, TailCallRewrite, ReturnConversion}; use super::rewriter::plan_box::RewrittenBlocks; // Phase 286C-5 Step 1: Import CarrierInputsCollector for DRY use super::rewriter::carrier_inputs_collector::CarrierInputsCollector; +// Phase 286C-4.2: Import contract_checks for carrier_inputs verification +use super::contract_checks; /// Stage 1: Scan - Read-only analysis to identify what needs rewriting /// @@ -1226,6 +1228,11 @@ pub(super) fn merge_and_rewrite( ); } + // Phase 286C-4.2: Verify carrier_inputs completeness (Fail-Fast) + if let Some(b) = boundary { + contract_checks::verify_carrier_inputs_complete(b, &blocks.carrier_inputs)?; + } + // ===== STAGE 3: APPLY (Mutate builder) ===== if debug { log!(