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

View File

@ -127,13 +127,11 @@ pub fn lower_if_sum_pattern(
// main() locals
let i_init_val = alloc_value(); // i = 0
let sum_init_val = alloc_value(); // sum = 0
let count_init_val = alloc_value(); // count = 0 (optional)
let loop_result = alloc_value(); // result from loop_step
// loop_step params
let i_param = alloc_value();
let sum_param = alloc_value();
let count_param = alloc_value();
// loop_step locals
// Phase 220-D: loop_limit_val and if_value_val are already allocated by extract_*_condition()
@ -142,19 +140,14 @@ pub fn lower_if_sum_pattern(
let exit_cond = alloc_value(); // negated loop condition
let if_cmp = alloc_value(); // if condition comparison
let sum_then = alloc_value(); // sum + update_addend
let count_const = alloc_value(); // count increment (1)
let count_then = alloc_value(); // count + 1
let const_0 = alloc_value(); // 0 for else branch
let sum_else = alloc_value(); // sum + 0 (identity)
let count_else = alloc_value(); // count + 0 (identity)
let sum_new = alloc_value(); // Select result for sum
let count_new = alloc_value(); // Select result for count
let step_const = alloc_value(); // counter step
let i_next = alloc_value(); // i + step
// k_exit params
let sum_final = alloc_value();
let count_final = alloc_value();
// === main() function ===
let mut main_func = JoinFunction::new(main_id, "main".to_string(), vec![]);
@ -171,16 +164,10 @@ pub fn lower_if_sum_pattern(
value: ConstValue::Integer(0),
}));
// count_init = 0
main_func.body.push(JoinInst::Compute(MirLikeInst::Const {
dst: count_init_val,
value: ConstValue::Integer(0),
}));
// result = loop_step(i_init, sum_init, count_init)
// result = loop_step(i_init, sum_init)
main_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_init_val, sum_init_val, count_init_val],
args: vec![i_init_val, sum_init_val],
k_next: None,
dst: Some(loop_result),
});
@ -191,11 +178,11 @@ pub fn lower_if_sum_pattern(
join_module.add_function(main_func);
// === loop_step(i, sum, count) function ===
// === loop_step(i, sum) function ===
let mut loop_step_func = JoinFunction::new(
loop_step_id,
"loop_step".to_string(),
vec![i_param, sum_param, count_param],
vec![i_param, sum_param],
);
// --- Exit Condition Check ---
@ -230,7 +217,7 @@ pub fn lower_if_sum_pattern(
// Jump to exit if condition is false
loop_step_func.body.push(JoinInst::Jump {
cont: k_exit_id.as_cont(),
args: vec![sum_param, count_param],
args: vec![sum_param],
cond: Some(exit_cond),
});
@ -270,22 +257,6 @@ pub fn lower_if_sum_pattern(
rhs: const_0,
}));
// count_then = count + 1
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Const {
dst: count_const,
value: ConstValue::Integer(1),
}));
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: count_then,
op: BinOpKind::Add,
lhs: count_param,
rhs: count_const,
}));
// --- Else Branch ---
// sum_else = sum + 0 (identity)
loop_step_func
@ -303,16 +274,6 @@ pub fn lower_if_sum_pattern(
rhs: step_const,
}));
// count_else = count + 0 (identity)
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: count_else,
op: BinOpKind::Add,
lhs: count_param,
rhs: step_const, // 0
}));
// --- Select ---
loop_step_func
.body
@ -323,15 +284,6 @@ pub fn lower_if_sum_pattern(
else_val: sum_else,
}));
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Select {
dst: count_new,
cond: if_cmp,
then_val: count_then,
else_val: count_else,
}));
// --- Counter Update ---
let step_const2 = alloc_value();
loop_step_func
@ -352,19 +304,15 @@ pub fn lower_if_sum_pattern(
// --- Tail Recursion ---
loop_step_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_next, sum_new, count_new],
args: vec![i_next, sum_new],
k_next: None,
dst: None,
});
join_module.add_function(loop_step_func);
// === k_exit(sum_final, count_final) function ===
let mut k_exit_func = JoinFunction::new(
k_exit_id,
"k_exit".to_string(),
vec![sum_final, count_final],
);
// === k_exit(sum_final) function ===
let mut k_exit_func = JoinFunction::new(k_exit_id, "k_exit".to_string(), vec![sum_final]);
k_exit_func.body.push(JoinInst::Ret {
value: Some(sum_final),
@ -374,9 +322,9 @@ pub fn lower_if_sum_pattern(
join_module.entry = Some(main_id);
// Build ExitMeta
// SSOT: use the accumulator name extracted from AST (no hardcoded carrier names).
let mut exit_values = vec![];
exit_values.push(("sum".to_string(), sum_final));
exit_values.push(("count".to_string(), count_final));
exit_values.push((update_var.clone(), sum_final));
let exit_meta = ExitMeta::multiple(exit_values);