feat(joinir): Phase 200-B/C/D capture analysis + Phase 201-A reserved_value_ids infra
Phase 200-B: FunctionScopeCaptureAnalyzer implementation - analyze_captured_vars_v2() with structural loop matching - CapturedEnv for immutable function-scope variables - ParamRole::Condition for condition-only variables Phase 200-C: ConditionEnvBuilder extension - build_with_captures() integrates CapturedEnv into ConditionEnv - fn_body propagation through LoopPatternContext to Pattern 2 Phase 200-D: E2E verification - capture detection working for base, limit, n etc. - Test files: phase200d_capture_minimal.hako, phase200d_capture_in_condition.hako Phase 201-A: MirBuilder reserved_value_ids infrastructure - reserved_value_ids: HashSet<ValueId> field in MirBuilder - next_value_id() skips reserved IDs - merge/mod.rs sets/clears reserved IDs around JoinIR merge Phase 201: JoinValueSpace design document - Param/Local/PHI disjoint regions design - API: alloc_param(), alloc_local(), reserve_phi() - Migration plan for Pattern 1-4 lowerers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -111,8 +111,93 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Remap ValueIds
|
||||
remap_values(builder, &used_values, &mut remapper, debug)?;
|
||||
// Phase 201-A: Build loop header PHI info BEFORE Phase 3
|
||||
//
|
||||
// We need to allocate PHI dst ValueIds before remap_values() runs,
|
||||
// to prevent conflicts where a Const instruction gets a ValueId that
|
||||
// will later be used as a PHI dst, causing carrier value corruption.
|
||||
//
|
||||
// This is a reordering of Phase 3 and Phase 3.5 logic.
|
||||
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
|
||||
let (entry_func_name, entry_func) = 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)
|
||||
.ok_or_else(|| format!("Entry block not found for {} (Phase 201-A)", entry_func_name))?;
|
||||
|
||||
// Get host's current block as the entry edge
|
||||
let host_entry_block = builder.current_block.ok_or(
|
||||
"Phase 201-A: No current block when building header PHIs"
|
||||
)?;
|
||||
|
||||
// Get loop variable's initial value from HOST
|
||||
let loop_var_init = boundary.host_inputs.first().copied().ok_or(
|
||||
"Phase 201-A: No host_inputs in boundary for loop_var_init"
|
||||
)?;
|
||||
|
||||
// Extract other carriers from exit_bindings
|
||||
let other_carriers: Vec<(String, ValueId)> = boundary.exit_bindings
|
||||
.iter()
|
||||
.filter(|b| b.carrier_name != *loop_var_name)
|
||||
.map(|b| (b.carrier_name.clone(), b.host_slot))
|
||||
.collect();
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 201-A: Pre-building header PHIs for loop_var='{}' at {:?}",
|
||||
loop_var_name, entry_block_remapped
|
||||
);
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] loop_var_init={:?}, carriers={:?}",
|
||||
loop_var_init,
|
||||
other_carriers.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
// Build PHI info (this allocates PHI dst ValueIds)
|
||||
LoopHeaderPhiBuilder::build(
|
||||
builder,
|
||||
entry_block_remapped,
|
||||
host_entry_block,
|
||||
loop_var_name,
|
||||
loop_var_init,
|
||||
&other_carriers,
|
||||
boundary.expr_result.is_some(),
|
||||
debug,
|
||||
)?
|
||||
} else {
|
||||
LoopHeaderPhiInfo::empty(remapper.get_block(
|
||||
mir_module.functions.iter().next().unwrap().0,
|
||||
mir_module.functions.iter().next().unwrap().1.entry_block
|
||||
).unwrap())
|
||||
}
|
||||
} else {
|
||||
LoopHeaderPhiInfo::empty(remapper.get_block(
|
||||
mir_module.functions.iter().next().unwrap().0,
|
||||
mir_module.functions.iter().next().unwrap().1.entry_block
|
||||
).unwrap())
|
||||
};
|
||||
|
||||
// Phase 201-A: Get reserved PHI dst ValueIds and set in MirBuilder
|
||||
let reserved_phi_dsts = loop_header_phi_info.reserved_value_ids();
|
||||
if debug && !reserved_phi_dsts.is_empty() {
|
||||
eprintln!("[cf_loop/joinir] Phase 201-A: Reserved PHI dsts: {:?}", reserved_phi_dsts);
|
||||
}
|
||||
|
||||
// Phase 201-A: Set reserved IDs in MirBuilder so next_value_id() skips them
|
||||
// This protects against carrier corruption when break conditions emit Const instructions
|
||||
builder.reserved_value_ids = reserved_phi_dsts.clone();
|
||||
if debug && !builder.reserved_value_ids.is_empty() {
|
||||
eprintln!("[cf_loop/joinir] Phase 201-A: Set builder.reserved_value_ids = {:?}", builder.reserved_value_ids);
|
||||
}
|
||||
|
||||
// Phase 3: Remap ValueIds (with reserved PHI dsts protection)
|
||||
remap_values(builder, &used_values, &mut remapper, &reserved_phi_dsts, debug)?;
|
||||
|
||||
// Phase 177-3 DEBUG: Verify remapper state after Phase 3
|
||||
eprintln!("[DEBUG-177] === Remapper state after Phase 3 ===");
|
||||
@ -138,86 +223,13 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
}
|
||||
eprintln!("[DEBUG-177] ==============================");
|
||||
|
||||
// Phase 3.5: Build loop header PHIs (if loop pattern with loop_var_name)
|
||||
// Phase 3.5: Override remapper for function parameters to use PHI dsts
|
||||
//
|
||||
// We need to know PHI dsts before instruction_rewriter runs, so that:
|
||||
// 1. Tail call handling can set latch_incoming
|
||||
// 2. Return handling can use PHI dsts for exit values
|
||||
let (entry_func_name, entry_func) = mir_module
|
||||
.functions
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or("JoinIR module has no functions (Phase 3.5)")?;
|
||||
let entry_block_remapped = remapper
|
||||
.get_block(entry_func_name, entry_func.entry_block)
|
||||
.ok_or_else(|| format!("Entry block not found for {} (Phase 3.5)", entry_func_name))?;
|
||||
|
||||
// Phase 33-16: Get host's current block as the entry edge (the block that jumps INTO the loop)
|
||||
let host_entry_block = builder.current_block.ok_or(
|
||||
"Phase 33-16: No current block when building header PHIs"
|
||||
)?;
|
||||
|
||||
let mut loop_header_phi_info = if let Some(boundary) = boundary {
|
||||
// Phase 201-A: This phase now uses the loop_header_phi_info built before Phase 3.
|
||||
// The PHI dst allocation has been moved earlier to prevent ValueId conflicts.
|
||||
if let Some(boundary) = boundary {
|
||||
if let Some(loop_var_name) = &boundary.loop_var_name {
|
||||
// Phase 33-16: Get loop variable's initial value from HOST (not JoinIR's ValueId(0))
|
||||
// boundary.host_inputs[0] is the host ValueId that holds the initial loop var value
|
||||
let loop_var_init = boundary.host_inputs.first().copied().ok_or(
|
||||
"Phase 33-16: No host_inputs in boundary for loop_var_init"
|
||||
)?;
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 3.5: Building header PHIs for loop_var='{}' at {:?}",
|
||||
loop_var_name, entry_block_remapped
|
||||
);
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] loop_var_init={:?} (from boundary.host_inputs[0])",
|
||||
loop_var_init
|
||||
);
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] host_entry_block={:?} (where initial value comes from)",
|
||||
host_entry_block
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 33-20: Extract other carriers from exit_bindings
|
||||
// Skip the loop variable (it's handled separately) and collect other carriers
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 33-20 DEBUG: exit_bindings count={}, loop_var_name={:?}",
|
||||
boundary.exit_bindings.len(),
|
||||
loop_var_name
|
||||
);
|
||||
for b in boundary.exit_bindings.iter() {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 33-20 DEBUG: exit_binding: carrier_name={:?}, host_slot={:?}",
|
||||
b.carrier_name, b.host_slot
|
||||
);
|
||||
}
|
||||
|
||||
let other_carriers: Vec<(String, ValueId)> = boundary.exit_bindings
|
||||
.iter()
|
||||
.filter(|b| b.carrier_name != *loop_var_name)
|
||||
.map(|b| (b.carrier_name.clone(), b.host_slot))
|
||||
.collect();
|
||||
|
||||
if debug && !other_carriers.is_empty() {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 33-20: Found {} other carriers from exit_bindings: {:?}",
|
||||
other_carriers.len(),
|
||||
other_carriers.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
let phi_info = LoopHeaderPhiBuilder::build(
|
||||
builder,
|
||||
entry_block_remapped, // header_block (JoinIR's entry block = loop header)
|
||||
host_entry_block, // entry_block (host's block that jumps to loop header)
|
||||
loop_var_name,
|
||||
loop_var_init,
|
||||
&other_carriers, // Phase 33-20: Pass other carriers from exit_bindings
|
||||
boundary.expr_result.is_some(), // expr_result_is_loop_var
|
||||
debug,
|
||||
)?;
|
||||
// Phase 201-A: PHI info is already built (before Phase 3) - just use it
|
||||
|
||||
// Phase 33-21: Override remapper for loop_step's parameters
|
||||
//
|
||||
@ -295,13 +307,13 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
);
|
||||
eprintln!(
|
||||
"[DEBUG-177] Phase 33-21: carrier_phis count: {}, names: {:?}",
|
||||
phi_info.carrier_phis.len(),
|
||||
phi_info.carrier_phis.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>()
|
||||
loop_header_phi_info.carrier_phis.len(),
|
||||
loop_header_phi_info.carrier_phis.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
// Map main's parameters to header PHI dsts
|
||||
// main params: [i_init, carrier1_init, ...]
|
||||
// carrier_phis: [("i", entry), ("sum", entry), ...]
|
||||
for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() {
|
||||
for (idx, (carrier_name, entry)) in loop_header_phi_info.carrier_phis.iter().enumerate() {
|
||||
if let Some(&main_param) = main_params.get(idx) {
|
||||
// Phase 177-3: Don't override condition_bindings
|
||||
if condition_binding_ids.contains(&main_param) {
|
||||
@ -323,7 +335,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
// Phase 177-3-B: Handle body-only carriers
|
||||
// These are carriers in carrier_phis that are NOT in main function params.
|
||||
// They appear in condition_bindings (added by Phase 176-5) but need PHI remapping.
|
||||
for (carrier_name, entry) in &phi_info.carrier_phis {
|
||||
for (carrier_name, entry) in &loop_header_phi_info.carrier_phis {
|
||||
// Check if this carrier has a condition_binding
|
||||
if let Some(binding) = boundary.condition_bindings.iter().find(|cb| cb.name == *carrier_name) {
|
||||
// Skip if it's a true condition-only variable (already protected above)
|
||||
@ -376,7 +388,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
// Check if this param was already handled by Phase 177-3-B
|
||||
let already_mapped = boundary.condition_bindings.iter().any(|cb| {
|
||||
cb.join_value == *loop_step_param &&
|
||||
phi_info.carrier_phis.iter().any(|(name, _)| name == &cb.name)
|
||||
loop_header_phi_info.carrier_phis.iter().any(|(name, _)| name == &cb.name)
|
||||
});
|
||||
if already_mapped {
|
||||
eprintln!(
|
||||
@ -394,8 +406,8 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
if let Some(param_idx) = loop_step_params.iter().position(|p| p == loop_step_param) {
|
||||
// Map params[i] to carrier_order[i]
|
||||
if let (Some(carrier_name), Some(entry)) = (
|
||||
phi_info.get_carrier_at_index(param_idx),
|
||||
phi_info.get_entry_at_index(param_idx),
|
||||
loop_header_phi_info.get_carrier_at_index(param_idx),
|
||||
loop_header_phi_info.get_entry_at_index(param_idx),
|
||||
) {
|
||||
eprintln!(
|
||||
"[DEBUG-177] Phase 177-STRUCT-2: REMAP loop_step param[{}] {:?} → {:?} (carrier '{}')",
|
||||
@ -410,7 +422,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
if function_params.get(main_func_name).is_none() && function_params.get(loop_step_func_name).is_none() {
|
||||
// Fallback: Use old behavior (ValueId(0), ValueId(1), ...)
|
||||
// This handles patterns that don't have loop_step function
|
||||
if let Some(phi_dst) = phi_info.get_carrier_phi(loop_var_name) {
|
||||
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(loop_var_name) {
|
||||
// Phase 177-3: Don't override condition_bindings
|
||||
if !condition_binding_ids.contains(&ValueId(0)) {
|
||||
remapper.set_value(ValueId(0), phi_dst);
|
||||
@ -427,11 +439,11 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
}
|
||||
}
|
||||
// Phase 177-STRUCT-2: Use carrier_order for deterministic iteration
|
||||
for (idx, carrier_name) in phi_info.carrier_order.iter().enumerate() {
|
||||
for (idx, carrier_name) in loop_header_phi_info.carrier_order.iter().enumerate() {
|
||||
if carrier_name == loop_var_name {
|
||||
continue;
|
||||
}
|
||||
let entry = match phi_info.carrier_phis.get(carrier_name) {
|
||||
let entry = match loop_header_phi_info.carrier_phis.get(carrier_name) {
|
||||
Some(e) => e,
|
||||
None => continue,
|
||||
};
|
||||
@ -464,13 +476,9 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
);
|
||||
}
|
||||
|
||||
phi_info
|
||||
} else {
|
||||
LoopHeaderPhiInfo::empty(entry_block_remapped)
|
||||
// Phase 201-A: loop_header_phi_info already built (no assignment needed)
|
||||
}
|
||||
} else {
|
||||
LoopHeaderPhiInfo::empty(entry_block_remapped)
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 4: Merge blocks and rewrite instructions
|
||||
// Phase 33-16: Pass mutable loop_header_phi_info for latch_incoming tracking
|
||||
@ -541,17 +549,12 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
|
||||
let exit_block_id = merge_result.exit_block_id;
|
||||
|
||||
// Jump from current block to entry function's entry block
|
||||
// (Reuse entry_func_name and entry_block_remapped from Phase 3.5)
|
||||
let entry_block = entry_block_remapped;
|
||||
// Phase 201-A: Get entry block from loop_header_phi_info
|
||||
// The header_block in loop_header_phi_info is the remapped entry block
|
||||
let entry_block = loop_header_phi_info.header_block;
|
||||
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Entry function name: {}", entry_func_name);
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Entry function's entry_block (JoinIR local): {:?}",
|
||||
entry_func.entry_block
|
||||
);
|
||||
eprintln!("[cf_loop/joinir] Entry block (remapped): {:?}", entry_block);
|
||||
eprintln!("[cf_loop/joinir] Entry block (from loop_header_phi_info): {:?}", entry_block);
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Current block before emit_jump: {:?}",
|
||||
builder.current_block
|
||||
@ -598,22 +601,48 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 201-A: Clear reserved ValueIds after merge completes
|
||||
// Future loops will set their own reserved IDs
|
||||
if !builder.reserved_value_ids.is_empty() {
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Phase 201-A: Clearing reserved_value_ids (was {:?})", builder.reserved_value_ids);
|
||||
}
|
||||
builder.reserved_value_ids.clear();
|
||||
}
|
||||
|
||||
Ok(exit_phi_result_id)
|
||||
}
|
||||
|
||||
/// Phase 3: Allocate new ValueIds for all collected values
|
||||
///
|
||||
/// Phase 201-A: Accept reserved ValueIds that must not be reused.
|
||||
/// These are PHI dst ValueIds that will be created by LoopHeaderPhiBuilder.
|
||||
/// We must skip these IDs to prevent carrier value corruption.
|
||||
fn remap_values(
|
||||
builder: &mut crate::mir::builder::MirBuilder,
|
||||
used_values: &std::collections::BTreeSet<ValueId>,
|
||||
remapper: &mut crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper,
|
||||
reserved_ids: &std::collections::HashSet<ValueId>,
|
||||
debug: bool,
|
||||
) -> Result<(), String> {
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Phase 3: Remapping {} ValueIds", used_values.len());
|
||||
eprintln!("[cf_loop/joinir] Phase 3: Remapping {} ValueIds (reserved: {})",
|
||||
used_values.len(), reserved_ids.len());
|
||||
}
|
||||
|
||||
for old_value in used_values {
|
||||
let new_value = builder.next_value_id();
|
||||
// Phase 201-A: Allocate new ValueId, skipping reserved PHI dsts
|
||||
let new_value = loop {
|
||||
let candidate = builder.next_value_id();
|
||||
if !reserved_ids.contains(&candidate) {
|
||||
break candidate;
|
||||
}
|
||||
// Skip reserved ID - will try next one
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Phase 201-A: Skipping reserved PHI dst {:?}", candidate);
|
||||
}
|
||||
};
|
||||
|
||||
remapper.set_value(*old_value, new_value);
|
||||
if debug {
|
||||
eprintln!(
|
||||
|
||||
Reference in New Issue
Block a user