wip(joinir): Phase 256 P1.10 - classify_tail_call continuation support
## 修正内容 - classify_tail_call に is_target_continuation パラメータ追加 - k_exit 呼び出しを ExitJump として分類(BackEdge に誤分類されるのを防止) - entry_func_name の決定ロジックを修正(loop_step を正しく識別) - Continuation param bindings の Copy 生成ロジック追加 ## 残存問題 bb8/bb9 の instructions が 0 になる問題 - add_block 時は 4 instructions ある - printer 時は instructions=0, spans=4 - 原因調査中 🔍 WIP - ChatGPT レビュー用 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -210,6 +210,10 @@ pub(super) fn merge_and_rewrite(
|
||||
function_entry_map.insert(func_name.clone(), entry_block_new);
|
||||
}
|
||||
|
||||
// Phase 256 P1.10: Pre-pass removed - param mappings are set up during block processing
|
||||
// The issue was that pre-pass used stale remapper values before Phase 33-21 updates.
|
||||
// Instead, we now generate Copies for non-carrier params in the main block processing loop.
|
||||
|
||||
// DETERMINISM FIX: Sort functions by name to ensure consistent iteration order
|
||||
if debug {
|
||||
log!(
|
||||
@ -222,7 +226,22 @@ pub(super) fn merge_and_rewrite(
|
||||
let mut functions_merge: Vec<_> = mir_module.functions.iter().collect();
|
||||
functions_merge.sort_by_key(|(name, _)| name.as_str());
|
||||
|
||||
let entry_func_name = functions_merge.first().map(|(name, _)| name.as_str());
|
||||
// Phase 256 P1.10: Determine actual entry function (loop header)
|
||||
//
|
||||
// The entry function is the one that:
|
||||
// - Is NOT a continuation function (k_exit, etc.)
|
||||
// - Is NOT "main" (the trampoline that calls the loop entry)
|
||||
//
|
||||
// This is the function that receives boundary inputs and is the 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());
|
||||
|
||||
for (func_name, func) in functions_merge {
|
||||
// Phase 33-15: Identify continuation functions (k_exit, etc.)
|
||||
@ -301,6 +320,23 @@ pub(super) fn merge_and_rewrite(
|
||||
entry_func_name == Some(func_name.as_str()) && *old_block_id == func.entry_block;
|
||||
|
||||
// DEBUG: Print block being processed
|
||||
// Phase 256 P1.10: Always log block mapping for debugging
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Block mapping: func='{}' old={:?} → new={:?} (inst_count={})",
|
||||
func_name, old_block_id, new_block_id, old_block.instructions.len()
|
||||
);
|
||||
|
||||
// Phase 256 P1.10 DEBUG: Log first instruction for k_exit to trace content placement
|
||||
if func_name == "k_exit" {
|
||||
if let Some(first_inst) = old_block.instructions.first() {
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: k_exit first instruction: {:?}",
|
||||
first_inst
|
||||
);
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log!(
|
||||
true,
|
||||
@ -552,8 +588,40 @@ pub(super) fn merge_and_rewrite(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target_func_name) = target_func_name {
|
||||
if let Some(target_params) = function_params.get(&target_func_name) {
|
||||
// Phase 256 P1.10: Define is_target_continuation for classify_tail_call
|
||||
// This must be defined before the inner if lets so it's in scope for classify_tail_call
|
||||
let is_target_continuation = target_func_name
|
||||
.as_ref()
|
||||
.map(|name| continuation_candidates.contains(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
if let Some(ref target_func_name) = target_func_name {
|
||||
if let Some(target_params) = function_params.get(target_func_name) {
|
||||
// Phase 256 P1.10: Detect call type for param binding strategy
|
||||
// - Recursive call (loop_step → loop_step): Skip all param bindings (PHI handles via latch edges)
|
||||
// - Exit call (loop_step → k_exit): Skip all param bindings (exit PHI handles via exit edges)
|
||||
// - Header entry (main → loop_step at header): Skip all (PHI handles via entry edges)
|
||||
let is_recursive_call = target_func_name == func_name;
|
||||
|
||||
// Phase 256 P1.10: Detect if target is the loop entry function
|
||||
// When calling the loop entry function, header PHIs will define the carriers.
|
||||
// We should skip param bindings in this case.
|
||||
let is_target_loop_entry = entry_func_name == Some(target_func_name.as_str());
|
||||
|
||||
// Phase 256 P1.10: Detect if target is a continuation (k_exit)
|
||||
// k_exit params are exit binding values, which are remapped to exit PHI dsts.
|
||||
// Generating Copies for k_exit would create multiple definitions (Copy + PHI).
|
||||
// Use continuation_candidates (not skippable_continuation_func_names) because
|
||||
// k_exit may have instructions (non-skippable) but still needs param skip.
|
||||
let is_target_continuation = continuation_candidates.contains(target_func_name);
|
||||
|
||||
// Phase 256 P1.10 DEBUG: Always log call type detection
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Tail call from '{}' to '{}' (is_recursive={}, is_loop_entry={}, is_target_loop_entry={}, is_target_continuation={})",
|
||||
func_name, target_func_name, is_recursive_call, is_loop_entry_point, is_target_loop_entry, is_target_continuation
|
||||
);
|
||||
|
||||
// Phase 33-21: Skip parameter binding in header block
|
||||
//
|
||||
// The header block (loop entry point) has PHIs that define carriers.
|
||||
@ -572,6 +640,58 @@ pub(super) fn merge_and_rewrite(
|
||||
"[cf_loop/joinir] Phase 33-21: Skip param bindings in header block (PHIs define carriers)"
|
||||
);
|
||||
}
|
||||
} else if is_recursive_call || is_target_loop_entry {
|
||||
// Phase 256 P1.10: Skip param bindings for:
|
||||
// - Recursive call (loop_step → loop_step): latch edge
|
||||
// - Entry call (main → loop_step): entry edge
|
||||
//
|
||||
// Header PHIs receive values from these edges via separate mechanism.
|
||||
// Generating Copies here would cause multiple definitions.
|
||||
//
|
||||
// Update remapper mappings for any further instructions.
|
||||
for (i, arg_val_remapped) in args.iter().enumerate() {
|
||||
if i < target_params.len() {
|
||||
let param_val_original = target_params[i];
|
||||
remapper.set_value(param_val_original, *arg_val_remapped);
|
||||
}
|
||||
}
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Skip Copy bindings for {} call to '{}' (remapper updated)",
|
||||
if is_recursive_call { "recursive" } else { "entry" },
|
||||
target_func_name
|
||||
);
|
||||
} else if is_target_continuation {
|
||||
// Phase 256 P1.10: Continuation call (loop_step → k_exit)
|
||||
// k_exit body uses its params to compute return values.
|
||||
// We must Copy call args to ORIGINAL params (not remapped).
|
||||
// Params are excluded from remapping (Phase 256 P1.10 exit_binding skip),
|
||||
// so using original params avoids conflict with exit PHI.
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Before continuation Copies, new_block {:?} has {} instructions",
|
||||
new_block_id, new_block.instructions.len()
|
||||
);
|
||||
for (i, arg_val_remapped) in args.iter().enumerate() {
|
||||
if i < target_params.len() {
|
||||
let param_val_original = target_params[i];
|
||||
// Use original param as dst - it won't be remapped
|
||||
new_block.instructions.push(MirInstruction::Copy {
|
||||
dst: param_val_original,
|
||||
src: *arg_val_remapped,
|
||||
});
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Continuation param binding: {:?} = copy {:?}",
|
||||
param_val_original, arg_val_remapped
|
||||
);
|
||||
}
|
||||
}
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: After continuation Copies, new_block {:?} has {} instructions",
|
||||
new_block_id, new_block.instructions.len()
|
||||
);
|
||||
} else {
|
||||
// Insert Copy instructions for parameter binding
|
||||
// Phase 131-6 FIX: Skip Copy when dst is a header PHI destination
|
||||
@ -579,40 +699,49 @@ pub(super) fn merge_and_rewrite(
|
||||
for (i, arg_val_remapped) in args.iter().enumerate() {
|
||||
if i < target_params.len() {
|
||||
let param_val_original = target_params[i];
|
||||
if let Some(param_val_remapped) =
|
||||
remapper.get_value(param_val_original)
|
||||
{
|
||||
// Phase 131-6: Check if this would overwrite a header PHI dst
|
||||
let is_header_phi_dst = loop_header_phi_info
|
||||
.carrier_phis
|
||||
.values()
|
||||
.any(|entry| entry.phi_dst == param_val_remapped);
|
||||
// Phase 256 P1.10: Use original param if not remapped
|
||||
// Params are excluded from used_values to prevent undefined remaps,
|
||||
// so we use the original param ValueId as the Copy dst.
|
||||
let param_remap_result = remapper.get_value(param_val_original);
|
||||
let param_val_dst = param_remap_result
|
||||
.unwrap_or(param_val_original);
|
||||
|
||||
if is_header_phi_dst {
|
||||
if debug {
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 131-6: Skip param binding to PHI dst {:?} (PHI receives value via incoming edge)",
|
||||
param_val_remapped
|
||||
);
|
||||
}
|
||||
// Skip - PHI will receive this value as incoming edge
|
||||
continue;
|
||||
}
|
||||
// Phase 256 P1.10 DEBUG: Log remap decision
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Param[{}] original={:?} remap_result={:?} final_dst={:?}",
|
||||
i, param_val_original, param_remap_result, param_val_dst
|
||||
);
|
||||
|
||||
new_block.instructions.push(MirInstruction::Copy {
|
||||
dst: param_val_remapped,
|
||||
src: *arg_val_remapped,
|
||||
});
|
||||
// Phase 131-6: Check if this would overwrite a header PHI dst
|
||||
let is_header_phi_dst = loop_header_phi_info
|
||||
.carrier_phis
|
||||
.values()
|
||||
.any(|entry| entry.phi_dst == param_val_dst);
|
||||
|
||||
if is_header_phi_dst {
|
||||
if debug {
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Param binding: arg {:?} → param {:?}",
|
||||
arg_val_remapped, param_val_remapped
|
||||
"[cf_loop/joinir] Phase 131-6: Skip param binding to PHI dst {:?} (PHI receives value via incoming edge)",
|
||||
param_val_dst
|
||||
);
|
||||
}
|
||||
// Skip - PHI will receive this value as incoming edge
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase 256 P1.10 DEBUG: Always log Copy generation
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Generating Copy: {:?} = copy {:?} (func='{}', target='{}')",
|
||||
param_val_dst, arg_val_remapped, func_name, target_func_name
|
||||
);
|
||||
|
||||
new_block.instructions.push(MirInstruction::Copy {
|
||||
dst: param_val_dst,
|
||||
src: *arg_val_remapped,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -726,10 +855,13 @@ pub(super) fn merge_and_rewrite(
|
||||
// - LoopEntry: First entry (entry function's entry block) → no redirect
|
||||
// - BackEdge: Loop continuation (other blocks) → redirect to header PHI
|
||||
// - ExitJump: Exit to continuation → handled by Return conversion
|
||||
// Phase 256 P1.10: Add is_target_continuation to prevent k_exit calls
|
||||
// from being redirected to header block.
|
||||
let tail_call_kind = classify_tail_call(
|
||||
is_loop_entry_point,
|
||||
!loop_header_phi_info.carrier_phis.is_empty(),
|
||||
boundary.is_some(),
|
||||
is_target_continuation,
|
||||
);
|
||||
|
||||
let actual_target = match tail_call_kind {
|
||||
@ -1003,6 +1135,17 @@ pub(super) fn merge_and_rewrite(
|
||||
|
||||
// Add block to current function
|
||||
if let Some(ref mut current_func) = builder.scope_ctx.current_function {
|
||||
// Phase 256 P1.10 DEBUG: Log block content before adding (for blocks with multiple instructions)
|
||||
if new_block.instructions.len() >= 4 {
|
||||
log!(
|
||||
true,
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Adding block {:?} with {} instructions to function",
|
||||
new_block_id, new_block.instructions.len()
|
||||
);
|
||||
for (idx, inst) in new_block.instructions.iter().enumerate() {
|
||||
log!(true, "[cf_loop/joinir] Phase 256 P1.10 DEBUG: [{}] {:?}", idx, inst);
|
||||
}
|
||||
}
|
||||
current_func.add_block(new_block);
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,16 +230,33 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
used_values.insert(binding.join_value);
|
||||
}
|
||||
|
||||
// Phase 172-3: Add exit_bindings' join_exit_values to used_values for remapping
|
||||
// Phase 172-3 + Phase 256 P1.10: Add exit_bindings' join_exit_values to used_values
|
||||
// UNLESS they are function params. Params should NOT be remapped (they're defined
|
||||
// by call site Copies and used directly in k_exit body).
|
||||
let all_params: std::collections::HashSet<ValueId> = function_params
|
||||
.values()
|
||||
.flat_map(|params| params.iter().copied())
|
||||
.collect();
|
||||
|
||||
for binding in &boundary.exit_bindings {
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 172-3: Adding exit binding '{}' JoinIR {:?} to used_values",
|
||||
binding.carrier_name, binding.join_exit_value
|
||||
),
|
||||
debug,
|
||||
);
|
||||
used_values.insert(binding.join_exit_value);
|
||||
if all_params.contains(&binding.join_exit_value) {
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Skipping exit binding '{}' (JoinIR {:?} is a param)",
|
||||
binding.carrier_name, binding.join_exit_value
|
||||
),
|
||||
debug,
|
||||
);
|
||||
} else {
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 172-3: Adding exit binding '{}' JoinIR {:?} to used_values",
|
||||
binding.carrier_name, binding.join_exit_value
|
||||
),
|
||||
debug,
|
||||
);
|
||||
used_values.insert(binding.join_exit_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,10 +270,17 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
let mut loop_header_phi_info = if let Some(boundary) = boundary {
|
||||
if let Some(loop_var_name) = &boundary.loop_var_name {
|
||||
// Get entry function and block for building PHI info
|
||||
// Phase 256 P1.10: Find the actual entry function (loop header)
|
||||
// The entry function is NOT a continuation (k_exit) and NOT "main".
|
||||
let (entry_func_name, entry_func) = mir_module
|
||||
.functions
|
||||
.iter()
|
||||
.next()
|
||||
.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
|
||||
})
|
||||
.or_else(|| mir_module.functions.iter().next())
|
||||
.ok_or("JoinIR module has no functions (Phase 201-A)")?;
|
||||
let entry_block_remapped = remapper
|
||||
.get_block(entry_func_name, entry_func.entry_block)
|
||||
@ -310,6 +334,14 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Phase 256 P1.10: Always log entry function determination
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 256 P1.10: Entry function='{}', entry_block_remapped={:?}",
|
||||
entry_func_name, entry_block_remapped
|
||||
),
|
||||
true, // Always log for debugging
|
||||
);
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 201-A: Pre-building header PHIs for loop_var='{}' at {:?}",
|
||||
|
||||
@ -37,6 +37,7 @@ pub enum TailCallKind {
|
||||
/// * `is_entry_func_entry_block` - True if this is the first function's first block (loop entry point)
|
||||
/// * `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)
|
||||
///
|
||||
/// # Returns
|
||||
/// The classification of this tail call
|
||||
@ -44,7 +45,15 @@ pub fn classify_tail_call(
|
||||
is_entry_func_entry_block: bool,
|
||||
has_loop_header_phis: bool,
|
||||
has_boundary: bool,
|
||||
is_target_continuation: bool,
|
||||
) -> TailCallKind {
|
||||
// Phase 256 P1.10: Continuation calls (k_exit) are always ExitJump
|
||||
// They should NOT be redirected to the header block.
|
||||
// k_exit body needs to execute before exiting.
|
||||
if is_target_continuation {
|
||||
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 {
|
||||
@ -68,9 +77,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_classify_loop_entry() {
|
||||
let result = classify_tail_call(
|
||||
true, // is_entry_func_entry_block
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
true, // is_entry_func_entry_block
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
);
|
||||
assert_eq!(result, TailCallKind::LoopEntry);
|
||||
}
|
||||
@ -81,6 +91,7 @@ mod tests {
|
||||
false, // is_entry_func_entry_block (not entry block)
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
);
|
||||
assert_eq!(result, TailCallKind::BackEdge);
|
||||
}
|
||||
@ -91,6 +102,7 @@ mod tests {
|
||||
false, // is_entry_func_entry_block
|
||||
false, // has_loop_header_phis (no header PHIs)
|
||||
true, // has_boundary
|
||||
false, // is_target_continuation
|
||||
);
|
||||
assert_eq!(result, TailCallKind::ExitJump);
|
||||
}
|
||||
@ -101,6 +113,20 @@ mod tests {
|
||||
false, // is_entry_func_entry_block
|
||||
true, // has_loop_header_phis
|
||||
false, // has_boundary (no boundary → exit)
|
||||
false, // is_target_continuation
|
||||
);
|
||||
assert_eq!(result, TailCallKind::ExitJump);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_continuation_target() {
|
||||
// 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
|
||||
true, // has_loop_header_phis
|
||||
true, // has_boundary
|
||||
true, // is_target_continuation ← this makes it ExitJump
|
||||
);
|
||||
assert_eq!(result, TailCallKind::ExitJump);
|
||||
}
|
||||
|
||||
@ -52,6 +52,15 @@ 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
|
||||
trace.stderr_if(
|
||||
&format!(
|
||||
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Function '{}' params: {:?}",
|
||||
func_name, func.params
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
for block in func.blocks.values() {
|
||||
// Phase 189: Use remapper to collect values
|
||||
let block_values = remapper.collect_values_in_block(block);
|
||||
@ -81,7 +90,7 @@ pub(super) fn collect_values(
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 33-15: DO NOT collect parameter ValueIds into used_values
|
||||
// Phase 33-15 / Phase 256 P1.10: DO NOT remap parameter ValueIds
|
||||
//
|
||||
// Reasoning: Parameters are implicitly defined at function entry in JoinIR.
|
||||
// When inlined into host MIR, if parameters are remapped, the remapped ValueIds
|
||||
@ -92,13 +101,12 @@ pub(super) fn collect_values(
|
||||
// 1. BoundaryInjector Copy (for entry function: host_input → join_param)
|
||||
// 2. Tail call Copy (for recursive calls: call_arg → param)
|
||||
//
|
||||
// We still store parameters in function_params for tail call handling,
|
||||
// but don't add them to used_values so they won't be remapped.
|
||||
//
|
||||
// OLD CODE (removed):
|
||||
// for param in &func.params {
|
||||
// used_values.insert(*param);
|
||||
// }
|
||||
// Phase 256 P1.10: We must REMOVE params from used_values after block collection,
|
||||
// because collect_values_in_block() adds all operand ValueIds including params.
|
||||
// Without this removal, params get remapped and their remapped values are undefined.
|
||||
for param in &func.params {
|
||||
used_values.remove(param);
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
|
||||
Reference in New Issue
Block a user