fix(joinir): Phase 118 Pattern3 exit carrier PHI SSOT

This commit is contained in:
nyash-codex
2025-12-18 03:43:00 +09:00
parent 2c7f5f7a5e
commit 1080dee58f
4 changed files with 161 additions and 134 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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?**