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:
2025-12-25 05:43:34 +09:00
parent 86dfa30abe
commit ad1a8bd8ec
2 changed files with 152 additions and 3 deletions

View File

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

View File

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