fix(joinir): Phase 118 Pattern3 exit carrier PHI SSOT
This commit is contained in:
@ -1,9 +1,48 @@
|
||||
use super::LoopHeaderPhiInfo;
|
||||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use super::LoopHeaderPhiInfo;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
|
||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction};
|
||||
#[cfg(debug_assertions)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Phase 118 P2: Contract check (Fail-Fast) - exit_bindings carriers must have exit PHI dsts.
|
||||
///
|
||||
/// This prevents latent "Carrier '<name>' not found in carrier_phis" failures later in
|
||||
/// ExprResultResolver and ExitLine reconnection.
|
||||
pub(super) fn verify_exit_bindings_have_exit_phis(
|
||||
boundary: &JoinInlineBoundary,
|
||||
exit_carrier_phis: &BTreeMap<String, 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 {
|
||||
if binding.role == CarrierRole::ConditionOnly {
|
||||
continue;
|
||||
}
|
||||
if exit_carrier_phis.contains_key(&binding.carrier_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(error_tags::freeze_with_hint(
|
||||
"phase118/exit_phi/missing_carrier_phi",
|
||||
&format!(
|
||||
"exit_bindings carrier '{}' is missing from exit_carrier_phis",
|
||||
binding.carrier_name
|
||||
),
|
||||
"exit_bindings carriers must be included in exit_phi_builder inputs; check carrier_inputs derivation from jump_args",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn verify_loop_header_phis(
|
||||
func: &MirFunction,
|
||||
|
||||
@ -650,12 +650,46 @@ pub(super) fn merge_and_rewrite(
|
||||
|
||||
// Phase 246-EX: Collect exit values from jump_args
|
||||
if let Some(b) = boundary {
|
||||
if let Some(ref carrier_info) = b.carrier_info {
|
||||
let expected_args = carrier_info.carriers.len() + 1; // loop_var + carriers
|
||||
if remapped_args.len() < expected_args {
|
||||
// ExprResult collection: first jump arg is preserved as Return value by the converter,
|
||||
// and is the SSOT for exit_phi_inputs in this merge phase.
|
||||
if let Some(&first_exit) = remapped_args.first() {
|
||||
exit_phi_inputs.push((new_block_id, first_exit));
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX: exit_phi_inputs from jump_args[0]: ({:?}, {:?})",
|
||||
new_block_id, first_exit
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 118 P2: carrier_inputs SSOT = boundary.exit_bindings
|
||||
//
|
||||
// Contract:
|
||||
// - Every LoopState carrier in exit_bindings must have an exit PHI input.
|
||||
// - jump_args order is assumed to match exit_bindings order, with at most one
|
||||
// leading extra slot (legacy layouts).
|
||||
//
|
||||
// This avoids Pattern3-specific assumptions such as "jump_args[0] is loop_var".
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
|
||||
let exit_phi_bindings: Vec<_> = b
|
||||
.exit_bindings
|
||||
.iter()
|
||||
.filter(|eb| eb.role != CarrierRole::ConditionOnly)
|
||||
.collect();
|
||||
|
||||
if !exit_phi_bindings.is_empty() {
|
||||
let offset = if remapped_args.len()
|
||||
== exit_phi_bindings.len()
|
||||
{
|
||||
0usize
|
||||
} else if remapped_args.len()
|
||||
== exit_phi_bindings.len() + 1
|
||||
{
|
||||
1usize
|
||||
} else if remapped_args.len() < exit_phi_bindings.len()
|
||||
{
|
||||
let msg = format!(
|
||||
"[joinir/exit-line] jump_args length mismatch: expected at least {} (loop_var + carriers) but got {} in block {:?}",
|
||||
expected_args,
|
||||
"[joinir/exit-line] jump_args too short: need {} carrier args (from exit_bindings) but got {} in block {:?}",
|
||||
exit_phi_bindings.len(),
|
||||
remapped_args.len(),
|
||||
old_block.id
|
||||
);
|
||||
@ -664,91 +698,56 @@ pub(super) fn merge_and_rewrite(
|
||||
} else {
|
||||
log!(verbose, "[DEBUG-177] {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First arg is the loop variable (expr_result)
|
||||
if let Some(&loop_var_exit) = remapped_args.first() {
|
||||
exit_phi_inputs.push((new_block_id, loop_var_exit));
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX: exit_phi_inputs from jump_args[0]: ({:?}, {:?})",
|
||||
new_block_id, loop_var_exit
|
||||
);
|
||||
|
||||
// Phase 246-EX-P5: Also add loop_var to carrier_inputs if it's a named variable
|
||||
// (Pattern 5 case: counter is both loop_var AND a carrier)
|
||||
if let Some(ref loop_var_name) = b.loop_var_name {
|
||||
carrier_inputs
|
||||
.entry(loop_var_name.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((new_block_id, loop_var_exit));
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX-P5: Added loop_var '{}' to carrier_inputs: ({:?}, {:?})",
|
||||
loop_var_name, new_block_id, loop_var_exit
|
||||
0usize
|
||||
} else {
|
||||
let msg = format!(
|
||||
"[joinir/exit-line] jump_args length mismatch: expected {} or {} (exit_bindings carriers ±1) but got {} in block {:?}",
|
||||
exit_phi_bindings.len(),
|
||||
exit_phi_bindings.len() + 1,
|
||||
remapped_args.len(),
|
||||
old_block.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 246-EX-FIX: jump_args are in carrier_info.carriers order, not exit_bindings order!
|
||||
//
|
||||
// jump_args layout: [loop_var, carrier1, carrier2, ...]
|
||||
// where carriers follow carrier_info.carriers order (NOT exit_bindings order)
|
||||
//
|
||||
// We need to:
|
||||
// 1. Iterate through carrier_info.carriers (to get the right index)
|
||||
// 2. Skip ConditionOnly carriers (they don't participate in exit PHI)
|
||||
// 3. Map the jump_args index to the carrier name
|
||||
if let Some(ref carrier_info) = b.carrier_info {
|
||||
for (carrier_idx, carrier) in
|
||||
carrier_info.carriers.iter().enumerate()
|
||||
{
|
||||
// Phase 227: Skip ConditionOnly carriers
|
||||
if carrier.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::ConditionOnly {
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 227: Skipping ConditionOnly carrier '{}' from exit PHI",
|
||||
carrier.name
|
||||
);
|
||||
continue;
|
||||
if strict_exit {
|
||||
return Err(msg);
|
||||
} else {
|
||||
log!(verbose, "[DEBUG-177] {}", msg);
|
||||
}
|
||||
0usize
|
||||
};
|
||||
|
||||
// jump_args[0] = loop_var, jump_args[1..] = carriers
|
||||
let jump_args_idx = carrier_idx + 1;
|
||||
for (binding_idx, binding) in
|
||||
exit_phi_bindings.iter().enumerate()
|
||||
{
|
||||
let jump_args_idx = offset + binding_idx;
|
||||
if let Some(&carrier_exit) =
|
||||
remapped_args.get(jump_args_idx)
|
||||
{
|
||||
carrier_inputs
|
||||
.entry(carrier.name.clone())
|
||||
.entry(binding.carrier_name.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((new_block_id, carrier_exit));
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX-FIX: Collecting carrier '{}': from {:?} using jump_args[{}] = {:?}",
|
||||
carrier.name, new_block_id, jump_args_idx, carrier_exit
|
||||
"[DEBUG-177] Phase 118: Collecting carrier '{}': from {:?} using jump_args[{}] = {:?}",
|
||||
binding.carrier_name,
|
||||
new_block_id,
|
||||
jump_args_idx,
|
||||
carrier_exit
|
||||
);
|
||||
} else {
|
||||
let msg = format!(
|
||||
"[joinir/exit-line] Missing jump_args entry for carrier '{}' at index {} in block {:?}",
|
||||
carrier.name, jump_args_idx, old_block.id
|
||||
"[joinir/exit-line] Missing jump_args entry for exit_binding carrier '{}' at index {} in block {:?}",
|
||||
binding.carrier_name,
|
||||
jump_args_idx,
|
||||
old_block.id
|
||||
);
|
||||
if strict_exit {
|
||||
return Err(msg);
|
||||
} else {
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX WARNING: No jump_args entry for carrier '{}' at index {}",
|
||||
carrier.name, jump_args_idx
|
||||
);
|
||||
log!(verbose, "[DEBUG-177] {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log!(
|
||||
verbose,
|
||||
"[DEBUG-177] Phase 246-EX WARNING: No carrier_info in boundary!"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
|
||||
mod block_allocator;
|
||||
mod carrier_init_builder;
|
||||
#[cfg(debug_assertions)]
|
||||
mod contract_checks;
|
||||
pub mod exit_line;
|
||||
mod exit_phi_builder;
|
||||
@ -738,6 +737,48 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
debug,
|
||||
)?;
|
||||
|
||||
// Phase 118 P2: Contract check (Fail-Fast) - exit_bindings LoopState carriers must have exit PHIs.
|
||||
if let Some(boundary) = boundary {
|
||||
contract_checks::verify_exit_bindings_have_exit_phis(boundary, &exit_carrier_phis)?;
|
||||
}
|
||||
|
||||
// Phase 118 P1: Dev-only carrier-phi SSOT logs (exit_bindings vs carrier_inputs vs exit_carrier_phis)
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
if let Some(boundary) = boundary {
|
||||
let exit_binding_names: Vec<&str> = boundary
|
||||
.exit_bindings
|
||||
.iter()
|
||||
.map(|b| b.carrier_name.as_str())
|
||||
.collect();
|
||||
let carrier_input_names: Vec<&str> =
|
||||
merge_result.carrier_inputs.keys().map(|s| s.as_str()).collect();
|
||||
let exit_phi_names: Vec<&str> =
|
||||
exit_carrier_phis.keys().map(|s| s.as_str()).collect();
|
||||
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[joinir/phase118/dev] exit_bindings carriers={:?}",
|
||||
exit_binding_names
|
||||
),
|
||||
true,
|
||||
);
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[joinir/phase118/dev] carrier_inputs keys={:?}",
|
||||
carrier_input_names
|
||||
),
|
||||
true,
|
||||
);
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[joinir/phase118/dev] exit_carrier_phis keys={:?}",
|
||||
exit_phi_names
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 246-EX: CRITICAL FIX - Use exit PHI dsts for variable_map reconnection
|
||||
//
|
||||
// **Why EXIT PHI, not HEADER PHI?**
|
||||
|
||||
Reference in New Issue
Block a user