feat(joinir): Add carrier_inputs completeness contract (Phase 286C-4.2)
Adds Fail-Fast contract to verify carrier_inputs completeness at plan stage, preventing silent bugs where carrier collection is skipped. ## Changes 1. **contract_checks.rs**: - Added `verify_carrier_inputs_complete()` function - Checks all non-ConditionOnly carriers are present in carrier_inputs - Error tag: `[joinir/contract:C4]` for grep-friendly diagnostics - Added test helper `make_boundary()` for JoinInlineBoundary construction - Added 3 unit tests (missing carrier, ConditionOnly skip, valid case) 2. **instruction_rewriter.rs**: - Call `verify_carrier_inputs_complete()` after plan_rewrites() - Runs before apply_rewrites() for clean error state ## Contract For each non-ConditionOnly exit_binding: - `carrier_inputs[carrier_name]` must exist Catches bugs where: - CarrierInputsCollector fails to add a carrier - plan_rewrites skips a carrier mistakenly - exit_phi_builder receives incomplete carrier_inputs ## Test Results - ✅ json_lint_vm: PASS (was FAIL in 286C-4.1 before fix) - ✅ Full suite: 45/46 PASS (no regression) - ❌ core_direct_array_oob_set_rc_vm: FAIL (existing known issue) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -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<String, Vec<(BasicBlockId, ValueId)>>,
|
||||
) -> 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<crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding>) -> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user