## Summary Root cause: ConditionLoweringBox was bypassing ConditionContext.alloc_value (SSOT allocator), causing ValueId collisions between JoinIR condition params and lowered instructions. ## Changes 1. **ConditionLoweringBox (expr_lowerer.rs)**: Must use ConditionContext.alloc_value - Pass &mut ConditionContext to lower_condition (SSOT allocator) - Eliminates internal counter usage 2. **Allocator unification (condition_lowerer.rs, method_call_lowerer.rs)**: - Accept &mut dyn FnMut() -> ValueId as allocator parameter - Ensures all lowering paths use same SSOT allocator 3. **Boundary Copy deduplication (joinir_inline_boundary_injector.rs)**: - Deduplicate condition_bindings by dst - Fail-Fast if different sources target same dst (MIR SSA violation) 4. **Trim pattern fixes (trim_loop_lowering.rs, trim_pattern_validator.rs, stmts.rs)**: - Use builder.next_value_id() instead of value_gen.next() in function context - Ensures function-level ValueId allocation respects reserved PHI dsts ## Acceptance ✅ ./target/release/hakorune --verify apps/tests/phase133_json_skip_whitespace_min.hako ✅ Smoke: phase135_trim_mir_verify.sh - MIR SSA validation PASS ✅ Regression: phase132_exit_phi_parity.sh - 3/3 PASS ✅ Regression: phase133_json_skip_whitespace_llvm_exe.sh - compile-only PASS 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
359 lines
14 KiB
Rust
359 lines
14 KiB
Rust
//! Phase 188-Impl-3: JoinInlineBoundary Copy Instruction Injector
|
||
//!
|
||
//! 責務:
|
||
//! - JoinInlineBoundary で指定された入出力の Copy instruction 生成
|
||
//! - Entry block への Copy instruction 挿入
|
||
//! - SSA 値空間の接続
|
||
|
||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||
use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
|
||
pub struct BoundaryInjector;
|
||
|
||
impl BoundaryInjector {
|
||
/// JoinInlineBoundary で指定された入力を entry block に Copy instruction として挿入
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `func` - 対象の MirFunction
|
||
/// * `entry_block_id` - entry block の ID
|
||
/// * `boundary` - JoinInlineBoundary(入力マッピング情報)
|
||
/// * `value_map` - ValueId リマッピング情報
|
||
/// * `debug` - デバッグログ出力
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// * `Ok(())` - 成功
|
||
/// * `Err(String)` - エラー(ブロックが見つからない等)
|
||
///
|
||
/// # Phase 33-16: Loop Variable Header PHI Support
|
||
///
|
||
/// When `boundary.loop_var_name` is set, the first join_input (loop variable)
|
||
/// is handled by the header PHI instead of a Copy instruction. We skip
|
||
/// emitting the Copy for join_inputs[0] in this case to avoid overwriting
|
||
/// the PHI result with the initial value.
|
||
///
|
||
/// # Phase 33-20: All Carriers Header PHI Support
|
||
///
|
||
/// 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 BTreeMap mapping original ValueIds to their reallocated ValueIds
|
||
/// (only for condition_bindings that had collisions).
|
||
/// Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
pub fn inject_boundary_copies(
|
||
func: &mut MirFunction,
|
||
entry_block_id: BasicBlockId,
|
||
boundary: &JoinInlineBoundary,
|
||
value_map: &BTreeMap<ValueId, ValueId>, // Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
phi_dst_ids: &std::collections::HashSet<ValueId>,
|
||
debug: bool,
|
||
) -> Result<BTreeMap<ValueId, ValueId>, String> {
|
||
// Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
// 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.
|
||
let skip_all_join_inputs = boundary.loop_var_name.is_some();
|
||
|
||
// Phase 171-fix: Check both join_inputs and condition_bindings
|
||
let effective_join_inputs = if skip_all_join_inputs {
|
||
0 // Phase 33-20: All join_inputs are handled by header PHIs
|
||
} else {
|
||
boundary.join_inputs.len()
|
||
};
|
||
let total_inputs = effective_join_inputs + boundary.condition_bindings.len();
|
||
if total_inputs == 0 {
|
||
// No inputs to process, return empty reallocations map
|
||
// Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
return Ok(BTreeMap::new());
|
||
}
|
||
|
||
if debug {
|
||
eprintln!(
|
||
"[BoundaryInjector] Phase 33-20: Injecting {} Copy instructions ({} join_inputs, {} condition_bindings) at entry block {:?}{}",
|
||
total_inputs,
|
||
effective_join_inputs,
|
||
boundary.condition_bindings.len(),
|
||
entry_block_id,
|
||
if skip_all_join_inputs { " (skipping ALL join_inputs - handled by header PHIs)" } else { "" }
|
||
);
|
||
}
|
||
|
||
// 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
|
||
// Phase 222.5-E: HashMap → BTreeMap for determinism
|
||
let mut reallocations = BTreeMap::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))?;
|
||
|
||
// Copy instructions を生成して挿入
|
||
let mut copy_instructions = Vec::new();
|
||
|
||
// Phase 171: Inject Copy instructions for join_inputs (loop parameters)
|
||
// Phase 33-20: Skip ALL join_inputs when loop_var_name is set (header PHIs handle them)
|
||
if !skip_all_join_inputs {
|
||
for (join_input, host_input) in
|
||
boundary.join_inputs.iter().zip(boundary.host_inputs.iter())
|
||
{
|
||
// リマップ後の ValueId を取得
|
||
let remapped_join = value_map.get(join_input).copied().unwrap_or(*join_input);
|
||
let remapped_host = *host_input; // host_input is already in host space
|
||
|
||
// Copy instruction: remapped_join = Copy remapped_host
|
||
let copy_inst = MirInstruction::Copy {
|
||
dst: remapped_join,
|
||
src: remapped_host,
|
||
};
|
||
|
||
copy_instructions.push(copy_inst);
|
||
|
||
if debug {
|
||
eprintln!(
|
||
"[BoundaryInjector] Join input: Copy {:?} = Copy {:?}",
|
||
remapped_join, remapped_host
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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
|
||
let mut seen_dst_to_src: BTreeMap<ValueId, ValueId> = BTreeMap::new();
|
||
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);
|
||
|
||
// 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);
|
||
|
||
// Phase 135-P0: Deduplicate condition bindings that alias the same JoinIR ValueId.
|
||
//
|
||
// Promoters may produce multiple names that map to the same JoinIR ValueId
|
||
// (e.g. 'char' and 'is_char_match' sharing a promoted carrier slot).
|
||
// Emitting multiple `Copy` instructions to the same `dst` violates MIR SSA and
|
||
// fails `--verify` (ValueId defined multiple times).
|
||
if let Some(prev_src) = seen_dst_to_src.get(&final_dst).copied() {
|
||
if prev_src != binding.host_value {
|
||
return Err(format!(
|
||
"[BoundaryInjector] condition_bindings conflict: dst {:?} would be assigned from two different sources: {:?} and {:?} (JoinIR {:?}, name '{}')",
|
||
final_dst, prev_src, binding.host_value, binding.join_value, binding.name
|
||
));
|
||
}
|
||
continue;
|
||
}
|
||
seen_dst_to_src.insert(final_dst, binding.host_value);
|
||
|
||
// Copy instruction: final_dst = Copy host_value
|
||
let copy_inst = MirInstruction::Copy {
|
||
dst: final_dst,
|
||
src: binding.host_value,
|
||
};
|
||
|
||
copy_instructions.push(copy_inst);
|
||
|
||
if debug {
|
||
eprintln!(
|
||
"[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() }
|
||
);
|
||
}
|
||
}
|
||
|
||
// Entry block の先頭に Copy instructions を挿入
|
||
// Reverse order to preserve original order when inserting at position 0
|
||
// Phase 189 FIX: Also insert corresponding spans
|
||
let default_span = entry_block
|
||
.instruction_spans
|
||
.first()
|
||
.copied()
|
||
.unwrap_or_else(crate::ast::Span::unknown);
|
||
for inst in copy_instructions.into_iter().rev() {
|
||
entry_block.instructions.insert(0, inst);
|
||
entry_block.instruction_spans.insert(0, default_span);
|
||
}
|
||
|
||
// Return reallocations map for condition_bindings that had PHI collisions
|
||
Ok(reallocations)
|
||
}
|
||
}
|
||
|
||
// TODO: These tests need to be updated to use the new MirModule API
|
||
// #[cfg(test)]
|
||
// mod tests {
|
||
// use super::*;
|
||
// use crate::mir::{BasicBlock, MirModule};
|
||
//
|
||
// #[test]
|
||
// fn test_injector_empty_boundary() {
|
||
// // 空の boundary で何もしない
|
||
// let boundary = JoinInlineBoundary::new_inputs_only(vec![], vec![]);
|
||
// let mut module = MirModule::new();
|
||
// let mut func = module.define_function("test".to_string(), vec![]);
|
||
// let entry_block = func.create_block();
|
||
// let value_map = HashMap::new();
|
||
//
|
||
// let result = BoundaryInjector::inject_boundary_copies(
|
||
// &mut func,
|
||
// entry_block,
|
||
// &boundary,
|
||
// &value_map,
|
||
// false,
|
||
// );
|
||
//
|
||
// assert!(result.is_ok());
|
||
// }
|
||
//
|
||
// #[test]
|
||
// fn test_injector_single_copy() {
|
||
// // 単一の Copy instruction を挿入
|
||
// let boundary = JoinInlineBoundary::new_inputs_only(
|
||
// vec![ValueId(0)],
|
||
// vec![ValueId(10)],
|
||
// );
|
||
//
|
||
// let mut module = MirModule::new();
|
||
// let mut func = module.define_function("test".to_string(), vec![]);
|
||
// let entry_block = func.create_block();
|
||
//
|
||
// let mut value_map = HashMap::new();
|
||
// value_map.insert(ValueId(0), ValueId(100)); // JoinIR ValueId(0) remapped to ValueId(100)
|
||
//
|
||
// let result = BoundaryInjector::inject_boundary_copies(
|
||
// &mut func,
|
||
// entry_block,
|
||
// &boundary,
|
||
// &value_map,
|
||
// false,
|
||
// );
|
||
//
|
||
// assert!(result.is_ok());
|
||
//
|
||
// // Copy instruction が挿入されたことを確認
|
||
// let block = func.get_block(entry_block).unwrap();
|
||
// assert!(!block.instructions.is_empty());
|
||
//
|
||
// // First instruction should be Copy
|
||
// match &block.instructions[0] {
|
||
// MirInstruction::Copy { dst, src } => {
|
||
// assert_eq!(*dst, ValueId(100)); // Remapped join input
|
||
// assert_eq!(*src, ValueId(10)); // Host input
|
||
// }
|
||
// _ => panic!("Expected Copy instruction"),
|
||
// }
|
||
// }
|
||
//
|
||
// #[test]
|
||
// fn test_injector_multiple_copies() {
|
||
// // 複数の Copy instruction を挿入
|
||
// let boundary = JoinInlineBoundary::new_inputs_only(
|
||
// vec![ValueId(0), ValueId(1)],
|
||
// vec![ValueId(10), ValueId(20)],
|
||
// );
|
||
//
|
||
// let mut module = MirModule::new();
|
||
// let mut func = module.define_function("test".to_string(), vec![]);
|
||
// let entry_block = func.create_block();
|
||
//
|
||
// let mut value_map = HashMap::new();
|
||
// value_map.insert(ValueId(0), ValueId(100));
|
||
// value_map.insert(ValueId(1), ValueId(101));
|
||
//
|
||
// let result = BoundaryInjector::inject_boundary_copies(
|
||
// &mut func,
|
||
// entry_block,
|
||
// &boundary,
|
||
// &value_map,
|
||
// false,
|
||
// );
|
||
//
|
||
// assert!(result.is_ok());
|
||
//
|
||
// let block = func.get_block(entry_block).unwrap();
|
||
// assert_eq!(block.instructions.len(), 2);
|
||
//
|
||
// // Check both copy instructions
|
||
// match &block.instructions[0] {
|
||
// MirInstruction::Copy { dst, src } => {
|
||
// assert_eq!(*dst, ValueId(100));
|
||
// assert_eq!(*src, ValueId(10));
|
||
// }
|
||
// _ => panic!("Expected Copy instruction"),
|
||
// }
|
||
//
|
||
// match &block.instructions[1] {
|
||
// MirInstruction::Copy { dst, src } => {
|
||
// assert_eq!(*dst, ValueId(101));
|
||
// assert_eq!(*src, ValueId(20));
|
||
// }
|
||
// _ => panic!("Expected Copy instruction"),
|
||
// }
|
||
// }
|
||
// }
|