//! JoinIR MIR Block Merging Coordinator //! //! This module coordinates the merging of JoinIR-generated MIR functions //! into the host MIR builder. The process is broken into 6 phases: //! //! 1. Block ID allocation (block_allocator.rs) //! 2. Value collection (value_collector.rs) //! 3. ValueId remapping (uses JoinIrIdRemapper) //! 4. Instruction rewriting (instruction_rewriter.rs) //! 5. Exit PHI construction (exit_phi_builder.rs) //! 6. Boundary reconnection (inline in this file) //! //! Phase 4 Refactoring: Breaking down 714-line merge_joinir_mir_blocks() into focused modules mod block_allocator; mod carrier_init_builder; mod contract_checks; pub mod exit_args_collector; // Phase 118: Exit args collection box pub mod exit_line; mod exit_phi_builder; mod expr_result_resolver; mod instruction_rewriter; mod loop_header_phi_builder; mod loop_header_phi_info; mod merge_result; mod phi_block_remapper; // Phase 94: Phi block-id remap box mod tail_call_classifier; mod tail_call_lowering_policy; // Phase 131 Task 2: k_exit exit edge normalization mod value_collector; #[cfg(test)] mod tests; // Phase 132-R0 Task 3: Continuation contract tests // Phase 33-17: Re-export for use by other modules pub use loop_header_phi_builder::LoopHeaderPhiBuilder; pub use loop_header_phi_info::LoopHeaderPhiInfo; // Phase 131 P1 Task 1: Re-export MergeContracts for SSOT visibility pub use merge_result::MergeContracts; // Phase 131 P1 Task 6: MergeConfig is defined in this module (no re-export needed) use super::trace; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; use crate::mir::{MirModule, ValueId}; use std::collections::BTreeMap; /// Phase 131 P1 Task 6: Merge configuration consolidation /// /// Consolidates all merge-related configuration into a single structure /// to reduce parameter clutter and improve maintainability. #[derive(Debug, Clone)] pub struct MergeConfig { /// Enable detailed trace logs (dev mode) pub dev_log: bool, /// Enable strict contract verification (fail-fast on violations) pub strict_mode: bool, /// Exit reconnection mode (Phi or DirectValue) pub exit_reconnect_mode: Option, /// Allow missing exit block in contract checks (typically exit_block_id before insertion) pub allow_missing_exit_block: bool, } impl MergeConfig { /// Default configuration for normal operation pub fn default() -> Self { Self { dev_log: crate::config::env::joinir_dev_enabled(), strict_mode: crate::config::env::joinir_strict_enabled(), exit_reconnect_mode: None, allow_missing_exit_block: true, } } /// Strict configuration for development/debugging (all checks enabled) pub fn strict() -> Self { Self { dev_log: true, strict_mode: true, exit_reconnect_mode: None, allow_missing_exit_block: true, } } /// Configuration for specific debug session pub fn with_debug(debug: bool) -> Self { let mut config = Self::default(); config.dev_log = debug || config.dev_log; config } } /// Phase 49-3.2: Merge JoinIR-generated MIR blocks into current_function /// /// # Phase 189: Multi-Function MIR Merge /// /// This merges JoinIR-generated blocks by: /// 1. Remapping all block IDs across ALL functions to avoid conflicts /// 2. Remapping all value IDs across ALL functions to avoid conflicts /// 3. Adding all blocks from all functions to current_function /// 4. Jumping from current_block to the entry block /// 5. Converting Return → Jump to exit block for all functions /// /// **Multi-Function Support** (Phase 189): /// - Pattern 1 (Simple While) generates 3 functions: entry + loop_step + k_exit /// - All functions are flattened into current_function with global ID remapping /// - Single exit block receives all Return instructions from all functions /// /// # Phase 188-Impl-3: JoinInlineBoundary Support /// /// When `boundary` is provided, injects Copy instructions at the entry block /// to connect host ValueIds to JoinIR local ValueIds: /// /// ```text /// entry_block: /// // Injected by boundary /// ValueId(100) = Copy ValueId(4) // join_input → host_input /// // Original JoinIR instructions follow... /// ``` /// /// This enables clean separation: JoinIR uses local IDs (0,1,2...), /// host uses its own IDs, and Copy instructions bridge the gap. /// /// # Returns /// /// Returns `Ok(Some(exit_phi_id))` if the merged JoinIR functions have return values /// that were collected into an exit block PHI. さらに、`boundary` に /// host_outputs が指定されている場合は、exit PHI の結果をホスト側の /// SSA スロットへ再接続する(variable_map 内の ValueId を更新する)。 pub(in crate::mir::builder) fn merge_joinir_mir_blocks( builder: &mut crate::mir::builder::MirBuilder, mir_module: &MirModule, boundary: Option<&JoinInlineBoundary>, debug: bool, ) -> Result, String> { // Phase 131 Task 6: Use MergeConfig for consolidated configuration let config = MergeConfig::with_debug(debug); let verbose = config.dev_log; let trace = trace::trace(); trace.stderr_if( &format!( "[cf_loop/joinir] merge_joinir_mir_blocks called with {} functions", mir_module.functions.len() ), debug, ); if verbose { if let Some(boundary) = boundary { let exit_summary: Vec = boundary .exit_bindings .iter() .map(|b| { format!( "{}: join {:?} → host {:?} ({:?})", b.carrier_name, b.join_exit_value, b.host_slot, b.role ) }) .collect(); let cond_summary: Vec = boundary .condition_bindings .iter() .map(|b| { format!( "{}: host {:?} → join {:?}", b.name, b.host_value, b.join_value ) }) .collect(); trace.stderr_if( &format!( "[cf_loop/joinir] Boundary join_inputs={:?} host_inputs={:?}", boundary.join_inputs, boundary.host_inputs ), true, ); trace.stderr_if( &format!( "[cf_loop/joinir] Boundary exit_bindings ({}): {}", boundary.exit_bindings.len(), exit_summary.join(", ") ), true, ); if !cond_summary.is_empty() { trace.stderr_if( &format!( "[cf_loop/joinir] Boundary condition_bindings ({}): {}", cond_summary.len(), cond_summary.join(", ") ), true, ); } if let Some(ci) = &boundary.carrier_info { let carriers: Vec = ci.carriers.iter().map(|c| c.name.clone()).collect(); trace.stderr_if( &format!( "[cf_loop/joinir] Boundary carrier_info: loop_var='{}', carriers={:?}", ci.loop_var_name, carriers ), true, ); } } else { trace.stderr_if("[cf_loop/joinir] No boundary provided", true); } } // Phase 1: Allocate block IDs for all functions // Phase 177-3: block_allocator now returns exit_block_id to avoid conflicts let (mut remapper, exit_block_id) = block_allocator::allocate_blocks(builder, mir_module, debug)?; // Phase 2: Collect values from all functions let (mut used_values, value_to_func_name, function_params) = value_collector::collect_values(mir_module, &remapper, debug)?; // Phase 171-fix: Add condition_bindings' join_values to used_values for remapping if let Some(boundary) = boundary { for binding in &boundary.condition_bindings { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 171-fix: Adding condition binding '{}' JoinIR {:?} to used_values", binding.name, binding.join_value ), debug, ); used_values.insert(binding.join_value); } // Phase 172-3: Add exit_bindings' join_exit_values to used_values for remapping for binding in &boundary.exit_bindings { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 172-3: Adding exit binding '{}' JoinIR {:?} to used_values", binding.carrier_name, binding.join_exit_value ), debug, ); used_values.insert(binding.join_exit_value); } } // 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")?; // Phase 228-4: Extract carriers with their initialization strategy let other_carriers: Vec<( String, ValueId, crate::mir::join_ir::lowering::carrier_info::CarrierInit, crate::mir::join_ir::lowering::carrier_info::CarrierRole, )> = if let Some(ref carrier_info) = boundary.carrier_info { // Use carrier_info if available (Phase 228) carrier_info .carriers .iter() .filter(|c| c.name != *loop_var_name) .map(|c| (c.name.clone(), c.host_id, c.init, c.role)) .collect() } else { // Fallback: exit_bindings から取得(既存動作) boundary .exit_bindings .iter() .filter(|b| b.carrier_name != *loop_var_name) .map(|b| { ( b.carrier_name.clone(), b.host_slot, crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState, ) }) .collect() }; trace.stderr_if( &format!( "[cf_loop/joinir] Phase 201-A: Pre-building header PHIs for loop_var='{}' at {:?}", loop_var_name, entry_block_remapped ), debug, ); trace.stderr_if( &format!( "[cf_loop/joinir] loop_var_init={:?}, carriers={:?}", loop_var_init, other_carriers .iter() .map(|(n, _, _, _)| n.as_str()) .collect::>() ), debug, ); // 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(); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 201-A: Reserved PHI dsts: {:?}", reserved_phi_dsts ), debug && !reserved_phi_dsts.is_empty(), ); // 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.comp_ctx.reserved_value_ids = reserved_phi_dsts.clone(); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 201-A: Set builder.comp_ctx.reserved_value_ids = {:?}", builder.comp_ctx.reserved_value_ids ), debug && !builder.comp_ctx.reserved_value_ids.is_empty(), ); // 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 trace.stderr_if("[DEBUG-177] === Remapper state after Phase 3 ===", verbose); trace.stderr_if( &format!("[DEBUG-177] used_values count: {}", used_values.len()), verbose, ); for value_id in &used_values { if let Some(remapped) = remapper.get_value(*value_id) { trace.stderr_if( &format!("[DEBUG-177] JoinIR {:?} → Host {:?}", value_id, remapped), verbose, ); } else { trace.stderr_if( &format!("[DEBUG-177] JoinIR {:?} → NOT FOUND ❌", value_id), verbose, ); } } // Check condition_bindings specifically if let Some(boundary) = boundary { trace.stderr_if("[DEBUG-177] === Condition bindings check ===", verbose); for binding in &boundary.condition_bindings { let lookup_result = remapper.get_value(binding.join_value); trace.stderr_if( &format!( "[DEBUG-177] '{}': JoinIR {:?} → {:?}", binding.name, binding.join_value, lookup_result ), verbose, ); } } trace.stderr_if("[DEBUG-177] ==============================", verbose); // Phase 3.5: Override remapper for function parameters to use PHI dsts // // 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 201-A: PHI info is already built (before Phase 3) - just use it // Phase 33-21: Override remapper for loop_step's parameters // // JoinIR generates separate parameter ValueIds for each function: // - main(): ValueId(0), ValueId(1), ... for (i_init, carrier1_init, ...) // - loop_step(): ValueId(3), ValueId(4), ... for (i_param, carrier1_param, ...) // // The loop body uses loop_step's parameters, so we need to remap THOSE // to the header PHI dsts, not main()'s parameters. // // We get loop_step's parameters from function_params collected earlier. // Phase 33-21: Override remapper for ALL functions' parameters // // JoinIR generates separate parameter ValueIds for each function: // - main (join_func_0): ValueId(0), ValueId(1), ... for (i_init, carrier1_init, ...) // - loop_step (join_func_1): ValueId(3), ValueId(4), ... for (i_param, carrier1_param, ...) // // ALL of these need to be mapped to header PHI dsts so that: // 1. condition evaluation uses PHI result // 2. loop body uses PHI result // 3. tail call args are correctly routed // Phase 177-3 fix: Protect condition-ONLY bindings from being overridden to PHI dsts // // Problem: condition_bindings may contain: // 1. True condition-only variables (e.g., 'limit' in loop(i < limit)) - NOT carriers // 2. Body-only carriers added by Phase 176-5 (e.g., 'result') - ARE carriers // // We must ONLY protect (1), not (2), because: // - Condition-only vars should keep their HOST mapping (e.g., limit = %8) // - Body-only carriers MUST be remapped to PHI dsts (e.g., result = %24) // // Solution: Protect condition_bindings that are NOT in exit_bindings (i.e., not carriers) let carrier_names: std::collections::HashSet<&str> = boundary .exit_bindings .iter() .map(|eb| eb.carrier_name.as_str()) .collect(); let condition_binding_ids: std::collections::HashSet = boundary .condition_bindings .iter() .filter(|cb| !carrier_names.contains(cb.name.as_str())) .map(|cb| cb.join_value) .collect(); if !condition_binding_ids.is_empty() { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 177-3: Protected ValueIds (condition-only, not carriers): {:?}", condition_binding_ids ), verbose, ); for cb in &boundary.condition_bindings { let is_carrier = carrier_names.contains(cb.name.as_str()); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 177-3: '{}': JoinIR {:?} (carrier={})", cb.name, cb.join_value, is_carrier ), verbose, ); } } let main_func_name = "join_func_0"; let loop_step_func_name = "join_func_1"; if function_params.get(main_func_name).is_none() { trace.stderr_if( &format!( "[cf_loop/joinir] WARNING: function_params.get('{}') returned None. Available keys: {:?}", main_func_name, function_params.keys().collect::>() ), verbose, ); } if let Some(main_params) = function_params.get(main_func_name) { trace.stderr_if( &format!( "[DEBUG-177] Phase 33-21: main ({}) params: {:?}", main_func_name, main_params ), verbose, ); trace.stderr_if( &format!( "[DEBUG-177] Phase 33-21: carrier_phis count: {}, names: {:?}", loop_header_phi_info.carrier_phis.len(), loop_header_phi_info .carrier_phis .iter() .map(|(n, _)| n.as_str()) .collect::>() ), verbose, ); // 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 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) { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 177-3: Skipping override for condition_binding {:?} ('{}')", main_param, carrier_name ), verbose, ); continue; } trace.stderr_if( &format!( "[DEBUG-177] Phase 33-21: REMAP main param[{}] {:?} → {:?} ('{}')", idx, main_param, entry.phi_dst, carrier_name ), verbose, ); remapper.set_value(main_param, entry.phi_dst); } } } // 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 &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) if condition_binding_ids.contains(&binding.join_value) { continue; } // This is a body-only carrier - remap it to PHI dst trace.stderr_if( &format!( "[cf_loop/joinir] Phase 177-3-B: Body-only carrier '{}': JoinIR {:?} → PHI {:?}", carrier_name, binding.join_value, entry.phi_dst ), verbose, ); remapper.set_value(binding.join_value, entry.phi_dst); } } // Map loop_step's parameters // DEBUG-177: Always log function_params keys to diagnose multi-carrier issue trace.stderr_if( &format!( "[DEBUG-177] Phase 33-21: function_params keys: {:?}", function_params.keys().collect::>() ), verbose, ); if function_params.get(loop_step_func_name).is_none() { trace.stderr_if( &format!( "[cf_loop/joinir] WARNING: function_params.get('{}') returned None. Available keys: {:?}", loop_step_func_name, function_params.keys().collect::>() ), verbose, ); } if let Some(loop_step_params) = function_params.get(loop_step_func_name) { // DEBUG-177: Always log loop_step params trace.stderr_if( &format!( "[DEBUG-177] Phase 33-21: loop_step ({}) params: {:?}", loop_step_func_name, loop_step_params ), verbose, ); // Phase 177-FIX: Process loop_step params but skip if already mapped // // We use a name-based approach: for each carrier_phi, check if // its join_value was already set in Phase 177-3-B (body-only carriers). // Only process loop_step params for carriers NOT already handled. for loop_step_param in loop_step_params { // Phase 177-3: Don't override condition_bindings if condition_binding_ids.contains(loop_step_param) { trace.stderr_if( &format!( "[DEBUG-177] Phase 177-FIX: Skipping condition_binding {:?}", loop_step_param ), verbose, ); continue; } // Find which carrier this param belongs to by matching join_value // 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 && loop_header_phi_info .carrier_phis .iter() .any(|(name, _)| name == &cb.name) }); if already_mapped { trace.stderr_if( &format!( "[DEBUG-177] Phase 177-FIX: Skipping {:?} (already mapped by Phase 177-3-B)", loop_step_param ), verbose, ); continue; } // Phase 177-STRUCT-2: Use carrier_order for index-based matching // // Problem: BTreeMap iterates in alphabetical order, but JoinIR // generates params in exit_bindings order. // // Solution: Use carrier_order (Vec) which preserves insertion order. 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)) = ( loop_header_phi_info.get_carrier_at_index(param_idx), loop_header_phi_info.get_entry_at_index(param_idx), ) { trace.stderr_if( &format!( "[DEBUG-177] Phase 177-STRUCT-2: REMAP loop_step param[{}] {:?} → {:?} (carrier '{}')", param_idx, loop_step_param, entry.phi_dst, carrier_name ), verbose, ); remapper.set_value(*loop_step_param, entry.phi_dst); } } } } 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) = 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); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 33-16 fallback: Override remap ValueId(0) → {:?} (PHI dst)", phi_dst ), debug, ); } else { trace.stderr_if( "[cf_loop/joinir] Phase 177-3 fallback: Skipping override for condition_binding ValueId(0)", verbose, ); } } // Phase 177-STRUCT-2: Use carrier_order for deterministic iteration for (idx, carrier_name) in loop_header_phi_info.carrier_order.iter().enumerate() { if carrier_name == loop_var_name { continue; } let entry = match loop_header_phi_info.carrier_phis.get(carrier_name) { Some(e) => e, None => continue, }; let join_value_id = ValueId(idx as u32); // Phase 177-3: Don't override condition_bindings if !condition_binding_ids.contains(&join_value_id) { remapper.set_value(join_value_id, entry.phi_dst); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 33-20 fallback: Override remap {:?} → {:?} (carrier '{}' PHI dst)", join_value_id, entry.phi_dst, carrier_name ), debug, ); } else { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 177-3 fallback: Skipping override for condition_binding {:?} ('{}')", join_value_id, carrier_name ), verbose, ); } } } // Phase 177-3 DEBUG: Check remapper after Phase 33-21 overrides trace.stderr_if("[DEBUG-177] === Remapper state after Phase 33-21 ===", verbose); for binding in &boundary.condition_bindings { let lookup_result = remapper.get_value(binding.join_value); trace.stderr_if( &format!( "[DEBUG-177] '{}': JoinIR {:?} → {:?} (after 33-21)", binding.name, binding.join_value, lookup_result ), verbose, ); } // Phase 201-A: loop_header_phi_info already built (no assignment needed) } } // Phase 4: Merge blocks and rewrite instructions // Phase 33-16: Pass mutable loop_header_phi_info for latch_incoming tracking // Phase 177-3: Pass exit_block_id from allocator to avoid conflicts let merge_result = instruction_rewriter::merge_and_rewrite( builder, mir_module, &mut remapper, &value_to_func_name, &function_params, boundary, &mut loop_header_phi_info, exit_block_id, debug, )?; // Phase 4.5: Finalize loop header PHIs (insert into header block) // // By now, instruction_rewriter has set latch_incoming for all carriers. // We can finalize the PHIs and insert them into the header block. if !loop_header_phi_info.carrier_phis.is_empty() { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 4.5: Finalizing {} header PHIs", loop_header_phi_info.carrier_phis.len() ), debug, ); LoopHeaderPhiBuilder::finalize(builder, &loop_header_phi_info, debug)?; } // Contract check (Fail-Fast): ensure we didn't leave dangling Jump/Branch targets. // Phase 131 Task 6: Use MergeConfig.strict_mode instead of env checks if config.strict_mode || config.dev_log { if let Some(ref current_func) = builder.scope_ctx.current_function { // Note: exit_block_id may be allocated but not inserted yet (it becomes the // current block after merge, and subsequent AST lowering fills it). // We still want to catch truly dangling targets (e.g., jumps to skipped k_exit). let contracts = MergeContracts { allowed_missing_jump_targets: vec![merge_result.exit_block_id], }; contract_checks::verify_all_terminator_targets_exist(current_func, &contracts)?; } } // Phase 5: Build exit PHI (expr result only, not carrier PHIs) // Phase 33-20: Carrier PHIs are now taken from header PHI info, not exit block // Phase 246-EX: REVERT Phase 33-20 - Use EXIT PHI dsts, not header PHI dsts! // Phase 131 P1.5: DirectValue mode completely skips PHI generation trace.stderr_if( &format!( "[cf_loop/joinir] Phase 131 P1.5 DEBUG: boundary={:?}, mode={:?}", boundary.is_some(), boundary.map(|b| b.exit_reconnect_mode) ), debug, ); // Phase 131 P1.5: Check if DirectValue mode (skip PHI generation) let is_direct_value_mode = boundary .map(|b| b.exit_reconnect_mode == crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::DirectValue) .unwrap_or(false); // Phase 131 P1.5: Mode detection (dev-only visibility) trace.stderr_if( &format!( "[cf_loop/joinir] Phase 131 P1.5: exit_reconnect_mode={:?}, is_direct_value_mode={}", boundary.map(|b| b.exit_reconnect_mode), is_direct_value_mode ), debug || config.dev_log, ); let (exit_phi_result_id, exit_carrier_phis) = if is_direct_value_mode { // DirectValue mode: Skip PHI generation completely trace.stderr_if( "[cf_loop/joinir] Phase 131 P1.5: DirectValue mode - skipping exit PHI generation", debug, ); (None, BTreeMap::new()) } else { // Phi mode: Generate exit PHIs as usual trace.stderr_if( "[cf_loop/joinir] Phase 131 P1.5: Phi mode - generating exit PHIs", debug, ); exit_phi_builder::build_exit_phi( builder, merge_result.exit_block_id, &merge_result.exit_phi_inputs, &merge_result.carrier_inputs, debug, )? }; // Phase 118 P2: Contract check (Fail-Fast) - exit_bindings LoopState carriers must have exit PHIs. // Phase 131 P1.5: Skip this check in DirectValue mode if let Some(boundary) = boundary { if !is_direct_value_mode { contract_checks::verify_exit_bindings_have_exit_phis(boundary, &exit_carrier_phis)?; } } // Phase 118 P1: Dev-only carrier-phi SSOT logs (exit_bindings vs carrier_inputs vs exit_carrier_phis) // Phase 131 Task 6: Use config.dev_log instead of env check if config.dev_log { if let Some(boundary) = boundary { let exit_binding_names: Vec<&str> = boundary .exit_bindings .iter() .map(|b| b.carrier_name.as_str()) .collect(); let carrier_input_names: Vec<&str> = merge_result.carrier_inputs.keys().map(|s| s.as_str()).collect(); let exit_phi_names: Vec<&str> = exit_carrier_phis.keys().map(|s| s.as_str()).collect(); trace.stderr_if( &format!( "[joinir/phase118/dev] exit_bindings carriers={:?}", exit_binding_names ), true, ); trace.stderr_if( &format!( "[joinir/phase118/dev] carrier_inputs keys={:?}", carrier_input_names ), true, ); trace.stderr_if( &format!( "[joinir/phase118/dev] exit_carrier_phis keys={:?}", exit_phi_names ), true, ); } } // Phase 246-EX: CRITICAL FIX - Use exit PHI dsts for variable_map reconnection // // **Why EXIT PHI, not HEADER PHI?** // // Header PHI represents the value at the BEGINNING of each iteration. // Exit PHI represents the FINAL value when leaving the loop (from any exit path). // // For Pattern 2 loops with multiple exit paths (natural exit + break): // - Header PHI: `%15 = phi [%3, bb7], [%42, bb14]` (loop variable at iteration start) // - Exit PHI: `%5 = phi [%15, bb11], [%15, bb13]` (final value from exit paths) // // When we exit the loop, we want the FINAL value (%5), not the iteration-start value (%15). // Phase 33-20 incorrectly used header PHI, causing loops to return initial values (e.g., 0 instead of 42). // // Example (_atoi): // - Initial: result=0 (header PHI) // - After iteration 1: result=4 (updated in loop body) // - After iteration 2: result=42 (updated in loop body) // - Exit: Should return 42 (exit PHI), not 0 (header PHI initial value) // // The exit PHI correctly merges values from both exit paths, giving us the final result. let carrier_phis = &exit_carrier_phis; trace.stderr_if( &format!( "[cf_loop/joinir] Phase 246-EX: Using EXIT PHI dsts for variable_map (not header): {:?}", carrier_phis .iter() .map(|(n, v)| (n.as_str(), v)) .collect::>() ), debug && !carrier_phis.is_empty(), ); // Phase 6: Reconnect boundary (if specified) // Phase 197-B: Pass remapper to enable per-carrier exit value lookup // Phase 33-10-Refactor-P3: Delegate to ExitLineOrchestrator // Phase 246-EX: Now uses EXIT PHI dsts (reverted Phase 33-20) // Phase 131 P2: DirectValue mode SSOT uses MergeResult.remapped_exit_values let remapped_exit_values = merge_result.remapped_exit_values.clone(); if let Some(boundary) = boundary { exit_line::ExitLineOrchestrator::execute( builder, boundary, carrier_phis, &remapped_exit_values, // Phase 131 P1.5: Now populated with exit PHI dsts debug, )?; } let exit_block_id = merge_result.exit_block_id; // 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; trace.stderr_if( &format!( "[cf_loop/joinir] Entry block (from loop_header_phi_info): {:?}", entry_block ), debug, ); trace.stderr_if( &format!( "[cf_loop/joinir] Current block before emit_jump: {:?}", builder.current_block ), debug, ); trace.stderr_if( &format!( "[cf_loop/joinir] Jumping to entry block: {:?}", entry_block ), debug, ); crate::mir::builder::emission::branch::emit_jump(builder, entry_block)?; trace.stderr_if( &format!( "[cf_loop/joinir] After emit_jump, current_block: {:?}", builder.current_block ), debug, ); // Switch to exit block for subsequent code builder.start_new_block(exit_block_id)?; trace.stderr_if( &format!( "[cf_loop/joinir] Phase 189: Merge complete: {} functions merged, continuing from {:?}", mir_module.functions.len(), exit_block_id ), debug, ); // Phase 200-3: Verify JoinIR contracts (debug only) #[cfg(debug_assertions)] { if let Some(boundary) = boundary { if let Some(ref func) = builder.scope_ctx.current_function { verify_joinir_contracts( func, entry_block, exit_block_id, &loop_header_phi_info, boundary, ); } trace.stderr_if( "[cf_loop/joinir] Phase 200-3: Contract verification passed", debug, ); } } // Phase 201-A: Clear reserved ValueIds after merge completes // Future loops will set their own reserved IDs if !builder.comp_ctx.reserved_value_ids.is_empty() { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 201-A: Clearing reserved_value_ids (was {:?})", builder.comp_ctx.reserved_value_ids ), debug, ); builder.comp_ctx.reserved_value_ids.clear(); } // Phase 246-EX-FIX: Handle loop variable expr_result separately from carrier expr_result // // The loop variable (e.g., 'i') is returned via exit_phi_result_id, not carrier_phis. // Other carriers use carrier_phis. We need to check which case we're in. let expr_result_value = if let Some(b) = boundary { if let Some(expr_result_id) = b.expr_result { // Check if expr_result is the loop variable if let Some(loop_var_name) = &b.loop_var_name { // Find the exit binding for the loop variable let loop_var_binding = b .exit_bindings .iter() .find(|binding| binding.carrier_name == *loop_var_name); if let Some(binding) = loop_var_binding { if binding.join_exit_value == expr_result_id { // expr_result is the loop variable! Use exit_phi_result_id trace.stderr_if( &format!( "[cf_loop/joinir] Phase 246-EX-FIX: expr_result {:?} is loop variable '{}', using exit_phi_result_id {:?}", expr_result_id, loop_var_name, exit_phi_result_id ), debug, ); exit_phi_result_id } else { // expr_result is not the loop variable, resolve as carrier expr_result_resolver::ExprResultResolver::resolve( Some(expr_result_id), b.exit_bindings.as_slice(), &carrier_phis, &remapper, debug, )? } } else { // No loop variable binding, resolve normally expr_result_resolver::ExprResultResolver::resolve( Some(expr_result_id), b.exit_bindings.as_slice(), &carrier_phis, &remapper, debug, )? } } else { // No loop variable name, resolve normally expr_result_resolver::ExprResultResolver::resolve( Some(expr_result_id), b.exit_bindings.as_slice(), &carrier_phis, &remapper, debug, )? } } else { None } } else { None }; // Return expr_result if present, otherwise fall back to exit_phi_result_id if let Some(resolved) = expr_result_value { trace.stderr_if( &format!( "[cf_loop/joinir] Phase 246-EX-FIX: Returning expr_result_value {:?}", resolved ), debug, ); Ok(Some(resolved)) } else { // Fallback: return exit_phi_result_id (for legacy patterns or carrier-only loops) trace.stderr_if( &format!( "[cf_loop/joinir] Phase 221-R: Returning exit_phi_result_id (fallback): {:?}", exit_phi_result_id ), debug && exit_phi_result_id.is_some(), ); 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, remapper: &mut crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper, reserved_ids: &std::collections::HashSet, debug: bool, ) -> Result<(), String> { let trace = trace::trace(); trace.stderr_if( &format!( "[cf_loop/joinir] Phase 3: Remapping {} ValueIds (reserved: {})", used_values.len(), reserved_ids.len() ), debug, ); for old_value in used_values { // 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 trace.stderr_if( &format!( "[cf_loop/joinir] Phase 201-A: Skipping reserved PHI dst {:?}", candidate ), debug, ); }; remapper.set_value(*old_value, new_value); trace.stderr_if( &format!( "[cf_loop/joinir] Value remap: {:?} → {:?}", old_value, new_value ), debug, ); } Ok(()) } /// Verify that PHI dst values are not overwritten by later instructions (Phase 204-2) /// /// # Checks /// /// 1. PHI instructions define dst values in header block /// 2. No subsequent instruction in the same block overwrites these dsts /// /// # Rationale /// /// PHI dst overwrite violates SSA invariant (single assignment) and causes undefined behavior. /// While Phase 201 JoinValueSpace prevents ValueId collisions, manual coding errors can still /// occur during pattern lowering. /// /// # Panics /// /// Panics in debug mode if PHI dst is overwritten by a later instruction. #[cfg(debug_assertions)] fn verify_no_phi_dst_overwrite( func: &crate::mir::MirFunction, header_block: crate::mir::BasicBlockId, loop_info: &LoopHeaderPhiInfo, ) { if loop_info.carrier_phis.is_empty() { return; // No PHIs to verify } let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| { panic!( "[JoinIRVerifier] Header block {} not found ({} blocks in func)", header_block, func.blocks.len() ) }); // 1. Collect all PHI dsts let phi_dsts: std::collections::HashSet = loop_info .carrier_phis .values() .map(|entry| entry.phi_dst) .collect(); // 2. Check instructions after PHI definitions let mut after_phis = false; for instr in &header_block_data.instructions { match instr { crate::mir::MirInstruction::Phi { dst, .. } => { // PHI instructions come first in basic block if after_phis { panic!( "[JoinIRVerifier] PHI instruction {:?} appears after non-PHI instructions in block {}", dst, header_block.0 ); } } _ => { after_phis = true; // Check if this instruction writes to a PHI dst if let Some(dst) = get_instruction_dst(instr) { if phi_dsts.contains(&dst) { panic!( "[JoinIRVerifier/Phase204] PHI dst {:?} is overwritten by instruction in header block {}: {:?}", dst, header_block.0, instr ); } } } } } } /// Helper: Extract dst ValueId from MirInstruction (Phase 204-2) #[cfg(debug_assertions)] fn get_instruction_dst(instr: &crate::mir::MirInstruction) -> Option { use crate::mir::MirInstruction; match instr { // Instructions with always-present dst MirInstruction::Const { dst, .. } | MirInstruction::Load { dst, .. } | MirInstruction::UnaryOp { dst, .. } | MirInstruction::BinOp { dst, .. } | MirInstruction::Compare { dst, .. } | MirInstruction::TypeOp { dst, .. } | MirInstruction::NewBox { dst, .. } | MirInstruction::NewClosure { dst, .. } | MirInstruction::Copy { dst, .. } | MirInstruction::Cast { dst, .. } | MirInstruction::TypeCheck { dst, .. } | MirInstruction::Phi { dst, .. } | MirInstruction::ArrayGet { dst, .. } | MirInstruction::RefNew { dst, .. } | MirInstruction::RefGet { dst, .. } | MirInstruction::WeakNew { dst, .. } | MirInstruction::WeakLoad { dst, .. } | MirInstruction::WeakRef { dst, .. } | MirInstruction::FutureNew { dst, .. } | MirInstruction::Await { dst, .. } => Some(*dst), // Instructions with Option dst MirInstruction::BoxCall { dst, .. } | MirInstruction::ExternCall { dst, .. } | MirInstruction::Call { dst, .. } | MirInstruction::PluginInvoke { dst, .. } => *dst, // Instructions without dst (side-effects only) _ => None, } } /// Verify PHI inputs are defined (Phase 204-3 - Conservative sanity checks) /// /// # Checks /// /// 1. PHI inputs have reasonable ValueId values (< threshold) /// 2. No obviously undefined values (e.g., suspiciously large IDs) /// /// # Note /// /// Full data-flow analysis (DFA) verification is deferred to Phase 205+. /// This function only performs conservative sanity checks. /// /// # Panics /// /// Panics in debug mode if suspicious PHI inputs are detected. #[cfg(debug_assertions)] fn verify_phi_inputs_defined( func: &crate::mir::MirFunction, header_block: crate::mir::BasicBlockId, ) { let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| { panic!( "[JoinIRVerifier] Header block {} not found ({} blocks in func)", header_block, func.blocks.len() ) }); for instr in &header_block_data.instructions { if let crate::mir::MirInstruction::Phi { dst, inputs, type_hint: _, } = instr { for (value_id, pred_block) in inputs { // Conservative sanity check: ValueId should not be suspiciously large // Phase 201 JoinValueSpace uses regions: // - PHI Reserved: 0-99 // - Param: 100-999 // - Local: 1000+ // - Reasonable max: 100000 (arbitrary but catches obvious bugs) if value_id.0 >= 100000 { panic!( "[JoinIRVerifier/Phase204-3] PHI {:?} has suspiciously large input {:?} from predecessor block {:?}", dst, value_id, pred_block ); } } } } } /// Verify all loop contracts for a merged JoinIR function /// /// This is the main entry point for verification. It runs all checks /// and panics if any contract violation is found. /// /// # Panics /// /// Panics in debug mode if any contract violation is detected. #[cfg(debug_assertions)] fn verify_joinir_contracts( func: &crate::mir::MirFunction, header_block: crate::mir::BasicBlockId, exit_block: crate::mir::BasicBlockId, loop_info: &LoopHeaderPhiInfo, boundary: &JoinInlineBoundary, ) { // Phase 135 P1 Step 1: Verify condition_bindings consistency (before merge) contract_checks::verify_condition_bindings_consistent(boundary); contract_checks::verify_loop_header_phis(func, header_block, loop_info, boundary); verify_no_phi_dst_overwrite(func, header_block, loop_info); // Phase 204-2 verify_phi_inputs_defined(func, header_block); // Phase 204-3 contract_checks::verify_exit_line(func, exit_block, boundary); contract_checks::verify_valueid_regions(loop_info, boundary); // Phase 205-4 // Phase 135 P1 Step 2: Verify header PHI dsts not redefined (after merge) let phi_dsts: std::collections::HashSet<_> = loop_info .carrier_phis .values() .map(|entry| entry.phi_dst) .collect(); contract_checks::verify_header_phi_dsts_not_redefined(func, header_block, &phi_dsts); }