feat(joinir): Phase 197 Pattern4 multi-carrier exit fix & AST-based update

Phase 197-B: Multi-Carrier Exit Mechanism
- Fixed reconnect_boundary() to use remapper for per-carrier exit values
- ExitMeta now uses carrier_param_ids (Jump arguments) instead of
  carrier_exit_ids (k_exit parameters)
- Root cause: k_exit parameters aren't defined when JoinIR functions
  merge into host MIR

Phase 197-C: AST-Based Update Expression
- LoopUpdateAnalyzer extracts update patterns from loop body AST
- Pattern4 lowerer uses UpdateExpr for semantically correct RHS
- sum = sum + i → uses i_next (current iteration value)
- count = count + 1 → uses const_1

Files modified:
- src/mir/builder/control_flow/joinir/merge/mod.rs
- src/mir/join_ir/lowering/loop_with_continue_minimal.rs
- src/mir/join_ir/lowering/loop_update_analyzer.rs (new)

Test results:
- loop_continue_multi_carrier.hako: 25, 5 
- loop_continue_pattern4.hako: 25 

Pattern 1–4 now unified with:
- Structure-based detection (LoopFeatures + classify)
- Carrier/Exit metadata (CarrierInfo + ExitMeta)
- Boundary connection (JoinInlineBoundary + LoopExitBinding)
- AST-based update expressions (LoopUpdateAnalyzer)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-06 14:46:33 +09:00
parent e0cff2ef00
commit ae61226691
8 changed files with 1123 additions and 165 deletions

View File

@ -100,8 +100,9 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
)?;
// Phase 6: Reconnect boundary (if specified)
// Phase 197-B: Pass remapper to enable per-carrier exit value lookup
if let Some(boundary) = boundary {
reconnect_boundary(builder, boundary, exit_phi_result_id, debug)?;
reconnect_boundary(builder, boundary, &remapper, exit_phi_result_id, debug)?;
}
// Jump from current block to entry function's entry block
@ -177,47 +178,90 @@ fn remap_values(
}
/// Phase 6: Reconnect boundary to update host variable_map
///
/// Phase 197-B: Multi-carrier support
/// Previously, a single exit_phi_result was applied to all carriers, causing
/// all carriers to get the same value (e.g., both sum and count = 5).
///
/// Now, we use each binding's join_exit_value and the remapper to find
/// the actual exit value for each carrier.
fn reconnect_boundary(
builder: &mut crate::mir::builder::MirBuilder,
boundary: &JoinInlineBoundary,
exit_phi_result: Option<ValueId>,
remapper: &crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper,
_exit_phi_result: Option<ValueId>,
debug: bool,
) -> Result<(), String> {
// Phase 190: Use explicit LoopExitBinding to reconnect exit PHI to variable_map
// Each binding explicitly names the carrier variable and maps exit PHI to it.
if let Some(phi_result) = exit_phi_result {
// Phase 190: Use exit_bindings for explicit carrier naming
// This eliminates ambiguity about which variable is being updated
for binding in &boundary.exit_bindings {
// Find the variable in variable_map that matches the binding's host_slot
for (var_name, vid) in builder.variable_map.iter_mut() {
if *vid == binding.host_slot {
*vid = phi_result;
if debug {
eprintln!(
"[cf_loop/joinir] Phase 190: Reconnected exit PHI {:?} to variable_map['{}'] (carrier: {})",
phi_result, var_name, binding.carrier_name
);
}
// Validate carrier name matches
if var_name != &binding.carrier_name && debug {
eprintln!(
"[cf_loop/joinir] WARNING: Carrier name mismatch: expected '{}', found '{}'",
binding.carrier_name, var_name
);
}
}
}
// Phase 197-B: Multi-carrier exit binding using join_exit_value directly
//
// Problem: Previously used single exit_phi_result for all carriers.
// Solution: Each carrier's exit value is in k_exit parameters.
// We use remapper to find the remapped ValueId for each carrier.
//
// For Pattern 4 with 2 carriers (sum, count):
// - k_exit(sum_exit, count_exit) receives values via Jump args
// - sum_exit = ValueId(N) in JoinIR → remapped to ValueId(M) in host
// - count_exit = ValueId(N+1) in JoinIR → remapped to ValueId(M+1) in host
//
// Each carrier's variable_map entry is updated with its specific remapped value.
if boundary.exit_bindings.is_empty() {
if debug {
eprintln!("[cf_loop/joinir] Phase 197-B: No exit bindings, skipping reconnect");
}
return Ok(());
}
if debug {
eprintln!(
"[cf_loop/joinir] Phase 197-B: Reconnecting {} exit bindings",
boundary.exit_bindings.len()
);
}
// For each binding, look up the remapped exit value and update variable_map
for binding in &boundary.exit_bindings {
// Phase 197-B: Use remapper to find the remapped exit value
let remapped_exit = remapper.get_value(binding.join_exit_value);
if debug {
eprintln!(
"[cf_loop/joinir] Phase 197-B: Carrier '{}' join_exit={:?} → remapped={:?}",
binding.carrier_name, binding.join_exit_value, remapped_exit
);
}
// Phase 190: Backward compatibility - also check deprecated host_outputs
#[allow(deprecated)]
if !boundary.host_outputs.is_empty() && debug {
if let Some(remapped_value) = remapped_exit {
// Update variable_map with the remapped exit value
if let Some(var_vid) = builder.variable_map.get_mut(&binding.carrier_name) {
if debug {
eprintln!(
"[cf_loop/joinir] Phase 197-B: Updated variable_map['{}'] {:?}{:?}",
binding.carrier_name, var_vid, remapped_value
);
}
*var_vid = remapped_value;
} else if debug {
eprintln!(
"[cf_loop/joinir] Phase 197-B WARNING: Carrier '{}' not found in variable_map",
binding.carrier_name
);
}
} else if debug {
eprintln!(
"[cf_loop/joinir] WARNING: Using deprecated host_outputs. Migrate to exit_bindings."
"[cf_loop/joinir] Phase 197-B WARNING: No remapped value for join_exit={:?}",
binding.join_exit_value
);
}
}
// Phase 190: Backward compatibility - also check deprecated host_outputs
#[allow(deprecated)]
if !boundary.host_outputs.is_empty() && debug {
eprintln!(
"[cf_loop/joinir] WARNING: Using deprecated host_outputs. Migrate to exit_bindings."
);
}
Ok(())
}