fix(joinir): Phase 177-3 ValueId collision fix for multi-carrier loops

Root cause: JoinIR ValueId collision between function parameters and condition bindings
- Same ValueId used for both `result_init` (carrier param) and `limit` (condition var)
- Phase 33-21 was overwriting condition bindings when remapping carrier PHIs

Fix implemented (Option B - immediate protection):
1. Phase 177-3: Protect condition-only variables from Phase 33-21 override
   - Collect condition_bindings that are NOT carriers (by checking exit_bindings)
   - Skip remapping for these protected ValueIds
2. Phase 177-3-B: Handle body-only carriers explicitly
   - Carriers that appear in condition_bindings (added by Phase 176-5)
   - Map them to correct PHI dsts by name lookup

Investigation tools added:
- [DEBUG-177] trace logs for remapper state tracking
- Phase 177-3 protection logging
- BoundaryInjector PHI collision detection

Test results:
-  Integer multi-carrier test: Output 3 (expected)
- ⚠️ String test: RC=0 but empty output (separate issue - string concat emit)

Design docs created:
- phase177-parse-string-design.md: _parse_string loop analysis
- phase177-carrier-evolution.md: Carrier progression Phase 174-179

Next: Investigate string concatenation emit for full _parse_string support
This commit is contained in:
nyash-codex
2025-12-08 16:34:04 +09:00
parent 99d329096f
commit 7a01ffe522
6 changed files with 646 additions and 40 deletions

View File

@ -39,13 +39,26 @@ impl BoundaryInjector {
/// When `boundary.loop_var_name` is set, ALL carriers (loop var + other carriers
/// from exit_bindings) are handled by header PHIs. We skip ALL join_inputs
/// Copy instructions to avoid overwriting the PHI results.
///
/// # Phase 177-3: PHI Collision Avoidance (Option B)
///
/// When `phi_dst_ids` contains ValueIds of existing PHI dsts in the header block,
/// we allocate a NEW ValueId for condition_bindings instead of skipping the copy.
/// This ensures the condition variable is available even when its remapped ValueId
/// collides with a PHI dst.
///
/// # Returns
///
/// Returns a HashMap mapping original ValueIds to their reallocated ValueIds
/// (only for condition_bindings that had collisions).
pub fn inject_boundary_copies(
func: &mut MirFunction,
entry_block_id: BasicBlockId,
boundary: &JoinInlineBoundary,
value_map: &HashMap<ValueId, ValueId>,
phi_dst_ids: &std::collections::HashSet<ValueId>,
debug: bool,
) -> Result<(), String> {
) -> Result<HashMap<ValueId, ValueId>, String> {
// Phase 33-20: When loop_var_name is set, ALL join_inputs are handled by header PHIs
// This includes the loop variable AND all other carriers from exit_bindings.
// We skip ALL join_inputs Copy instructions, only condition_bindings remain.
@ -59,7 +72,8 @@ impl BoundaryInjector {
};
let total_inputs = effective_join_inputs + boundary.condition_bindings.len();
if total_inputs == 0 {
return Ok(());
// No inputs to process, return empty reallocations map
return Ok(HashMap::new());
}
if debug {
@ -73,7 +87,38 @@ impl BoundaryInjector {
);
}
// Entry block を取得
// Phase 177-3: Use PHI dst IDs passed from caller
if debug && !phi_dst_ids.is_empty() {
eprintln!(
"[BoundaryInjector] Phase 177-3: Received {} PHI dst IDs to avoid: {:?}",
phi_dst_ids.len(),
phi_dst_ids
);
}
// Phase 177-3 Option B: First pass - allocate all new ValueIds for PHI collisions
// We need to do this BEFORE acquiring the entry_block reference to avoid borrow conflicts
let mut reallocations = HashMap::new();
for binding in &boundary.condition_bindings {
let remapped_join = value_map.get(&binding.join_value).copied().unwrap_or(binding.join_value);
if phi_dst_ids.contains(&remapped_join) {
// Collision detected! Allocate a fresh ValueId
let fresh_dst = func.next_value_id();
if debug {
eprintln!(
"[BoundaryInjector] Phase 177-3 Option B: PHI collision for condition binding '{}': {:?} → reallocated to {:?}",
binding.name, remapped_join, fresh_dst
);
}
reallocations.insert(binding.join_value, fresh_dst);
}
}
// Now get entry block reference (after all ValueId allocations are done)
let entry_block = func
.get_block_mut(entry_block_id)
.ok_or(format!("Entry block {:?} not found", entry_block_id))?;
@ -110,17 +155,34 @@ impl BoundaryInjector {
}
}
// Phase 177-3 DEBUG: Check value_map in BoundaryInjector
if debug {
eprintln!("[DEBUG-177] === BoundaryInjector value_map ===");
for binding in &boundary.condition_bindings {
let lookup = value_map.get(&binding.join_value);
eprintln!(
"[DEBUG-177] '{}': JoinIR {:?}{:?}",
binding.name, binding.join_value, lookup
);
}
}
// Phase 171-fix: Inject Copy instructions for condition_bindings (condition-only variables)
// These variables are read-only and used ONLY in the loop condition.
// Each binding explicitly specifies HOST ValueId → JoinIR ValueId mapping.
// We inject Copy: remapped_join_value = Copy host_value
//
// Phase 177-3 Option B: Use pre-allocated reallocations for PHI collision cases
for binding in &boundary.condition_bindings {
// Look up the remapped JoinIR ValueId from value_map
let remapped_join = value_map.get(&binding.join_value).copied().unwrap_or(binding.join_value);
// Copy instruction: remapped_join_value = Copy host_value
// Phase 177-3 Option B: Check if this binding was reallocated (PHI collision case)
let final_dst = reallocations.get(&binding.join_value).copied().unwrap_or(remapped_join);
// Copy instruction: final_dst = Copy host_value
let copy_inst = MirInstruction::Copy {
dst: remapped_join,
dst: final_dst,
src: binding.host_value,
};
@ -128,8 +190,9 @@ impl BoundaryInjector {
if debug {
eprintln!(
"[BoundaryInjector] Condition binding '{}': Copy {:?} = Copy {:?} (JoinIR {:?} → remapped {:?})",
binding.name, remapped_join, binding.host_value, binding.join_value, remapped_join
"[BoundaryInjector] Condition binding '{}': Copy {:?} = Copy {:?} (JoinIR {:?} → remapped {:?}{})",
binding.name, final_dst, binding.host_value, binding.join_value, remapped_join,
if final_dst != remapped_join { format!(" → reallocated {:?}", final_dst) } else { String::new() }
);
}
}
@ -145,7 +208,8 @@ impl BoundaryInjector {
entry_block.instruction_spans.insert(0, default_span);
}
Ok(())
// Return reallocations map for condition_bindings that had PHI collisions
Ok(reallocations)
}
}