fix(joinir): Phase 287 P2 - Pattern6 nested loop latch overwrite fix
Fix infinite loop in Pattern6 (nested loop minimal) caused by main→loop_step overwriting k_inner_exit→loop_step latch values. Root cause: JoinIR main entry block was incorrectly treated as BackEdge, causing it to overwrite the correct latch incoming values set by the true back edge (k_inner_exit → loop_step). Solution: - Restrict latch recording to TailCallKind::BackEdge only - Treat only MAIN's entry block as entry-like (not loop_step's entry block) - Add debug_assert! to detect double latch set in future Refactoring: - Extract latch recording to latch_incoming_recorder module (SSOT) - Add boundary.loop_header_func_name for explicit header identification - Strengthen tail_call_classifier with is_source_entry_like parameter Tests: apps/tests/phase1883_nested_minimal.hako → RC:9 (was infinite loop) Smoke: 154/154 PASS, no regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -754,21 +754,19 @@ 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 {
|
||||
fn make_boundary(
|
||||
exit_bindings: Vec<crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding>,
|
||||
) -> JoinInlineBoundary {
|
||||
use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode;
|
||||
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
|
||||
|
||||
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,
|
||||
}
|
||||
let mut boundary = JoinInlineBoundaryBuilder::new()
|
||||
.with_inputs(vec![], vec![])
|
||||
.with_exit_bindings(exit_bindings)
|
||||
.build();
|
||||
|
||||
boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue;
|
||||
boundary
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -783,9 +781,9 @@ mod carrier_inputs_tests {
|
||||
let boundary = make_boundary(vec![
|
||||
LoopExitBinding {
|
||||
carrier_name: "sum".to_string(),
|
||||
role: CarrierRole::Accumulator,
|
||||
host_slot: ValueId(10),
|
||||
join_exit_value: ValueId(100),
|
||||
role: CarrierRole::LoopState,
|
||||
},
|
||||
]);
|
||||
|
||||
@ -822,13 +820,13 @@ mod carrier_inputs_tests {
|
||||
|
||||
#[test]
|
||||
fn test_verify_carrier_inputs_complete_valid() {
|
||||
// Setup: Accumulator carrier with inputs
|
||||
// Setup: LoopState 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),
|
||||
role: CarrierRole::LoopState,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -143,7 +143,9 @@ impl ExitArgsCollectorBox {
|
||||
if strict_exit {
|
||||
return Err(msg);
|
||||
} else {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
if crate::config::env::is_joinir_debug() {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,7 +181,9 @@ impl ExitArgsCollectorBox {
|
||||
if strict_exit {
|
||||
Err(msg)
|
||||
} else {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
if crate::config::env::is_joinir_debug() {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
}
|
||||
Ok(0) // Best effort: try direct mapping
|
||||
}
|
||||
} else {
|
||||
@ -214,7 +218,9 @@ impl ExitArgsCollectorBox {
|
||||
if strict_exit {
|
||||
Err(msg)
|
||||
} else {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
if crate::config::env::is_joinir_debug() {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ impl ExitLineOrchestrator {
|
||||
"[joinir/exit-line] orchestrator start: {} carrier PHIs",
|
||||
carrier_phis.len()
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ impl ExitLineOrchestrator {
|
||||
ExitLineReconnector::reconnect(builder, boundary, carrier_phis, remapped_exit_values, debug)?;
|
||||
|
||||
if verbose {
|
||||
trace.stderr_if("[joinir/exit-line] orchestrator complete", true);
|
||||
trace.stderr_if("[joinir/exit-line] orchestrator complete", verbose);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -98,7 +98,7 @@ impl ExitLineReconnector {
|
||||
boundary.exit_bindings.len(),
|
||||
carrier_phis.len()
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
if !boundary.exit_bindings.is_empty() {
|
||||
trace.stderr_if(
|
||||
@ -110,7 +110,7 @@ impl ExitLineReconnector {
|
||||
.map(|b| (&b.carrier_name, b.role, b.join_exit_value))
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -118,7 +118,7 @@ impl ExitLineReconnector {
|
||||
// Early return for empty exit_bindings
|
||||
if boundary.exit_bindings.is_empty() {
|
||||
if verbose {
|
||||
trace.stderr_if("[joinir/exit-line] reconnect: no exit bindings, skip", true);
|
||||
trace.stderr_if("[joinir/exit-line] reconnect: no exit bindings, skip", verbose);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@ -130,7 +130,7 @@ impl ExitLineReconnector {
|
||||
boundary.exit_bindings.len(),
|
||||
carrier_phis.len()
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
// Phase 260 P0.1 Step 3: Import from helpers module
|
||||
use super::rewriter::helpers::is_skippable_continuation;
|
||||
use super::rewriter::latch_incoming_recorder; // Phase 287 P2: latch recording SSOT
|
||||
// Phase 286C-2 Step 2: Import from instruction_filter_box module
|
||||
use super::rewriter::instruction_filter_box::InstructionFilterBox;
|
||||
// Phase 286C-2 Step 2: Import from parameter_binding_box module
|
||||
@ -283,15 +284,39 @@ fn plan_rewrites(
|
||||
functions_merge.sort_by_key(|(name, _)| name.as_str());
|
||||
|
||||
// Determine entry function (loop header)
|
||||
let entry_func_name = functions_merge
|
||||
.iter()
|
||||
.find(|(name, _)| {
|
||||
let name_str = name.as_str();
|
||||
let is_continuation = continuation_candidates.contains(*name);
|
||||
let is_main = name_str == crate::mir::join_ir::lowering::canonical_names::MAIN;
|
||||
!is_continuation && !is_main
|
||||
})
|
||||
.map(|(name, _)| name.as_str());
|
||||
//
|
||||
// Phase 287 P2: Prefer boundary SSOT (loop_header_func_name) over heuristic.
|
||||
let entry_func_name = boundary
|
||||
.and_then(|b| b.loop_header_func_name.as_deref())
|
||||
.or_else(|| {
|
||||
functions_merge
|
||||
.iter()
|
||||
.find(|(name, _)| {
|
||||
let name_str = name.as_str();
|
||||
let is_continuation = continuation_candidates.contains(*name);
|
||||
let is_main = name_str == crate::mir::join_ir::lowering::canonical_names::MAIN;
|
||||
!is_continuation && !is_main
|
||||
})
|
||||
.map(|(name, _)| name.as_str())
|
||||
});
|
||||
|
||||
fn resolve_target_func_name<'a>(
|
||||
function_entry_map: &'a BTreeMap<String, BasicBlockId>,
|
||||
target_block: BasicBlockId,
|
||||
) -> Option<&'a str> {
|
||||
function_entry_map
|
||||
.iter()
|
||||
.find_map(|(fname, &entry_block)| (entry_block == target_block).then(|| fname.as_str()))
|
||||
}
|
||||
|
||||
fn is_joinir_main_entry_block(
|
||||
func_name: &str,
|
||||
func: &crate::mir::MirFunction,
|
||||
old_block_id: BasicBlockId,
|
||||
) -> bool {
|
||||
func_name == crate::mir::join_ir::lowering::canonical_names::MAIN
|
||||
&& old_block_id == func.entry_block
|
||||
}
|
||||
|
||||
// Process each function
|
||||
for (func_name, func) in functions_merge {
|
||||
@ -328,13 +353,13 @@ fn plan_rewrites(
|
||||
let mut blocks_merge: Vec<_> = func.blocks.iter().collect();
|
||||
blocks_merge.sort_by_key(|(id, _)| id.0);
|
||||
|
||||
// Determine if this is the loop entry point
|
||||
let is_loop_entry_point =
|
||||
entry_func_name == Some(func_name.as_str()) && blocks_merge.first().map(|(id, _)| **id) == Some(func.entry_block);
|
||||
// Determine if this is the loop header entry block (loop_step entry).
|
||||
let is_loop_header_entry_block = entry_func_name == Some(func_name.as_str())
|
||||
&& blocks_merge.first().map(|(id, _)| **id) == Some(func.entry_block);
|
||||
|
||||
// Check if loop header has PHIs
|
||||
let is_loop_header_with_phi =
|
||||
is_loop_entry_point && !loop_header_phi_info.carrier_phis.is_empty();
|
||||
is_loop_header_entry_block && !loop_header_phi_info.carrier_phis.is_empty();
|
||||
|
||||
// Collect PHI dst IDs for this block (if loop header)
|
||||
let phi_dst_ids_for_block: std::collections::HashSet<ValueId> =
|
||||
@ -395,7 +420,11 @@ fn plan_rewrites(
|
||||
|
||||
// Skip boundary input Const instructions
|
||||
let boundary_inputs: Vec<ValueId> = boundary_input_set.iter().cloned().collect();
|
||||
if InstructionFilterBox::should_skip_boundary_input_const(*dst, &boundary_inputs, is_loop_entry_point) {
|
||||
if InstructionFilterBox::should_skip_boundary_input_const(
|
||||
*dst,
|
||||
&boundary_inputs,
|
||||
is_loop_header_entry_block,
|
||||
) {
|
||||
log!(verbose, "[plan_rewrites] Skipping boundary input const: {:?}", inst);
|
||||
continue;
|
||||
}
|
||||
@ -478,33 +507,34 @@ fn plan_rewrites(
|
||||
|
||||
// Second pass: Insert parameter bindings for tail calls (if any)
|
||||
if let Some((target_block, ref args)) = tail_call_target {
|
||||
// Find the target function name from the target_block
|
||||
let mut target_func_name: Option<String> = None;
|
||||
for (fname, &entry_block) in &ctx.function_entry_map {
|
||||
if entry_block == target_block {
|
||||
target_func_name = Some(fname.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let target_func_name = resolve_target_func_name(&ctx.function_entry_map, target_block);
|
||||
|
||||
// Check if target is continuation/recursive/loop entry
|
||||
let is_target_continuation = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| continuation_candidates.contains(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_recursive_call = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| name == func_name)
|
||||
.unwrap_or(false);
|
||||
let is_recursive_call = target_func_name.map(|name| name == func_name).unwrap_or(false);
|
||||
|
||||
// Phase 188.3: Define is_target_loop_entry early for latch incoming logic
|
||||
let is_target_loop_entry = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| entry_func_name == Some(name.as_str()))
|
||||
.map(|name| entry_func_name == Some(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
if let Some(ref target_func_name) = target_func_name {
|
||||
// Phase 287 P2: Calculate tail_call_kind early for latch incoming logic
|
||||
// Only treat MAIN's entry block as entry-like (not loop_step's entry block)
|
||||
let is_entry_like_block_for_latch =
|
||||
is_joinir_main_entry_block(func_name, func, *old_block_id);
|
||||
|
||||
let tail_call_kind = classify_tail_call(
|
||||
is_entry_like_block_for_latch,
|
||||
!loop_header_phi_info.carrier_phis.is_empty(),
|
||||
boundary.is_some(),
|
||||
is_target_continuation,
|
||||
is_target_loop_entry,
|
||||
);
|
||||
|
||||
if let Some(target_func_name) = target_func_name {
|
||||
if let Some(target_params) = function_params.get(target_func_name) {
|
||||
|
||||
log!(
|
||||
@ -519,7 +549,7 @@ fn plan_rewrites(
|
||||
// 3. Continuation call (handled separately below)
|
||||
// Phase 287 P1: Skip ONLY when target is loop header
|
||||
// (not when source is entry func but target is non-entry like inner_step)
|
||||
if is_loop_entry_point && is_target_loop_entry {
|
||||
if is_loop_header_entry_block && is_target_loop_entry {
|
||||
log!(
|
||||
verbose,
|
||||
"[plan_rewrites] Skip param bindings in header block (PHIs define carriers)"
|
||||
@ -586,59 +616,15 @@ fn plan_rewrites(
|
||||
}
|
||||
}
|
||||
|
||||
// Record latch incoming for loop header PHI (only for calls to loop entry func)
|
||||
// Phase 287 P2: Restrict to is_target_loop_entry only (not is_recursive_call)
|
||||
// This prevents inner_step recursion from overwriting outer loop's latch values
|
||||
if is_target_loop_entry {
|
||||
if let Some(b) = boundary {
|
||||
if let Some(loop_var_name) = &b.loop_var_name {
|
||||
if !args.is_empty() {
|
||||
let latch_value = args[0];
|
||||
loop_header_phi_info.set_latch_incoming(
|
||||
loop_var_name,
|
||||
new_block_id,
|
||||
latch_value,
|
||||
);
|
||||
log!(
|
||||
verbose,
|
||||
"[plan_rewrites] Set latch incoming for '{}': block={:?}, value={:?}",
|
||||
loop_var_name, new_block_id, latch_value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set latch incoming for other carriers
|
||||
let mut carrier_arg_idx = if b.loop_var_name.is_some() { 1 } else { 0 };
|
||||
for binding in b.exit_bindings.iter() {
|
||||
if let Some(ref loop_var) = b.loop_var_name {
|
||||
if &binding.carrier_name == loop_var {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if carrier_arg_idx < args.len() {
|
||||
let latch_value = args[carrier_arg_idx];
|
||||
loop_header_phi_info.set_latch_incoming(
|
||||
&binding.carrier_name,
|
||||
new_block_id,
|
||||
latch_value,
|
||||
);
|
||||
carrier_arg_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Set latch incoming for loop invariants
|
||||
for (inv_name, _inv_host_id) in b.loop_invariants.iter() {
|
||||
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
|
||||
loop_header_phi_info.set_latch_incoming(
|
||||
inv_name,
|
||||
new_block_id,
|
||||
phi_dst,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record latch incoming for loop header PHI (SSOT)
|
||||
// Phase 287 P2: BackEdge のみ latch 記録(LoopEntry main → loop_step を除外)
|
||||
latch_incoming_recorder::record_if_backedge(
|
||||
tail_call_kind,
|
||||
boundary,
|
||||
new_block_id,
|
||||
args,
|
||||
loop_header_phi_info,
|
||||
);
|
||||
}
|
||||
|
||||
// Synchronize spans
|
||||
@ -815,30 +801,23 @@ fn plan_rewrites(
|
||||
} else if let Some((target_block, args)) = tail_call_target {
|
||||
// Tail call: Set Jump terminator
|
||||
// Classify tail call and determine actual target
|
||||
let target_func_name = {
|
||||
let mut name: Option<String> = None;
|
||||
for (fname, &entry_block) in &ctx.function_entry_map {
|
||||
if entry_block == target_block {
|
||||
name = Some(fname.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
name
|
||||
};
|
||||
let target_func_name = resolve_target_func_name(&ctx.function_entry_map, target_block);
|
||||
|
||||
let is_target_continuation = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| continuation_candidates.contains(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Phase 287 P2: Compute is_target_loop_entry for classify_tail_call
|
||||
let is_target_loop_entry = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| entry_func_name == Some(name.as_str()))
|
||||
.map(|name| entry_func_name == Some(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Phase 287 P2: main の entry block からの呼び出しを LoopEntry 扱いにする
|
||||
// Only treat MAIN's entry block as entry-like (not loop_step's entry block)
|
||||
let is_entry_like_block = is_joinir_main_entry_block(func_name, func, *old_block_id);
|
||||
|
||||
let tail_call_kind = classify_tail_call(
|
||||
is_loop_entry_point,
|
||||
is_entry_like_block,
|
||||
!loop_header_phi_info.carrier_phis.is_empty(),
|
||||
boundary.is_some(),
|
||||
is_target_continuation,
|
||||
@ -864,15 +843,7 @@ fn plan_rewrites(
|
||||
}
|
||||
TailCallKind::ExitJump => {
|
||||
// Check if target is skippable continuation
|
||||
let mut target_func_name: Option<String> = None;
|
||||
for (fname, &entry_block) in &ctx.function_entry_map {
|
||||
if entry_block == target_block {
|
||||
target_func_name = Some(fname.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let is_target_skippable = target_func_name
|
||||
.as_ref()
|
||||
let is_target_skippable = resolve_target_func_name(&ctx.function_entry_map, target_block)
|
||||
.map(|name| skippable_continuation_func_names.contains(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
@ -97,6 +97,15 @@ impl LoopHeaderPhiInfo {
|
||||
/// Called from instruction_rewriter after processing tail call Copy instructions.
|
||||
pub fn set_latch_incoming(&mut self, name: &str, from_block: BasicBlockId, value: ValueId) {
|
||||
if let Some(entry) = self.carrier_phis.get_mut(name) {
|
||||
// Phase 287 P2 Fail-Fast: デバッグビルドのみ二重セット検知
|
||||
debug_assert!(
|
||||
entry.latch_incoming.is_none(),
|
||||
"Phase 287 P2 Fail-Fast: Double latch set for '{}'. Existing: {:?}, New: ({:?}, {:?})",
|
||||
name,
|
||||
entry.latch_incoming,
|
||||
from_block,
|
||||
value
|
||||
);
|
||||
entry.latch_incoming = Some((from_block, value));
|
||||
}
|
||||
}
|
||||
@ -170,4 +179,22 @@ mod tests {
|
||||
info.set_latch_incoming("i", BasicBlockId(20), ValueId(50));
|
||||
assert!(info.all_latch_set());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Double latch set")]
|
||||
fn set_latch_incoming_double_set_panics_in_debug() {
|
||||
let mut info = LoopHeaderPhiInfo::empty(BasicBlockId(9));
|
||||
info.carrier_phis.insert(
|
||||
"i".to_string(),
|
||||
CarrierPhiEntry {
|
||||
phi_dst: ValueId(100),
|
||||
entry_incoming: (BasicBlockId(1), ValueId(1)),
|
||||
latch_incoming: None,
|
||||
role: CarrierRole::LoopState,
|
||||
},
|
||||
);
|
||||
|
||||
info.set_latch_incoming("i", BasicBlockId(2), ValueId(2));
|
||||
info.set_latch_incoming("i", BasicBlockId(3), ValueId(3));
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,7 +217,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
"[cf_loop/joinir] Boundary join_inputs={:?} host_inputs={:?}",
|
||||
boundary.join_inputs, boundary.host_inputs
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
@ -225,7 +225,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
boundary.exit_bindings.len(),
|
||||
exit_summary.join(", ")
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
if !cond_summary.is_empty() {
|
||||
trace.stderr_if(
|
||||
@ -234,7 +234,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
cond_summary.len(),
|
||||
cond_summary.join(", ")
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
}
|
||||
if let Some(ci) = &boundary.carrier_info {
|
||||
@ -244,11 +244,11 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
"[cf_loop/joinir] Boundary carrier_info: loop_var='{}', carriers={:?}",
|
||||
ci.loop_var_name, carriers
|
||||
),
|
||||
true,
|
||||
verbose,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
trace.stderr_if("[cf_loop/joinir] No boundary provided", true);
|
||||
trace.stderr_if("[cf_loop/joinir] No boundary provided", verbose);
|
||||
}
|
||||
}
|
||||
|
||||
@ -364,18 +364,34 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
let (mut loop_header_phi_info, merge_entry_block) = if let Some(boundary) = boundary {
|
||||
if let Some(loop_var_name) = &boundary.loop_var_name {
|
||||
// Get loop_step function for PHI placement (the actual loop header)
|
||||
let (loop_step_func_name, loop_step_func) = mir_module
|
||||
.functions
|
||||
.iter()
|
||||
.find(|(name, _)| {
|
||||
let is_continuation = boundary.continuation_func_ids.contains(*name);
|
||||
let is_main = *name == crate::mir::join_ir::lowering::canonical_names::MAIN;
|
||||
!is_continuation && !is_main
|
||||
let loop_step_func_name = boundary
|
||||
.loop_header_func_name
|
||||
.as_deref()
|
||||
.or_else(|| {
|
||||
mir_module
|
||||
.functions
|
||||
.iter()
|
||||
.find(|(name, _)| {
|
||||
let is_continuation = boundary.continuation_func_ids.contains(*name);
|
||||
let is_main =
|
||||
*name == crate::mir::join_ir::lowering::canonical_names::MAIN;
|
||||
!is_continuation && !is_main
|
||||
})
|
||||
.map(|(name, _)| name.as_str())
|
||||
})
|
||||
.or_else(|| mir_module.functions.iter().next())
|
||||
.map(|(name, func)| (name.as_str(), func))
|
||||
.or_else(|| mir_module.functions.keys().next().map(|s| s.as_str()))
|
||||
.ok_or("JoinIR module has no functions (Phase 201-A)")?;
|
||||
|
||||
let loop_step_func = mir_module
|
||||
.functions
|
||||
.get(loop_step_func_name)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"loop_header_func_name '{}' not found in JoinIR module (Phase 287 P2)",
|
||||
loop_step_func_name
|
||||
)
|
||||
})?;
|
||||
|
||||
// Phase 256.7-fix: Determine merge_entry_block
|
||||
// When main has condition_bindings as params, we enter through main first
|
||||
// (for boundary Copies), then main's tail call jumps to loop_step.
|
||||
@ -479,7 +495,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
"[cf_loop/joinir] Phase 256.7-fix: merge_entry_func='{}', merge_entry_block={:?}, loop_header_block={:?}",
|
||||
entry_func_name, entry_block_remapped, loop_header_block
|
||||
),
|
||||
true, // Always log for debugging
|
||||
debug,
|
||||
);
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
|
||||
@ -70,7 +70,9 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn collect_exit_values_
|
||||
if strict_exit {
|
||||
return Err(msg);
|
||||
} else {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
if crate::config::env::is_joinir_debug() {
|
||||
eprintln!("[DEBUG-177] {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The jump_args are in JoinIR value space, remap them to HOST
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
//! Latch incoming recorder (SSOT)
|
||||
//!
|
||||
//! Phase 287 P2: Centralize "when is it legal to record latch_incoming" policy.
|
||||
//!
|
||||
//! Contract:
|
||||
//! - Only record latch_incoming for `TailCallKind::BackEdge`
|
||||
//! - Never record for LoopEntry (main → loop_step), to avoid overwriting the true latch
|
||||
|
||||
use crate::mir::builder::control_flow::joinir::merge::loop_header_phi_info::LoopHeaderPhiInfo;
|
||||
use crate::mir::builder::control_flow::joinir::merge::tail_call_classifier::TailCallKind;
|
||||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||||
use crate::mir::{BasicBlockId, ValueId};
|
||||
|
||||
pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
|
||||
tail_call_kind: TailCallKind,
|
||||
boundary: Option<&JoinInlineBoundary>,
|
||||
new_block_id: BasicBlockId,
|
||||
args: &[ValueId],
|
||||
loop_header_phi_info: &mut LoopHeaderPhiInfo,
|
||||
) {
|
||||
if tail_call_kind != TailCallKind::BackEdge {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(boundary) = boundary else { return };
|
||||
|
||||
if let Some(loop_var_name) = &boundary.loop_var_name {
|
||||
if let Some(&latch_value) = args.first() {
|
||||
loop_header_phi_info.set_latch_incoming(loop_var_name, new_block_id, latch_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Other carriers (excluding loop_var)
|
||||
let mut carrier_arg_idx = if boundary.loop_var_name.is_some() { 1 } else { 0 };
|
||||
for binding in boundary.exit_bindings.iter() {
|
||||
if let Some(ref loop_var) = boundary.loop_var_name {
|
||||
if &binding.carrier_name == loop_var {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(&latch_value) = args.get(carrier_arg_idx) {
|
||||
loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value);
|
||||
carrier_arg_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop invariants: latch incoming is the PHI destination itself (preserve value).
|
||||
for (inv_name, _inv_host_id) in boundary.loop_invariants.iter() {
|
||||
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
|
||||
loop_header_phi_info.set_latch_incoming(inv_name, new_block_id, phi_dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ pub(super) mod carrier_inputs_collector; // Phase 286C-5 Step 1: DRY carrier_inp
|
||||
pub(super) mod exit_collection; // Phase 260 P0.1 Step 6: Exit collection extracted ✅
|
||||
pub(super) mod helpers; // Phase 260 P0.1 Step 3: Helpers extracted ✅
|
||||
pub(super) mod instruction_filter_box; // Phase 286C-2 Step 2: Skip judgment logic extracted ✅
|
||||
pub(super) mod latch_incoming_recorder; // Phase 287 P2: latch recording SSOT ✅
|
||||
pub(super) mod logging; // Phase 260 P0.1 Step 2: Logging extracted ✅
|
||||
pub(super) mod parameter_binding_box; // Phase 286C-2 Step 2: Parameter binding helpers ✅
|
||||
pub(super) mod return_converter_box; // Phase 286C-2 Step 2: Return → Jump conversion helpers ✅
|
||||
|
||||
@ -34,7 +34,7 @@ pub enum TailCallKind {
|
||||
/// Classifies a tail call based on context
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `is_entry_func_entry_block` - True if this is the first function's first block (loop entry point)
|
||||
/// * `is_source_entry_like` - True if this tail call originates from an entry-like block
|
||||
/// * `has_loop_header_phis` - True if loop header PHI nodes exist
|
||||
/// * `has_boundary` - True if JoinInlineBoundary exists (indicates loop context)
|
||||
/// * `is_target_continuation` - True if the tail call target is a continuation function (k_exit)
|
||||
@ -43,7 +43,7 @@ pub enum TailCallKind {
|
||||
/// # Returns
|
||||
/// The classification of this tail call
|
||||
pub fn classify_tail_call(
|
||||
is_entry_func_entry_block: bool,
|
||||
is_source_entry_like: bool,
|
||||
has_loop_header_phis: bool,
|
||||
has_boundary: bool,
|
||||
is_target_continuation: bool,
|
||||
@ -56,9 +56,9 @@ pub fn classify_tail_call(
|
||||
return TailCallKind::ExitJump;
|
||||
}
|
||||
|
||||
// Entry function's entry block is the loop entry point
|
||||
// It already IS at the header, so no redirection needed
|
||||
if is_entry_func_entry_block {
|
||||
// Entry-like block jumping into the loop header is a LoopEntry (main → loop_step).
|
||||
// It should NOT be redirected to the header block, otherwise we create a self-loop.
|
||||
if is_source_entry_like && is_target_loop_entry {
|
||||
return TailCallKind::LoopEntry;
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ pub fn classify_tail_call(
|
||||
// This prevents inner_step→inner_step from being classified as BackEdge,
|
||||
// which would incorrectly redirect it to the outer header (loop_step).
|
||||
// inner_step→inner_step should jump to inner_step's entry block, not outer header.
|
||||
if is_target_loop_entry && has_boundary && has_loop_header_phis && !is_entry_func_entry_block {
|
||||
if is_target_loop_entry && has_boundary && has_loop_header_phis {
|
||||
return TailCallKind::BackEdge;
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_classify_loop_entry() {
|
||||
let result = classify_tail_call(
|
||||
true, // is_entry_func_entry_block
|
||||
true, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
@ -93,7 +93,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_classify_back_edge() {
|
||||
let result = classify_tail_call(
|
||||
false, // is_entry_func_entry_block (not entry block)
|
||||
false, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
@ -105,7 +105,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_classify_exit_jump() {
|
||||
let result = classify_tail_call(
|
||||
false, // is_entry_func_entry_block
|
||||
false, // is_source_entry_like
|
||||
false, // has_loop_header_phis (no header PHIs)
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
@ -117,7 +117,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_classify_no_boundary() {
|
||||
let result = classify_tail_call(
|
||||
false, // is_entry_func_entry_block
|
||||
false, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
false, // has_boundary (no boundary → exit)
|
||||
false, // is_target_continuation
|
||||
@ -131,7 +131,7 @@ mod tests {
|
||||
// Phase 256 P1.10: Continuation calls (k_exit) are always ExitJump
|
||||
// even when they would otherwise be classified as BackEdge
|
||||
let result = classify_tail_call(
|
||||
false, // is_entry_func_entry_block
|
||||
false, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
true, // is_target_continuation ← this makes it ExitJump
|
||||
@ -145,7 +145,7 @@ mod tests {
|
||||
// Phase 287 P2: inner_step→inner_step should NOT be BackEdge
|
||||
// even with boundary and header PHIs, because target is not loop entry func
|
||||
let result = classify_tail_call(
|
||||
false, // is_entry_func_entry_block (inner_step body block)
|
||||
false, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
@ -153,4 +153,17 @@ mod tests {
|
||||
);
|
||||
assert_eq!(result, TailCallKind::ExitJump);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_entry_like_but_not_loop_entry_target() {
|
||||
// Entry-like source does not imply LoopEntry unless the target is loop_step.
|
||||
let result = classify_tail_call(
|
||||
true, // is_source_entry_like
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
false, // is_target_loop_entry
|
||||
);
|
||||
assert_eq!(result, TailCallKind::ExitJump);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,13 +52,13 @@ pub(super) fn collect_values(
|
||||
// Phase 188-Impl-3: Collect function parameters for tail call conversion
|
||||
function_params.insert(func_name.clone(), func.params.clone());
|
||||
|
||||
// Phase 256 P1.10 DEBUG: Always log function params for debugging
|
||||
// Phase 256 P1.10 DEBUG: function params log (guarded)
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Function '{}' params: {:?}",
|
||||
func_name, func.params
|
||||
),
|
||||
true,
|
||||
debug,
|
||||
);
|
||||
|
||||
for block in func.blocks.values() {
|
||||
|
||||
@ -383,6 +383,7 @@ mod tests {
|
||||
expr_result: None, // Phase 33-14: Add missing field
|
||||
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
|
||||
loop_var_name: None, // Phase 33-16: Add missing field
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228: Add missing field
|
||||
loop_invariants: vec![], // Phase 255 P2: Add missing field
|
||||
continuation_func_ids: std::collections::BTreeSet::from([
|
||||
|
||||
@ -137,6 +137,7 @@ mod tests {
|
||||
expr_result: None, // Phase 33-14: Add missing field
|
||||
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
|
||||
loop_var_name: None, // Phase 33-16: Add missing field
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228: Add missing field
|
||||
loop_invariants: vec![], // Phase 255 P2: Add missing field
|
||||
continuation_func_ids: std::collections::BTreeSet::from([
|
||||
|
||||
@ -308,6 +308,15 @@ pub struct JoinInlineBoundary {
|
||||
/// Used to track which PHI corresponds to the loop variable.
|
||||
pub loop_var_name: Option<String>,
|
||||
|
||||
/// Phase 287 P2: Loop header function name (SSOT)
|
||||
///
|
||||
/// Merge must not guess the loop header function from "first non-main non-continuation".
|
||||
/// For loop patterns, set this explicitly (typically `"loop_step"`).
|
||||
///
|
||||
/// - `Some(name)`: Merge uses this as the loop header function.
|
||||
/// - `None`: Legacy heuristic remains (for backwards compatibility).
|
||||
pub loop_header_func_name: Option<String>,
|
||||
|
||||
/// Phase 228: Carrier metadata (for header PHI generation)
|
||||
///
|
||||
/// Contains full carrier information including initialization policies.
|
||||
@ -435,6 +444,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14: Default to carrier-only pattern
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228: Default to None
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -482,6 +492,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -546,6 +557,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -596,6 +608,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -650,6 +663,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -711,6 +725,7 @@ impl JoinInlineBoundary {
|
||||
expr_result: None, // Phase 33-14
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
|
||||
loop_var_name: None, // Phase 33-16
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228
|
||||
continuation_func_ids: Self::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||
@ -767,6 +782,7 @@ mod tests {
|
||||
expr_result: Some(ValueId(10)),
|
||||
jump_args_layout: JumpArgsLayout::ExprResultPlusCarriers,
|
||||
loop_var_name: None,
|
||||
loop_header_func_name: None,
|
||||
carrier_info: None,
|
||||
continuation_func_ids: JoinInlineBoundary::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(),
|
||||
|
||||
@ -98,6 +98,7 @@ impl JoinInlineBoundaryBuilder {
|
||||
expr_result: None,
|
||||
jump_args_layout: JumpArgsLayout::CarriersOnly,
|
||||
loop_var_name: None,
|
||||
loop_header_func_name: None, // Phase 287 P2
|
||||
carrier_info: None, // Phase 228: Initialize as None
|
||||
continuation_func_ids: JoinInlineBoundary::default_continuations(),
|
||||
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5
|
||||
@ -184,6 +185,14 @@ impl JoinInlineBoundaryBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Phase 287 P2: Set loop header function name (SSOT)
|
||||
///
|
||||
/// If omitted for loop patterns, `build()` defaults it to `"loop_step"` when `loop_var_name` is set.
|
||||
pub fn with_loop_header_func_name(mut self, name: Option<String>) -> Self {
|
||||
self.boundary.loop_header_func_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set expression result (Phase 33-14)
|
||||
///
|
||||
/// If the loop is used as an expression, this is the JoinIR-local ValueId
|
||||
@ -200,6 +209,14 @@ impl JoinInlineBoundaryBuilder {
|
||||
boundary.expr_result,
|
||||
boundary.exit_bindings.as_slice(),
|
||||
);
|
||||
|
||||
// Phase 287 P2: Default loop header function name for loop patterns.
|
||||
// If a pattern sets loop_var_name, it must have a loop header function.
|
||||
if boundary.loop_var_name.is_some() && boundary.loop_header_func_name.is_none() {
|
||||
boundary.loop_header_func_name =
|
||||
Some(crate::mir::join_ir::lowering::canonical_names::LOOP_STEP.to_string());
|
||||
}
|
||||
|
||||
boundary
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user