fix(joinir): Phase 196 Select double-remap bug in instruction_rewriter

Root cause: PHI inputs were being remapped twice in instruction_rewriter.rs
- Line 304: remap_instruction() already remapped JoinIR → Host ValueIds
- Line 328: remap_value() attempted to remap again → undefined ValueIds

Fix: Only remap block IDs, use already-remapped ValueIds as-is

Test results:
- phase195_sum_count.hako → 93  (multi-carrier P3)
- loop_if_phi.hako → sum=9  (single-carrier P3)
- loop_min_while.hako → 0,1,2  (Pattern 1)
- joinir_min_loop.hako → RC:0  (Pattern 2)
- No [joinir/freeze], no regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-09 14:45:04 +09:00
parent fa8727c2f8
commit 996925ebaf
11 changed files with 1207 additions and 90 deletions

View File

@ -320,13 +320,15 @@ pub(super) fn merge_and_rewrite(
type_hint: None,
} => MirInstruction::Phi {
dst,
// Phase 172: Fix P0 - Remap BOTH block ID AND value ID for PHI incoming
// Phase 196: Fix Select expansion PHI - ValueIds are ALREADY remapped by remap_instruction()
// We only need to remap block IDs here (not ValueIds!)
inputs: inputs
.iter()
.map(|(bb, val)| {
let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb);
let remapped_val = remapper.remap_value(*val);
(remapped_bb, remapped_val)
// Phase 196 FIX: Don't double-remap values!
// remapper.remap_instruction() already remapped *val
(remapped_bb, *val)
})
.collect(),
type_hint: None,

View File

@ -5,12 +5,12 @@ use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::super::trace;
/// Phase 179-A: Expected ValueId for k_exit parameter (sum_final) in Pattern 3
/// This corresponds to the exit PHI input in the JoinIR lowering for loop_with_if_phi_minimal
/// Phase 179-A / Phase 195: Expected ValueIds for k_exit parameters in Pattern 3
/// These correspond to the exit PHI inputs in the JoinIR lowering for loop_with_if_phi_minimal
///
/// # TODO (Phase 179 Task 2): Convert to ExitMeta-based exit binding generation
///
/// **Current State**: Hardcoded ValueId(18) - fragile and non-reusable
/// **Current State**: Hardcoded ValueIds - fragile and non-reusable
///
/// **Why it's hardcoded**:
/// - Pattern 3's lowerer (`lower_loop_with_if_phi_pattern`) returns `Option<JoinModule>`
@ -19,12 +19,15 @@ use super::super::trace;
///
/// **Migration Path** (when Pattern 3 lowerer is updated):
/// 1. Change `lower_loop_with_if_phi_pattern` to return `(JoinModule, JoinFragmentMeta)`
/// 2. Remove this constant
/// 2. Remove these constants
/// 3. Use ExitMeta loop (like Pattern 4 lines 350-378) to generate exit_bindings dynamically
/// 4. See: pattern4_with_continue.rs lines 350-378 for reference implementation
///
/// **Impact**: Low priority - Pattern 3 is test-only and works correctly with hardcoded value
const PATTERN3_K_EXIT_SUM_FINAL_ID: ValueId = ValueId(18);
/// **Impact**: Low priority - Pattern 3 is test-only and works correctly with hardcoded values
///
/// Phase 195: Multi-carrier support - now includes both sum_final and count_final
const PATTERN3_K_EXIT_SUM_FINAL_ID: ValueId = ValueId(24); // Phase 195: Updated from ValueId(18)
const PATTERN3_K_EXIT_COUNT_FINAL_ID: ValueId = ValueId(25); // Phase 195: New count carrier
/// Phase 194: Detection function for Pattern 3
///
@ -80,16 +83,20 @@ impl MirBuilder {
PatternVariant::Pattern3,
)?;
// Phase 179-B: Extract sum_var_id from context
// Pattern 3 specifically needs the "sum" carrier
let sum_var_id = ctx.carrier_info.carriers.iter()
// Phase 195: Extract carrier var_ids dynamically based on what exists
// This maintains backward compatibility with single-carrier (sum only) and multi-carrier (sum+count) tests
let sum_carrier = ctx.carrier_info.carriers.iter()
.find(|c| c.name == "sum")
.ok_or_else(|| {
format!(
"[cf_loop/pattern3] Accumulator variable 'sum' not found in variable_map"
)
})?
.host_id;
})?;
let sum_var_id = sum_carrier.host_id;
let count_carrier_opt = ctx.carrier_info.carriers.iter()
.find(|c| c.name == "count");
let has_count = count_carrier_opt.is_some();
// Phase 195: Use unified trace
trace::trace().varmap("pattern3_start", &self.variable_map);
@ -104,26 +111,57 @@ impl MirBuilder {
}
};
// Phase 179-B: Create boundary from context
// Phase 195: Create boundary from context (multi-carrier support with backward compatibility)
// Phase 201: Use JoinInlineBoundaryBuilder for clean construction
// Canonical Builder pattern - see docs/development/current/main/joinir-boundary-builder-pattern.md
self.trace_varmap("pattern3_before_merge");
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding;
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(
vec![ValueId(0), ValueId(1)], // JoinIR's main() parameters (i, sum init)
vec![ctx.loop_var_id, sum_var_id], // Host's loop variables
// Phase 195: Build inputs and exit_bindings dynamically based on available carriers
let (join_inputs, host_inputs, exit_bindings) = if has_count {
// Multi-carrier: i, sum, count
let count_var_id = count_carrier_opt.unwrap().host_id;
(
vec![ValueId(0), ValueId(1), ValueId(2)], // JoinIR's main() parameters
vec![ctx.loop_var_id, sum_var_id, count_var_id], // Host's loop variables
vec![
LoopExitBinding {
carrier_name: "sum".to_string(),
join_exit_value: PATTERN3_K_EXIT_SUM_FINAL_ID, // ValueId(24)
host_slot: sum_var_id,
},
LoopExitBinding {
carrier_name: "count".to_string(),
join_exit_value: PATTERN3_K_EXIT_COUNT_FINAL_ID, // ValueId(25)
host_slot: count_var_id,
}
]
)
.with_exit_bindings(vec![
// Phase 33-16: Only include non-loop-variable carriers in exit_bindings
// The loop variable is handled separately via boundary.loop_var_name
LoopExitBinding {
carrier_name: "sum".to_string(),
join_exit_value: PATTERN3_K_EXIT_SUM_FINAL_ID, // k_exit's parameter (sum_final)
host_slot: sum_var_id, // variable_map["sum"]
}
])
} else {
// Single-carrier (backward compatibility): i, sum only
// Phase 195: JoinIR lowerer now always generates 3 parameters (i, sum, count)
// For backward compat, we create a dummy count variable that will be discarded
use crate::mir::builder::emission::constant;
let dummy_count_id = constant::emit_void(self); // Use void as dummy value
(
vec![ValueId(0), ValueId(1), ValueId(2)], // JoinIR's main() parameters (i, sum, count)
vec![ctx.loop_var_id, sum_var_id, dummy_count_id], // Host's loop variables (count is dummy)
vec![
LoopExitBinding {
carrier_name: "sum".to_string(),
join_exit_value: PATTERN3_K_EXIT_SUM_FINAL_ID, // ValueId(24)
host_slot: sum_var_id,
}
// Don't bind count in single-carrier mode - it's just discarded
]
)
};
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(join_inputs, host_inputs)
.with_exit_bindings(exit_bindings)
.with_loop_var_name(Some(ctx.loop_var_name.clone())) // Phase 33-16: Enable header PHI generation for SSA correctness
.build();