refactor(joinir): Split contract_checks.rs into 2 files (Phase 286C-4.3)
Split 1,239-line contract_checks.rs into responsibility-based modules
for better maintainability and clarity.
## Changes
1. **New file: debug_assertions.rs** (440 lines)
- 6 debug-only verification functions (panic! on violation)
- All functions guarded with #[cfg(debug_assertions)]
- Excluded from release builds
- Functions:
* verify_loop_header_phis()
* verify_exit_line()
* verify_exit_phi_no_collision()
* verify_valueid_regions()
* verify_condition_bindings_consistent()
* verify_header_phi_dsts_not_redefined()
2. **Updated: contract_checks.rs** (1,239 → 848 lines, -391 lines)
- Kept 6 Fail-Fast functions (Result<(), String>)
- Kept all 13 unit tests
- Removed debug-only functions and imports
3. **Updated: mod.rs**
- Added `mod debug_assertions;` declaration
## Responsibility Split
- **contract_checks.rs**: Fail-Fast contracts (production)
- Return errors with diagnostic messages
- Run in both debug and release builds
- **debug_assertions.rs**: Debug-only assertions (development)
- Panic on contract violations
- Excluded from release builds (#[cfg(debug_assertions)])
## Benefits
- Single Responsibility Principle (each file <850 lines)
- Clear separation: Fail-Fast vs Debug-only
- Improved maintainability (localized changes)
- Better build performance (debug code stripped in release)
## Test Results
- ✅ Build: 0 errors
- ✅ Smoke tests: 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:
@ -4,13 +4,6 @@ use std::collections::BTreeMap;
|
||||
|
||||
use super::merge_result::MergeContracts;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use super::LoopHeaderPhiInfo;
|
||||
#[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;
|
||||
|
||||
/// Contract check (Fail-Fast): every Branch/Jump target must exist in the function.
|
||||
///
|
||||
/// This prevents latent runtime failures like:
|
||||
@ -161,390 +154,6 @@ pub(super) fn verify_carrier_inputs_complete(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_loop_header_phis(
|
||||
func: &MirFunction,
|
||||
header_block: BasicBlockId,
|
||||
loop_info: &LoopHeaderPhiInfo,
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
if let Some(ref loop_var_name) = boundary.loop_var_name {
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let has_loop_var_phi = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|instr| matches!(instr, MirInstruction::Phi { .. }));
|
||||
|
||||
if !has_loop_var_phi && !loop_info.carrier_phis.is_empty() {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Loop variable '{}' in boundary but no PHI in header block {} (has {} carrier PHIs)",
|
||||
loop_var_name, header_block.0, loop_info.carrier_phis.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !loop_info.carrier_phis.is_empty() {
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let phi_count = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
.filter(|instr| matches!(instr, MirInstruction::Phi { .. }))
|
||||
.count();
|
||||
|
||||
if phi_count == 0 {
|
||||
panic!(
|
||||
"[JoinIRVerifier] LoopHeaderPhiInfo has {} PHIs but header block {} has none",
|
||||
loop_info.carrier_phis.len(),
|
||||
header_block.0
|
||||
);
|
||||
}
|
||||
|
||||
for (carrier_name, entry) in &loop_info.carrier_phis {
|
||||
let phi_exists = header_block_data.instructions.iter().any(|instr| {
|
||||
if let MirInstruction::Phi { dst, .. } = instr {
|
||||
*dst == entry.phi_dst
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !phi_exists {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Carrier '{}' has PHI dst {:?} but PHI not found in header block {}",
|
||||
carrier_name, entry.phi_dst, header_block.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_exit_line(
|
||||
func: &MirFunction,
|
||||
exit_block: BasicBlockId,
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
if !func.blocks.contains_key(&exit_block) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit block {} out of range (func has {} blocks)",
|
||||
exit_block.0,
|
||||
func.blocks.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !boundary.exit_bindings.is_empty() {
|
||||
for binding in &boundary.exit_bindings {
|
||||
if binding.host_slot.0 >= 1_000_000 {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit binding '{}' has suspiciously large host_slot {:?}",
|
||||
binding.carrier_name, binding.host_slot
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 132-P2: Verify exit PHI ValueIds don't collide with other instructions
|
||||
verify_exit_phi_no_collision(func, exit_block);
|
||||
}
|
||||
|
||||
/// Phase 132-P2: Verify exit PHI dst ValueIds don't collide with other instructions
|
||||
///
|
||||
/// # Problem
|
||||
///
|
||||
/// If exit_phi_builder uses builder.value_gen.next() (module-level) instead of
|
||||
/// func.next_value_id() (function-level), it can allocate ValueIds that collide
|
||||
/// with existing instructions in the function.
|
||||
///
|
||||
/// Example collision:
|
||||
/// - bb0: %1 = const 0 (counter init)
|
||||
/// - bb3: %1 = phi ... (exit PHI - collision!)
|
||||
///
|
||||
/// This causes LLVM backend errors:
|
||||
/// "Cannot overwrite PHI dst=1. ValueId namespace collision detected."
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// All exit PHI dst ValueIds must be unique within the function and not
|
||||
/// overwrite any existing instruction dst.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any exit PHI dst collides with an existing instruction dst.
|
||||
#[cfg(debug_assertions)]
|
||||
fn verify_exit_phi_no_collision(func: &MirFunction, exit_block: BasicBlockId) {
|
||||
let exit_block_data = match func.blocks.get(&exit_block) {
|
||||
Some(block) => block,
|
||||
None => return, // Block not found, other verification will catch this
|
||||
};
|
||||
|
||||
// Collect all exit PHI dsts
|
||||
let mut exit_phi_dsts = std::collections::HashSet::new();
|
||||
for instr in &exit_block_data.instructions {
|
||||
if let MirInstruction::Phi { dst, .. } = instr {
|
||||
exit_phi_dsts.insert(*dst);
|
||||
}
|
||||
}
|
||||
|
||||
if exit_phi_dsts.is_empty() {
|
||||
return; // No exit PHIs, nothing to verify
|
||||
}
|
||||
|
||||
// Collect all instruction dsts in the entire function (excluding PHIs)
|
||||
let mut all_non_phi_dsts = std::collections::HashSet::new();
|
||||
for (block_id, block) in &func.blocks {
|
||||
if *block_id == exit_block {
|
||||
// For exit block, only check non-PHI instructions
|
||||
for instr in &block.instructions {
|
||||
if !matches!(instr, MirInstruction::Phi { .. }) {
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
all_non_phi_dsts.insert(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other blocks, check all instructions
|
||||
for instr in &block.instructions {
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
all_non_phi_dsts.insert(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for collisions
|
||||
for phi_dst in &exit_phi_dsts {
|
||||
if all_non_phi_dsts.contains(phi_dst) {
|
||||
// Find which instruction collides
|
||||
for (block_id, block) in &func.blocks {
|
||||
for instr in &block.instructions {
|
||||
if matches!(instr, MirInstruction::Phi { .. }) && *block_id == exit_block {
|
||||
continue; // Skip exit PHIs themselves
|
||||
}
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
if dst == *phi_dst {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase132-P2] Exit PHI dst {:?} collides with instruction in block {}: {:?}\n\
|
||||
This indicates exit_phi_builder used module-level value_gen.next() instead of function-level next_value_id().\n\
|
||||
Fix: Use func.next_value_id() in exit_phi_builder.rs",
|
||||
phi_dst, block_id.0, instr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Extract dst ValueId from MirInstruction
|
||||
#[cfg(debug_assertions)]
|
||||
fn get_instruction_dst(instr: &MirInstruction) -> Option<ValueId> {
|
||||
use MirInstruction;
|
||||
match instr {
|
||||
MirInstruction::Const { dst, .. }
|
||||
| MirInstruction::Load { dst, .. }
|
||||
| MirInstruction::UnaryOp { dst, .. }
|
||||
| MirInstruction::BinOp { dst, .. }
|
||||
| MirInstruction::Compare { dst, .. }
|
||||
| MirInstruction::TypeOp { dst, .. }
|
||||
| MirInstruction::NewBox { dst, .. }
|
||||
| MirInstruction::NewClosure { dst, .. }
|
||||
| MirInstruction::Copy { dst, .. }
|
||||
| MirInstruction::Cast { dst, .. }
|
||||
| MirInstruction::TypeCheck { dst, .. }
|
||||
| MirInstruction::Phi { dst, .. }
|
||||
| MirInstruction::ArrayGet { dst, .. }
|
||||
| MirInstruction::RefNew { dst, .. }
|
||||
| MirInstruction::RefGet { dst, .. }
|
||||
| MirInstruction::WeakNew { dst, .. }
|
||||
| MirInstruction::WeakLoad { dst, .. }
|
||||
| MirInstruction::WeakRef { dst, .. }
|
||||
| MirInstruction::FutureNew { dst, .. }
|
||||
| MirInstruction::Await { dst, .. } => Some(*dst),
|
||||
MirInstruction::BoxCall { dst, .. }
|
||||
| MirInstruction::ExternCall { dst, .. }
|
||||
| MirInstruction::Call { dst, .. }
|
||||
| MirInstruction::PluginInvoke { dst, .. } => *dst,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_valueid_regions(loop_info: &LoopHeaderPhiInfo, boundary: &JoinInlineBoundary) {
|
||||
fn region_name(id: ValueId) -> &'static str {
|
||||
if id.0 < PARAM_MIN {
|
||||
"PHI Reserved"
|
||||
} else if id.0 <= PARAM_MAX {
|
||||
"Param"
|
||||
} else if id.0 <= LOCAL_MAX {
|
||||
"Local"
|
||||
} else {
|
||||
"Invalid (> LOCAL_MAX)"
|
||||
}
|
||||
}
|
||||
|
||||
for join_id in &boundary.join_inputs {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&join_id.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] join_input {:?} not in Param region ({})",
|
||||
join_id,
|
||||
region_name(*join_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, entry) in &loop_info.carrier_phis {
|
||||
if entry.phi_dst.0 > LOCAL_MAX {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Carrier PHI dst {:?} outside Local region ({})",
|
||||
entry.phi_dst,
|
||||
region_name(entry.phi_dst)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for binding in &boundary.condition_bindings {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_value.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Condition binding '{}' join_value {:?} not in Param region ({})",
|
||||
binding.name,
|
||||
binding.join_value,
|
||||
region_name(binding.join_value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for binding in &boundary.exit_bindings {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_exit_value.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit binding '{}' join_exit_value {:?} not in Param region ({})",
|
||||
binding.carrier_name,
|
||||
binding.join_exit_value,
|
||||
region_name(binding.join_exit_value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 135 P1 Step 1: Verify condition_bindings consistency (alias allowed, conflict fails)
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// condition_bindings can have multiple names (aliases) pointing to the same join_value,
|
||||
/// but if the same join_value appears with different host_value, it's a contract violation.
|
||||
///
|
||||
/// This catches merge-time inconsistencies before BoundaryInjector tries to inject Copy
|
||||
/// instructions, preventing MIR SSA breakage.
|
||||
///
|
||||
/// # Example Valid (alias):
|
||||
/// ```text
|
||||
/// condition_bindings: [
|
||||
/// { name: "is_char_match", join_value: ValueId(104), host_value: ValueId(12) },
|
||||
/// { name: "char", join_value: ValueId(104), host_value: ValueId(12) } // Same host_value - OK
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Example Invalid (conflict):
|
||||
/// ```text
|
||||
/// condition_bindings: [
|
||||
/// { name: "is_char_match", join_value: ValueId(104), host_value: ValueId(12) },
|
||||
/// { name: "char", join_value: ValueId(104), host_value: ValueId(18) } // Different host_value - FAIL
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the same join_value has conflicting host_value mappings.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_condition_bindings_consistent(boundary: &JoinInlineBoundary) {
|
||||
let mut join_to_host: HashMap<ValueId, ValueId> = HashMap::new();
|
||||
|
||||
for binding in &boundary.condition_bindings {
|
||||
if let Some(&existing_host) = join_to_host.get(&binding.join_value) {
|
||||
if existing_host != binding.host_value {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase135-P1] condition_bindings conflict: join_value {:?} mapped to both {:?} and {:?}\n\
|
||||
Binding names with conflict: check all bindings with join_value={:?}\n\
|
||||
Contract: Same join_value can have multiple names (alias) but must have same host_value.\n\
|
||||
Fix: Ensure ConditionLoweringBox uses SSOT allocator (ConditionContext.alloc_value).",
|
||||
binding.join_value, existing_host, binding.host_value, binding.join_value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
join_to_host.insert(binding.join_value, binding.host_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 135 P1 Step 2: Verify header PHI dsts are not redefined by non-PHI instructions
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// Loop header PHI dst ValueIds must not be reused as dst in non-PHI instructions.
|
||||
/// This prevents "PHI dst overwrite" where a Copy/BinOp/etc. instruction redefines
|
||||
/// the PHI result, breaking MIR SSA.
|
||||
///
|
||||
/// # Example Invalid:
|
||||
/// ```text
|
||||
/// bb3 (header):
|
||||
/// %14 = phi [%2, bb1], [%28, bb8] // Header PHI
|
||||
/// %16 = copy %0
|
||||
/// %14 = call %16.length() // INVALID: Redefines PHI dst %14
|
||||
/// ```
|
||||
///
|
||||
/// This typically happens when:
|
||||
/// - ConditionLoweringBox bypasses SSOT allocator and reuses PHI dst ValueIds
|
||||
/// - JoinIR merge incorrectly remaps values to PHI dst range
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any header PHI dst is redefined by a non-PHI instruction in the function.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_header_phi_dsts_not_redefined(
|
||||
func: &MirFunction,
|
||||
header_block: BasicBlockId,
|
||||
phi_dsts: &std::collections::HashSet<ValueId>,
|
||||
) {
|
||||
if phi_dsts.is_empty() {
|
||||
return; // No PHI dsts to protect
|
||||
}
|
||||
|
||||
// Check all blocks for non-PHI instructions that redefine PHI dsts
|
||||
for (block_id, block) in &func.blocks {
|
||||
for instr in &block.instructions {
|
||||
// Skip PHIs in header block (they're the definitions we're protecting)
|
||||
if *block_id == header_block && matches!(instr, MirInstruction::Phi { .. }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this instruction redefines a PHI dst
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
if phi_dsts.contains(&dst) {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase135-P1] Header PHI dst {:?} redefined by non-PHI instruction in block {}:\n\
|
||||
Instruction: {:?}\n\
|
||||
Contract: Header PHI dsts must not be reused as dst in other instructions.\n\
|
||||
Fix: Ensure ConditionLoweringBox uses SSOT allocator (ConditionContext.alloc_value) to avoid ValueId collisions.",
|
||||
dst, block_id.0, instr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 286 P1: Boundary contract validation (B1/C2 invariants)
|
||||
///
|
||||
/// Validates boundary structure invariants BEFORE merge begins.
|
||||
|
||||
440
src/mir/builder/control_flow/joinir/merge/debug_assertions.rs
Normal file
440
src/mir/builder/control_flow/joinir/merge/debug_assertions.rs
Normal file
@ -0,0 +1,440 @@
|
||||
//! JoinIR Debug Assertions (Phase 286C-4.3)
|
||||
//!
|
||||
//! Debug-only verification functions that panic on contract violations.
|
||||
//! These are excluded from release builds.
|
||||
//!
|
||||
//! # Split from contract_checks.rs
|
||||
//!
|
||||
//! This file was extracted from contract_checks.rs to separate debug-only
|
||||
//! panic-based assertions from production Fail-Fast contract checks.
|
||||
//!
|
||||
//! All functions here are `#[cfg(debug_assertions)]` and panic on violations.
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||
#[cfg(debug_assertions)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use super::LoopHeaderPhiInfo;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
|
||||
|
||||
/// Verify loop header PHIs match boundary expectations (debug assertion)
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// - If boundary has loop_var_name, header block must have corresponding PHI
|
||||
/// - Each carrier_phi entry must have a PHI instruction in header block
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if PHI structure doesn't match boundary expectations.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_loop_header_phis(
|
||||
func: &MirFunction,
|
||||
header_block: BasicBlockId,
|
||||
loop_info: &LoopHeaderPhiInfo,
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
if let Some(ref loop_var_name) = boundary.loop_var_name {
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let has_loop_var_phi = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|instr| matches!(instr, MirInstruction::Phi { .. }));
|
||||
|
||||
if !has_loop_var_phi && !loop_info.carrier_phis.is_empty() {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Loop variable '{}' in boundary but no PHI in header block {} (has {} carrier PHIs)",
|
||||
loop_var_name, header_block.0, loop_info.carrier_phis.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !loop_info.carrier_phis.is_empty() {
|
||||
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
|
||||
header_block,
|
||||
func.blocks.len()
|
||||
)
|
||||
});
|
||||
let phi_count = header_block_data
|
||||
.instructions
|
||||
.iter()
|
||||
.filter(|instr| matches!(instr, MirInstruction::Phi { .. }))
|
||||
.count();
|
||||
|
||||
if phi_count == 0 {
|
||||
panic!(
|
||||
"[JoinIRVerifier] LoopHeaderPhiInfo has {} PHIs but header block {} has none",
|
||||
loop_info.carrier_phis.len(),
|
||||
header_block.0
|
||||
);
|
||||
}
|
||||
|
||||
for (carrier_name, entry) in &loop_info.carrier_phis {
|
||||
let phi_exists = header_block_data.instructions.iter().any(|instr| {
|
||||
if let MirInstruction::Phi { dst, .. } = instr {
|
||||
*dst == entry.phi_dst
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !phi_exists {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Carrier '{}' has PHI dst {:?} but PHI not found in header block {}",
|
||||
carrier_name, entry.phi_dst, header_block.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify exit line structure (debug assertion)
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// - Exit block must exist in function
|
||||
/// - Exit binding host_slots must be reasonable (< 1_000_000)
|
||||
/// - Exit PHI ValueIds must not collide with other instructions
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if exit line structure is invalid.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_exit_line(
|
||||
func: &MirFunction,
|
||||
exit_block: BasicBlockId,
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
if !func.blocks.contains_key(&exit_block) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit block {} out of range (func has {} blocks)",
|
||||
exit_block.0,
|
||||
func.blocks.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !boundary.exit_bindings.is_empty() {
|
||||
for binding in &boundary.exit_bindings {
|
||||
if binding.host_slot.0 >= 1_000_000 {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit binding '{}' has suspiciously large host_slot {:?}",
|
||||
binding.carrier_name, binding.host_slot
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 132-P2: Verify exit PHI ValueIds don't collide with other instructions
|
||||
verify_exit_phi_no_collision(func, exit_block);
|
||||
}
|
||||
|
||||
/// Phase 132-P2: Verify exit PHI dst ValueIds don't collide with other instructions
|
||||
///
|
||||
/// # Problem
|
||||
///
|
||||
/// If exit_phi_builder uses builder.value_gen.next() (module-level) instead of
|
||||
/// func.next_value_id() (function-level), it can allocate ValueIds that collide
|
||||
/// with existing instructions in the function.
|
||||
///
|
||||
/// Example collision:
|
||||
/// - bb0: %1 = const 0 (counter init)
|
||||
/// - bb3: %1 = phi ... (exit PHI - collision!)
|
||||
///
|
||||
/// This causes LLVM backend errors:
|
||||
/// "Cannot overwrite PHI dst=1. ValueId namespace collision detected."
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// All exit PHI dst ValueIds must be unique within the function and not
|
||||
/// overwrite any existing instruction dst.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any exit PHI dst collides with an existing instruction dst.
|
||||
#[cfg(debug_assertions)]
|
||||
fn verify_exit_phi_no_collision(func: &MirFunction, exit_block: BasicBlockId) {
|
||||
let exit_block_data = match func.blocks.get(&exit_block) {
|
||||
Some(block) => block,
|
||||
None => return, // Block not found, other verification will catch this
|
||||
};
|
||||
|
||||
// Collect all exit PHI dsts
|
||||
let mut exit_phi_dsts = std::collections::HashSet::new();
|
||||
for instr in &exit_block_data.instructions {
|
||||
if let MirInstruction::Phi { dst, .. } = instr {
|
||||
exit_phi_dsts.insert(*dst);
|
||||
}
|
||||
}
|
||||
|
||||
if exit_phi_dsts.is_empty() {
|
||||
return; // No exit PHIs, nothing to verify
|
||||
}
|
||||
|
||||
// Collect all instruction dsts in the entire function (excluding PHIs)
|
||||
let mut all_non_phi_dsts = std::collections::HashSet::new();
|
||||
for (block_id, block) in &func.blocks {
|
||||
if *block_id == exit_block {
|
||||
// For exit block, only check non-PHI instructions
|
||||
for instr in &block.instructions {
|
||||
if !matches!(instr, MirInstruction::Phi { .. }) {
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
all_non_phi_dsts.insert(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other blocks, check all instructions
|
||||
for instr in &block.instructions {
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
all_non_phi_dsts.insert(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for collisions
|
||||
for phi_dst in &exit_phi_dsts {
|
||||
if all_non_phi_dsts.contains(phi_dst) {
|
||||
// Find which instruction collides
|
||||
for (block_id, block) in &func.blocks {
|
||||
for instr in &block.instructions {
|
||||
if matches!(instr, MirInstruction::Phi { .. }) && *block_id == exit_block {
|
||||
continue; // Skip exit PHIs themselves
|
||||
}
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
if dst == *phi_dst {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase132-P2] Exit PHI dst {:?} collides with instruction in block {}: {:?}\n\
|
||||
This indicates exit_phi_builder used module-level value_gen.next() instead of function-level next_value_id().\n\
|
||||
Fix: Use func.next_value_id() in exit_phi_builder.rs",
|
||||
phi_dst, block_id.0, instr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Extract dst ValueId from MirInstruction
|
||||
#[cfg(debug_assertions)]
|
||||
fn get_instruction_dst(instr: &MirInstruction) -> Option<ValueId> {
|
||||
use MirInstruction;
|
||||
match instr {
|
||||
MirInstruction::Const { dst, .. }
|
||||
| MirInstruction::Load { dst, .. }
|
||||
| MirInstruction::UnaryOp { dst, .. }
|
||||
| MirInstruction::BinOp { dst, .. }
|
||||
| MirInstruction::Compare { dst, .. }
|
||||
| MirInstruction::TypeOp { dst, .. }
|
||||
| MirInstruction::NewBox { dst, .. }
|
||||
| MirInstruction::NewClosure { dst, .. }
|
||||
| MirInstruction::Copy { dst, .. }
|
||||
| MirInstruction::Cast { dst, .. }
|
||||
| MirInstruction::TypeCheck { dst, .. }
|
||||
| MirInstruction::Phi { dst, .. }
|
||||
| MirInstruction::ArrayGet { dst, .. }
|
||||
| MirInstruction::RefNew { dst, .. }
|
||||
| MirInstruction::RefGet { dst, .. }
|
||||
| MirInstruction::WeakNew { dst, .. }
|
||||
| MirInstruction::WeakLoad { dst, .. }
|
||||
| MirInstruction::WeakRef { dst, .. }
|
||||
| MirInstruction::FutureNew { dst, .. }
|
||||
| MirInstruction::Await { dst, .. } => Some(*dst),
|
||||
MirInstruction::BoxCall { dst, .. }
|
||||
| MirInstruction::ExternCall { dst, .. }
|
||||
| MirInstruction::Call { dst, .. }
|
||||
| MirInstruction::PluginInvoke { dst, .. } => *dst,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify ValueId regions are correct (debug assertion)
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// - join_inputs must be in Param region (100-999)
|
||||
/// - carrier PHI dsts must be in Local region (<= LOCAL_MAX)
|
||||
/// - condition_bindings join_values must be in Param region
|
||||
/// - exit_bindings join_exit_values must be in Param region
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any ValueId is outside its expected region.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_valueid_regions(loop_info: &LoopHeaderPhiInfo, boundary: &JoinInlineBoundary) {
|
||||
fn region_name(id: ValueId) -> &'static str {
|
||||
if id.0 < PARAM_MIN {
|
||||
"PHI Reserved"
|
||||
} else if id.0 <= PARAM_MAX {
|
||||
"Param"
|
||||
} else if id.0 <= LOCAL_MAX {
|
||||
"Local"
|
||||
} else {
|
||||
"Invalid (> LOCAL_MAX)"
|
||||
}
|
||||
}
|
||||
|
||||
for join_id in &boundary.join_inputs {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&join_id.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] join_input {:?} not in Param region ({})",
|
||||
join_id,
|
||||
region_name(*join_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, entry) in &loop_info.carrier_phis {
|
||||
if entry.phi_dst.0 > LOCAL_MAX {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Carrier PHI dst {:?} outside Local region ({})",
|
||||
entry.phi_dst,
|
||||
region_name(entry.phi_dst)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for binding in &boundary.condition_bindings {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_value.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Condition binding '{}' join_value {:?} not in Param region ({})",
|
||||
binding.name,
|
||||
binding.join_value,
|
||||
region_name(binding.join_value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for binding in &boundary.exit_bindings {
|
||||
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_exit_value.0) {
|
||||
panic!(
|
||||
"[JoinIRVerifier] Exit binding '{}' join_exit_value {:?} not in Param region ({})",
|
||||
binding.carrier_name,
|
||||
binding.join_exit_value,
|
||||
region_name(binding.join_exit_value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 135 P1 Step 1: Verify condition_bindings consistency (alias allowed, conflict fails)
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// condition_bindings can have multiple names (aliases) pointing to the same join_value,
|
||||
/// but if the same join_value appears with different host_value, it's a contract violation.
|
||||
///
|
||||
/// This catches merge-time inconsistencies before BoundaryInjector tries to inject Copy
|
||||
/// instructions, preventing MIR SSA breakage.
|
||||
///
|
||||
/// # Example Valid (alias):
|
||||
/// ```text
|
||||
/// condition_bindings: [
|
||||
/// { name: "is_char_match", join_value: ValueId(104), host_value: ValueId(12) },
|
||||
/// { name: "char", join_value: ValueId(104), host_value: ValueId(12) } // Same host_value - OK
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Example Invalid (conflict):
|
||||
/// ```text
|
||||
/// condition_bindings: [
|
||||
/// { name: "is_char_match", join_value: ValueId(104), host_value: ValueId(12) },
|
||||
/// { name: "char", join_value: ValueId(104), host_value: ValueId(18) } // Different host_value - FAIL
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the same join_value has conflicting host_value mappings.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_condition_bindings_consistent(boundary: &JoinInlineBoundary) {
|
||||
let mut join_to_host: HashMap<ValueId, ValueId> = HashMap::new();
|
||||
|
||||
for binding in &boundary.condition_bindings {
|
||||
if let Some(&existing_host) = join_to_host.get(&binding.join_value) {
|
||||
if existing_host != binding.host_value {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase135-P1] condition_bindings conflict: join_value {:?} mapped to both {:?} and {:?}\n\
|
||||
Binding names with conflict: check all bindings with join_value={:?}\n\
|
||||
Contract: Same join_value can have multiple names (alias) but must have same host_value.\n\
|
||||
Fix: Ensure ConditionLoweringBox uses SSOT allocator (ConditionContext.alloc_value).",
|
||||
binding.join_value, existing_host, binding.host_value, binding.join_value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
join_to_host.insert(binding.join_value, binding.host_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 135 P1 Step 2: Verify header PHI dsts are not redefined by non-PHI instructions
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// Loop header PHI dst ValueIds must not be reused as dst in non-PHI instructions.
|
||||
/// This prevents "PHI dst overwrite" where a Copy/BinOp/etc. instruction redefines
|
||||
/// the PHI result, breaking MIR SSA.
|
||||
///
|
||||
/// # Example Invalid:
|
||||
/// ```text
|
||||
/// bb3 (header):
|
||||
/// %14 = phi [%2, bb1], [%28, bb8] // Header PHI
|
||||
/// %16 = copy %0
|
||||
/// %14 = call %16.length() // INVALID: Redefines PHI dst %14
|
||||
/// ```
|
||||
///
|
||||
/// This typically happens when:
|
||||
/// - ConditionLoweringBox bypasses SSOT allocator and reuses PHI dst ValueIds
|
||||
/// - JoinIR merge incorrectly remaps values to PHI dst range
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any header PHI dst is redefined by a non-PHI instruction in the function.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_header_phi_dsts_not_redefined(
|
||||
func: &MirFunction,
|
||||
header_block: BasicBlockId,
|
||||
phi_dsts: &std::collections::HashSet<ValueId>,
|
||||
) {
|
||||
if phi_dsts.is_empty() {
|
||||
return; // No PHI dsts to protect
|
||||
}
|
||||
|
||||
// Check all blocks for non-PHI instructions that redefine PHI dsts
|
||||
for (block_id, block) in &func.blocks {
|
||||
for instr in &block.instructions {
|
||||
// Skip PHIs in header block (they're the definitions we're protecting)
|
||||
if *block_id == header_block && matches!(instr, MirInstruction::Phi { .. }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this instruction redefines a PHI dst
|
||||
if let Some(dst) = get_instruction_dst(instr) {
|
||||
if phi_dsts.contains(&dst) {
|
||||
panic!(
|
||||
"[JoinIRVerifier/Phase135-P1] Header PHI dst {:?} redefined by non-PHI instruction in block {}:\n\
|
||||
Instruction: {:?}\n\
|
||||
Contract: Header PHI dsts must not be reused as dst in other instructions.\n\
|
||||
Fix: Ensure ConditionLoweringBox uses SSOT allocator (ConditionContext.alloc_value) to avoid ValueId collisions.",
|
||||
dst, block_id.0, instr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,7 @@ mod block_allocator;
|
||||
mod block_remapper; // Phase 284 P1: Block ID remap SSOT
|
||||
mod carrier_init_builder;
|
||||
pub(super) mod contract_checks; // Phase 256 P1.5-DBG: Exposed for patterns to access verify_boundary_entry_params
|
||||
mod debug_assertions; // Phase 286C-4.3: Debug-only assertions (split from contract_checks)
|
||||
pub mod exit_args_collector; // Phase 118: Exit args collection box
|
||||
pub mod exit_line;
|
||||
mod exit_phi_builder;
|
||||
@ -1520,13 +1521,13 @@ fn verify_joinir_contracts(
|
||||
boundary: &JoinInlineBoundary,
|
||||
) {
|
||||
// Phase 135 P1 Step 1: Verify condition_bindings consistency (before merge)
|
||||
contract_checks::verify_condition_bindings_consistent(boundary);
|
||||
debug_assertions::verify_condition_bindings_consistent(boundary);
|
||||
|
||||
contract_checks::verify_loop_header_phis(func, header_block, loop_info, boundary);
|
||||
debug_assertions::verify_loop_header_phis(func, header_block, loop_info, boundary);
|
||||
verify_no_phi_dst_overwrite(func, header_block, loop_info); // Phase 204-2
|
||||
verify_phi_inputs_defined(func, header_block); // Phase 204-3
|
||||
contract_checks::verify_exit_line(func, exit_block, boundary);
|
||||
contract_checks::verify_valueid_regions(loop_info, boundary); // Phase 205-4
|
||||
debug_assertions::verify_exit_line(func, exit_block, boundary);
|
||||
debug_assertions::verify_valueid_regions(loop_info, boundary); // Phase 205-4
|
||||
|
||||
// Phase 135 P1 Step 2: Verify header PHI dsts not redefined (after merge)
|
||||
let phi_dsts: std::collections::HashSet<_> = loop_info
|
||||
@ -1534,5 +1535,5 @@ fn verify_joinir_contracts(
|
||||
.values()
|
||||
.map(|entry| entry.phi_dst)
|
||||
.collect();
|
||||
contract_checks::verify_header_phi_dsts_not_redefined(func, header_block, &phi_dsts);
|
||||
debug_assertions::verify_header_phi_dsts_not_redefined(func, header_block, &phi_dsts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user