diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs index d4a78ac9..b57650e7 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs @@ -1,5 +1,5 @@ use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; -use crate::mir::ValueId; +use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; use std::collections::BTreeMap; #[cfg(debug_assertions)] @@ -7,10 +7,73 @@ use super::LoopHeaderPhiInfo; #[cfg(debug_assertions)] use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN}; #[cfg(debug_assertions)] -use crate::mir::{BasicBlockId, MirFunction, MirInstruction}; -#[cfg(debug_assertions)] use std::collections::HashMap; +/// Contract check (Fail-Fast): every Branch/Jump target must exist in the function. +/// +/// This prevents latent runtime failures like: +/// - "Invalid basic block: bb not found" +/// +/// Typical root causes: +/// - Jumping to a continuation function entry block whose blocks were intentionally skipped +/// - Allocating a block ID but forgetting to insert the block +pub(super) fn verify_all_terminator_targets_exist( + func: &MirFunction, + allowed_missing_targets: &[BasicBlockId], +) -> Result<(), String> { + use crate::mir::join_ir::lowering::error_tags; + + for (block_id, block) in &func.blocks { + let Some(term) = &block.terminator else { continue }; + + match term { + MirInstruction::Jump { target } => { + if !func.blocks.contains_key(target) && !allowed_missing_targets.contains(target) { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_jump_target", + &format!( + "Jump target {:?} not found in function '{}' (from block {:?})", + target, func.signature.name, block_id + ), + "ensure merge inserts all remapped blocks and does not Jump to skipped continuation blocks (k_exit must Jump to exit_block_id)", + )); + } + } + MirInstruction::Branch { + then_bb, else_bb, .. + } => { + if !func.blocks.contains_key(then_bb) + && !allowed_missing_targets.contains(then_bb) + { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_branch_target", + &format!( + "Branch then_bb {:?} not found in function '{}' (from block {:?})", + then_bb, func.signature.name, block_id + ), + "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", + )); + } + if !func.blocks.contains_key(else_bb) + && !allowed_missing_targets.contains(else_bb) + { + return Err(error_tags::freeze_with_hint( + "joinir/merge/contract/missing_branch_target", + &format!( + "Branch else_bb {:?} not found in function '{}' (from block {:?})", + else_bb, func.signature.name, block_id + ), + "ensure all remapped blocks are inserted and Branch targets are block-remapped consistently", + )); + } + } + _ => {} + } + } + + Ok(()) +} + /// Phase 118 P2: Contract check (Fail-Fast) - exit_bindings carriers must have exit PHI dsts. /// /// This prevents latent "Carrier '' not found in carrier_phis" failures later in diff --git a/src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs b/src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs index 28763f8a..e29b8f34 100644 --- a/src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs @@ -70,6 +70,7 @@ impl ExitLineOrchestrator { /// - builder: MirBuilder with variable_map to update /// - boundary: JoinInlineBoundary with exit_bindings /// - carrier_phis: Map from carrier name to PHI dst ValueId (Phase 33-13) + /// - remapped_exit_values: Map from carrier name to remapped ValueId (Phase 131 P1.5) /// - debug: Debug logging enabled /// /// # Returns @@ -77,11 +78,15 @@ impl ExitLineOrchestrator { /// /// # Process /// 1. Validate exit_bindings (empty case) - /// 2. Delegate to ExitLineReconnector with carrier_phis + /// 2. Delegate to ExitLineReconnector with carrier_phis and remapped_exit_values + /// + /// # Phase 131 P1.5: DirectValue Mode Support + /// When boundary.exit_reconnect_mode == DirectValue, uses remapped_exit_values instead of carrier_phis pub fn execute( builder: &mut crate::mir::builder::MirBuilder, boundary: &crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary, carrier_phis: &BTreeMap, + remapped_exit_values: &BTreeMap, // Phase 131 P1.5 debug: bool, ) -> Result<(), String> { let trace = crate::mir::builder::control_flow::joinir::trace::trace(); @@ -96,8 +101,8 @@ impl ExitLineOrchestrator { ); } - // Phase 33-13: Delegate to ExitLineReconnector with carrier_phis - ExitLineReconnector::reconnect(builder, boundary, carrier_phis, debug)?; + // Phase 33-13 + Phase 131 P1.5: Delegate to ExitLineReconnector with carrier_phis and remapped_exit_values + ExitLineReconnector::reconnect(builder, boundary, carrier_phis, remapped_exit_values, debug)?; if verbose { trace.stderr_if("[joinir/exit-line] orchestrator complete", true); diff --git a/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs b/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs index 13133092..f6d53b20 100644 --- a/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs +++ b/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs @@ -67,16 +67,23 @@ impl ExitLineReconnector { /// Now, we use the carrier_phis map from exit_phi_builder, which contains /// the actual PHI dst ValueIds that are defined in the exit block. /// + /// # Phase 131 P1.5: DirectValue Mode Support + /// + /// For Normalized shadow (exit_reconnect_mode = DirectValue): + /// - Uses remapped_exit_values instead of carrier_phis + /// - No PHI generation, direct value assignment + /// /// # Algorithm /// /// For each exit_binding: - /// 1. Look up the PHI dst for this carrier in carrier_phis - /// 2. Update variable_ctx.variable_map[binding.carrier_name] with PHI dst + /// 1. Look up the PHI dst (Phi mode) or remapped value (DirectValue mode) + /// 2. Update variable_ctx.variable_map[binding.carrier_name] with the value /// 3. Log each update (if debug enabled) pub fn reconnect( builder: &mut MirBuilder, boundary: &JoinInlineBoundary, carrier_phis: &BTreeMap, + remapped_exit_values: &BTreeMap, // Phase 131 P1.5 debug: bool, ) -> Result<(), String> { let trace = crate::mir::builder::control_flow::joinir::trace::trace(); @@ -127,10 +134,23 @@ impl ExitLineReconnector { ); } + // Phase 131 P1.5: Check exit_reconnect_mode + use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode}; + let use_direct_values = boundary.exit_reconnect_mode == ExitReconnectMode::DirectValue; + + if verbose { + trace.stderr_if( + &format!( + "[joinir/exit-line] mode={:?}, use_direct_values={}", + boundary.exit_reconnect_mode, use_direct_values + ), + true, + ); + } + // Process each exit binding for binding in &boundary.exit_bindings { // Phase 228-8: Skip ConditionOnly carriers (no variable_ctx.variable_map update needed) - use crate::mir::join_ir::lowering::carrier_info::CarrierRole; if binding.role == CarrierRole::ConditionOnly { if verbose { trace.stderr_if( @@ -144,21 +164,27 @@ impl ExitLineReconnector { continue; } - // Phase 33-13: Look up the PHI dst for this carrier - let phi_dst = carrier_phis.get(&binding.carrier_name); + // Phase 131 P1.5: Choose value source based on mode + let final_value = if use_direct_values { + // DirectValue mode: Use remapped_exit_values (SSOT: merge owns remapper) + remapped_exit_values.get(&binding.carrier_name).copied() + } else { + // Phi mode: Use carrier_phis (Phase 33-13) + carrier_phis.get(&binding.carrier_name).copied() + }; if verbose { trace.stderr_if( &format!( - "[joinir/exit-line] carrier '{}' → phi_dst={:?}", - binding.carrier_name, phi_dst + "[joinir/exit-line] carrier '{}' → final_value={:?} (mode={:?})", + binding.carrier_name, final_value, boundary.exit_reconnect_mode ), true, ); } - // Update variable_ctx.variable_map with PHI dst - if let Some(&phi_value) = phi_dst { + // Update variable_ctx.variable_map with final value + if let Some(phi_value) = final_value { if let Some(var_vid) = builder .variable_ctx .variable_map diff --git a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 49bc0c27..3366f0a1 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -13,13 +13,15 @@ use super::merge_result::MergeResult; use super::tail_call_classifier::{classify_tail_call, TailCallKind}; use super::super::trace; use crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper; +use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; +use crate::mir::join_ir_vm_bridge::join_func_name; use crate::mir::{BasicBlock, BasicBlockId, MirInstruction, MirModule, ValueId}; use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism -/// Phase 179-A: Exit continuation function name (MIR convention) -/// This is the standard name for k_exit continuations in JoinIR → MIR lowering -const K_EXIT_FUNC_NAME: &str = "join_func_2"; +fn k_exit_function_name() -> String { + join_func_name(crate::mir::join_ir::JoinFuncId::new(2)) +} /// Phase 4: Merge ALL functions and rewrite instructions /// @@ -106,13 +108,14 @@ pub(super) fn merge_and_rewrite( functions_merge.sort_by_key(|(name, _)| name.as_str()); let entry_func_name = functions_merge.first().map(|(name, _)| name.as_str()); + let k_exit_func_name = k_exit_function_name(); for (func_name, func) in functions_merge { - // Phase 33-15: Identify continuation functions (join_func_2 = k_exit, etc.) + // Phase 33-15: Identify continuation functions (k_exit, etc.) // Continuation functions receive values from Jump args, not as independent sources // We should NOT collect their Return values for exit_phi_inputs // Phase 179-A: Use named constant for k_exit function name - let is_continuation_func = func_name == K_EXIT_FUNC_NAME || func_name.ends_with("k_exit"); + let is_continuation_func = func_name == &k_exit_func_name; if debug { log!( @@ -206,6 +209,10 @@ pub(super) fn merge_and_rewrite( // Remap instructions (Phase 189: Convert inter-function Calls to control flow) let mut found_tail_call = false; let mut tail_call_target: Option<(BasicBlockId, Vec)> = None; + // Phase 131 P2: k_exit (continuation) must not jump to its entry block. + // We skip merging continuation functions, so any tail-call to k_exit must be + // lowered as an exit jump to `exit_block_id` (and contribute exit values). + let mut k_exit_tail_call_args: Option> = None; // Phase 177-3: Check if this block is the loop header with PHI nodes let is_loop_header_with_phi = @@ -289,8 +296,29 @@ pub(super) fn merge_and_rewrite( // Phase 189: Detect tail calls and save parameters if let MirInstruction::Call { func, args, .. } = inst { - if let Some(func_name) = value_to_func_name.get(func) { - if let Some(&target_block) = function_entry_map.get(func_name) { + if let Some(callee_name) = value_to_func_name.get(func) { + // Phase 131 P2: Treat k_exit calls as "exit" (not a normal tail call). + // Otherwise we'd Jump to the k_exit entry block, but continuation + // blocks are intentionally not merged (skip), causing invalid BB. + if callee_name == &k_exit_func_name { + let remapped_args: Vec = args + .iter() + .map(|&v| remapper.get_value(v).unwrap_or(v)) + .collect(); + k_exit_tail_call_args = Some(remapped_args); + found_tail_call = true; + if debug { + log!( + true, + "[cf_loop/joinir] Phase 131 P2: Detected tail call to k_exit '{}' (args={:?}), will Jump to exit_block_id={:?}", + callee_name, + args, + exit_block_id + ); + } + continue; // Skip the Call instruction itself + } + if let Some(&target_block) = function_entry_map.get(callee_name) { // This is a tail call - save info and skip the Call instruction itself let remapped_args: Vec = args .iter() @@ -306,6 +334,7 @@ pub(super) fn merge_and_rewrite( func_name, args ); } + continue; // Skip the Call instruction itself } } @@ -707,6 +736,7 @@ pub(super) fn merge_and_rewrite( } // Phase 227: Filter out ConditionOnly carriers from exit PHI + // Phase 131 P1.5: For DirectValue mode, if no header PHI, use host_slot (initial value) for binding in &b.exit_bindings { if binding.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::ConditionOnly { continue; @@ -719,6 +749,18 @@ pub(super) fn merge_and_rewrite( .entry(binding.carrier_name.clone()) .or_insert_with(Vec::new) .push((new_block_id, phi_dst)); + } else if b.exit_reconnect_mode == crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::DirectValue { + // Phase 131 P1.5: DirectValue mode fallback - use host_slot (initial value) + // This handles k_exit blocks that don't have jump_args and no header PHI + carrier_inputs + .entry(binding.carrier_name.clone()) + .or_insert_with(Vec::new) + .push((new_block_id, binding.host_slot)); + log!( + verbose, + "[cf_loop/joinir] Phase 131 P1.5: DirectValue fallback for '{}': using host_slot {:?}", + binding.carrier_name, binding.host_slot + ); } } } @@ -753,6 +795,50 @@ pub(super) fn merge_and_rewrite( } } + // Phase 131 P2: If this block tail-calls k_exit, it is an "exit edge" for the fragment. + // We must Jump to the shared exit_block_id (not to k_exit's entry block). + if let Some(args) = k_exit_tail_call_args { + if let Some(b) = boundary { + let collector = ExitArgsCollectorBox::new(); + let collection_result = + collector.collect(&b.exit_bindings, &args, new_block_id, strict_exit)?; + if let Some(expr_result_value) = collection_result.expr_result_value { + exit_phi_inputs.push((collection_result.block_id, expr_result_value)); + } + for (carrier_name, pair) in collection_result.carrier_values { + carrier_inputs + .entry(carrier_name) + .or_insert_with(Vec::new) + .push(pair); + } + } else if strict_exit { + return Err(error_tags::freeze_with_hint( + "phase131/k_exit/no_boundary", + "k_exit tail call detected without JoinInlineBoundary", + "k_exit must be handled as fragment exit; ensure boundary is passed when merging JoinIR fragments", + )); + } + + // Strict guard: if we've already set a Jump target, it must be the exit block. + if strict_exit { + if let Some(MirInstruction::Jump { target }) = &new_block.terminator { + if *target != exit_block_id { + return Err(error_tags::freeze_with_hint( + "phase131/k_exit/wrong_jump_target", + &format!( + "k_exit tail call lowered to Jump {:?}, expected exit_block_id {:?}", + target, exit_block_id + ), + "k_exit continuation blocks are not merged; ensure k_exit calls become an exit jump to the shared exit block", + )); + } + } + } + new_block.terminator = Some(MirInstruction::Jump { + target: exit_block_id, + }); + } + // Phase 189 FIX: Ensure instruction_spans matches instructions length // The original spans may not cover all instructions after remapping/adding // (PHI instructions, tail call parameter bindings, etc.) @@ -846,9 +932,47 @@ pub(super) fn merge_and_rewrite( } } + // Phase 131 P2: DirectValue mode remapped_exit_values SSOT + // + // Contract (DirectValue): + // - boundary.exit_bindings[*].join_exit_value is a JoinIR-side ValueId that must be defined + // in the merged MIR (e.g., final env value produced by the loop body). + // - remapper owns JoinIR→Host mapping, so merge_and_rewrite is responsible for producing + // carrier_name → host ValueId. + let remapped_exit_values: BTreeMap = match boundary { + Some(b) + if b.exit_reconnect_mode + == crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::DirectValue => + { + let mut direct_values = BTreeMap::new(); + for binding in &b.exit_bindings { + if binding.role + == crate::mir::join_ir::lowering::carrier_info::CarrierRole::ConditionOnly + { + continue; + } + + let host_vid = remapper.get_value(binding.join_exit_value).ok_or_else(|| { + error_tags::freeze_with_hint( + "phase131/directvalue/remap_missing", + &format!( + "DirectValue: join_exit_value {:?} for carrier '{}' was not remapped", + binding.join_exit_value, binding.carrier_name + ), + "ensure exit_bindings.join_exit_value is included in merge used_values and references a value defined by the fragment", + ) + })?; + direct_values.insert(binding.carrier_name.clone(), host_vid); + } + direct_values + } + _ => BTreeMap::new(), + }; + Ok(MergeResult { exit_block_id, exit_phi_inputs, carrier_inputs, + remapped_exit_values, // Phase 131 P1.5 }) } diff --git a/src/mir/builder/control_flow/joinir/merge/merge_result.rs b/src/mir/builder/control_flow/joinir/merge/merge_result.rs index d418f6d6..ed397690 100644 --- a/src/mir/builder/control_flow/joinir/merge/merge_result.rs +++ b/src/mir/builder/control_flow/joinir/merge/merge_result.rs @@ -17,6 +17,16 @@ pub struct MergeResult { pub exit_phi_inputs: Vec<(BasicBlockId, ValueId)>, /// Map of carrier_name → Vec of (from_block, exit_value) for carrier PHI generation pub carrier_inputs: BTreeMap>, + /// Phase 131 P1.5: Remapped exit values (JoinIR → Host ValueId) + /// + /// This is the SSOT for exit value remapping. The merge box owns the remapper, + /// so it's responsible for converting JoinIR exit values to host ValueIds. + /// + /// Key: carrier_name (from exit_bindings) + /// Value: host ValueId (remapper.get_value(binding.join_exit_value)) + /// + /// Used by DirectValue mode to update variable_map without PHI generation. + pub remapped_exit_values: BTreeMap, } impl MergeResult { @@ -27,6 +37,7 @@ impl MergeResult { exit_block_id, exit_phi_inputs: Vec::new(), carrier_inputs: BTreeMap::new(), + remapped_exit_values: BTreeMap::new(), // Phase 131 P1.5 } } diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index 00d6d3d6..5d8ade1d 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -34,6 +34,7 @@ pub use loop_header_phi_info::LoopHeaderPhiInfo; use super::trace; use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; use crate::mir::{MirModule, ValueId}; +use std::collections::BTreeMap; /// Phase 49-3.2: Merge JoinIR-generated MIR blocks into current_function /// @@ -727,20 +728,76 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( LoopHeaderPhiBuilder::finalize(builder, &loop_header_phi_info, debug)?; } + // Contract check (Fail-Fast): ensure we didn't leave dangling Jump/Branch targets. + // Strict/dev only: avoids runtime "Invalid basic block" failures. + if crate::config::env::joinir_strict_enabled() || crate::config::env::joinir_dev_enabled() { + 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). + contract_checks::verify_all_terminator_targets_exist( + current_func, + &[merge_result.exit_block_id], + )?; + } + } + // 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! - let (exit_phi_result_id, exit_carrier_phis) = exit_phi_builder::build_exit_phi( - builder, - merge_result.exit_block_id, - &merge_result.exit_phi_inputs, - &merge_result.carrier_inputs, + // 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 || crate::config::env::joinir_dev_enabled(), + ); + + 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 { - contract_checks::verify_exit_bindings_have_exit_phis(boundary, &exit_carrier_phis)?; + 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) @@ -818,8 +875,17 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( // 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, debug)?; + 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; diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs index 939c03ac..0fe4ab84 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs @@ -383,6 +383,7 @@ mod tests { expr_result: None, // Phase 33-14: Add missing field loop_var_name: None, // Phase 33-16: Add missing field carrier_info: None, // Phase 228: Add missing field + exit_reconnect_mode: crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::default(), // Phase 131 P1.5 }; builder diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs index 669311e6..af72c79d 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs @@ -137,6 +137,7 @@ mod tests { expr_result: None, // Phase 33-14: Add missing field loop_var_name: None, // Phase 33-16: Add missing field carrier_info: None, // Phase 228: Add missing field + exit_reconnect_mode: crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::default(), // Phase 131 P1.5 }; apply_exit_bindings_to_boundary(&carrier_info, &exit_meta, &variable_map, &mut boundary) diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index b6ede26b..dee625c2 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -251,8 +251,9 @@ impl MirBuilder { /// Phase 49-3: JoinIR Frontend integration implementation /// /// Routes loop compilation through either: - /// 1. Pattern-based router (Phase 194+) - preferred for new patterns - /// 2. Legacy binding path (Phase 49-3) - for whitelisted functions only + /// 1. Normalized shadow (Phase 131 P1) - dev-only for loop(true) break-once + /// 2. Pattern-based router (Phase 194+) - preferred for new patterns + /// 3. Legacy binding path (Phase 49-3) - for whitelisted functions only pub(in crate::mir::builder) fn cf_loop_joinir_impl( &mut self, condition: &ASTNode, @@ -260,6 +261,13 @@ impl MirBuilder { func_name: &str, debug: bool, ) -> Result, String> { + // Phase 131 P1: Try Normalized shadow first (dev-only) + if crate::config::env::joinir_dev_enabled() { + if let Some(result) = self.try_normalized_shadow(condition, body, func_name, debug)? { + return Ok(Some(result)); + } + } + // Phase 137-2/137-4: Dev-only observation via Loop Canonicalizer if crate::config::env::joinir_dev_enabled() { use crate::ast::Span; @@ -390,4 +398,169 @@ impl MirBuilder { // Delegate to legacy binding path (routing_legacy_binding.rs) self.cf_loop_joinir_legacy_binding(condition, body, func_name, debug) } + + /// Phase 131 P1: Try Normalized shadow lowering (dev-only) + /// + /// Returns: + /// - Ok(Some(value_id)): Successfully lowered and merged via Normalized + /// - Ok(None): Out of scope (not a Normalized pattern) + /// - Err(msg): In scope but failed (Fail-Fast in strict mode) + fn try_normalized_shadow( + &mut self, + condition: &ASTNode, + body: &[ASTNode], + func_name: &str, + debug: bool, + ) -> Result, String> { + use crate::ast::Span; + use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox; + use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox; + use crate::mir::control_tree::StepTreeBuilderBox; + + // Build StepTree from loop AST + let loop_ast = ASTNode::Loop { + condition: Box::new(condition.clone()), + body: body.to_vec(), + span: Span::unknown(), + }; + let tree = StepTreeBuilderBox::build_from_ast(&loop_ast); + + // Collect available inputs from MirBuilder state + let available_inputs = AvailableInputsCollectorBox::collect(self, None); + + trace::trace().routing( + "router/normalized", + func_name, + &format!( + "Trying Normalized shadow lowering (available_inputs: {})", + available_inputs.len() + ), + ); + + // Try Normalized lowering (loop(true) break-once pattern) + match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree, &available_inputs) { + Ok(Some((join_module, join_meta))) => { + trace::trace().routing( + "router/normalized", + func_name, + &format!( + "Normalized lowering succeeded ({} functions)", + join_module.functions.len() + ), + ); + + // Phase 131 P1.5: Create boundary with DirectValue mode + // + // Strategy (SSOT: merge owns remapper): + // 1. Create boundary with exit_bindings from meta + // 2. Set exit_reconnect_mode = DirectValue (no PHI generation) + // 3. Merge populates MergeResult.remapped_exit_values (JoinIR → Host ValueIds) + // 4. Use remapped_exit_values for direct variable_map reconnection + use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode}; + use crate::mir::join_ir::lowering::inline_boundary::{JoinInlineBoundary, LoopExitBinding}; + + // Build exit_bindings from meta + let exit_bindings: Vec = join_meta + .exit_meta + .exit_values + .iter() + .map(|(carrier_name, join_exit_value)| { + // Get host_slot from variable_map + let host_slot = self + .variable_ctx + .variable_map + .get(carrier_name) + .copied() + .unwrap_or_else(|| { + panic!( + "[Phase 131 P1.5] Carrier '{}' not in variable_map (available: {:?})", + carrier_name, + self.variable_ctx.variable_map.keys().collect::>() + ) + }); + + LoopExitBinding { + carrier_name: carrier_name.clone(), + join_exit_value: *join_exit_value, + host_slot, + role: CarrierRole::LoopState, + } + }) + .collect(); + + // Create boundary with DirectValue mode + let mut boundary = JoinInlineBoundary::new_with_exit_bindings( + vec![], // No join_inputs for Normalized + vec![], // No host_inputs for Normalized + exit_bindings, + ); + boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; // Phase 131 P1.5: No PHI + + // Merge with boundary - this will populate MergeResult.remapped_exit_values + use crate::mir::builder::control_flow::joinir::merge; + use crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir_with_meta; + use crate::mir::join_ir::frontend::JoinFuncMetaMap; + use std::collections::BTreeMap; + + let empty_meta: JoinFuncMetaMap = BTreeMap::new(); + let mir_module = bridge_joinir_to_mir_with_meta(&join_module, &empty_meta) + .map_err(|e| format!("[normalized/pipeline] MIR conversion failed: {:?}", e))?; + + // Merge with boundary - this populates MergeResult.remapped_exit_values + // and calls ExitLineOrchestrator with DirectValue mode + let _exit_phi_result = merge::merge_joinir_mir_blocks( + self, + &mir_module, + Some(&boundary), + debug, + )?; + + trace::trace().routing( + "router/normalized", + func_name, + &format!( + "Normalized merge + reconnection completed ({} exit bindings)", + boundary.exit_bindings.len() + ), + ); + + // Phase 131 P1.5: Loop executed successfully, return void constant + use crate::mir::{ConstValue, MirInstruction}; + let void_id = self.next_value_id(); + self.emit_instruction(MirInstruction::Const { + dst: void_id, + value: ConstValue::Void, + })?; + + Ok(Some(void_id)) + } + Ok(None) => { + // Out of scope (not a Normalized pattern) + trace::trace().routing( + "router/normalized", + func_name, + "Normalized lowering: out of scope", + ); + Ok(None) + } + Err(e) => { + // In scope but failed - Fail-Fast in strict mode + let msg = format!( + "Phase 131/normalized: Failed to lower loop(true) break-once pattern in '{}': {}", + func_name, e + ); + if crate::config::env::joinir_dev::strict_enabled() { + use crate::mir::join_ir::lowering::error_tags; + return Err(error_tags::freeze_with_hint( + "phase131/normalized_loop/internal", + &e, + "Loop should be supported by Normalized but conversion failed. \ + Check that condition is Bool(true) and body ends with break.", + )); + } + trace::trace().routing("router/normalized/error", func_name, &msg); + Ok(None) // Non-strict: fall back to existing patterns + } + } + } } diff --git a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs index 6d2a5ce4..a6c99d4d 100644 --- a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs +++ b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs @@ -34,6 +34,7 @@ use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind, StepTree}; use crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta; use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::{ConstValue, JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst}; +use crate::mir::join_ir_vm_bridge::join_func_name; use crate::mir::ValueId; use std::collections::BTreeMap; @@ -126,16 +127,18 @@ impl LoopTrueBreakOnceBuilderBox { let mut next_value_id: u32 = 1; - // Function IDs (stable, dev-only) + // Function IDs (stable, dev-only). + // + // Contract: JoinIR→MIR bridge uses `JoinFuncId(2)` as the exit continuation (`k_exit`). let main_id = JoinFuncId::new(0); let loop_step_id = JoinFuncId::new(1); - let loop_body_id = JoinFuncId::new(2); - let k_exit_id = JoinFuncId::new(3); + let k_exit_id = JoinFuncId::new(2); + let loop_body_id = JoinFuncId::new(3); // main(env): → TailCall(loop_step, env) let main_params = alloc_env_params(&env_fields, &mut next_value_id); let mut env_main = build_env_map(&env_fields, &main_params); - let mut main_func = JoinFunction::new(main_id, "main".to_string(), main_params); + let mut main_func = JoinFunction::new(main_id, "join_func_0".to_string(), main_params); // Lower prefix (pre-loop) statements into main for n in prefix_nodes { @@ -174,35 +177,28 @@ impl LoopTrueBreakOnceBuilderBox { dst: None, }); - // loop_step(env): if true { TailCall(loop_body, env) } else { TailCall(k_exit, env) } + // loop_step(env): TailCall(loop_body, env) + // + // Contract: loop condition is Bool(true), so loop_step has no conditional branch. + // This avoids introducing an unreachable "else" exit path that would require PHI. let loop_step_params = alloc_env_params(&env_fields, &mut next_value_id); let env_loop_step = build_env_map(&env_fields, &loop_step_params); - let mut loop_step_func = JoinFunction::new(loop_step_id, "loop_step".to_string(), loop_step_params); - - // Generate condition: true - let cond_vid = alloc_value_id(&mut next_value_id); - loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: cond_vid, - value: ConstValue::Bool(true), - })); - - // Conditional jump: if true → loop_body, else → k_exit + let mut loop_step_func = + JoinFunction::new(loop_step_id, "join_func_1".to_string(), loop_step_params); let loop_step_args = collect_env_args(&env_fields, &env_loop_step)?; - loop_step_func.body.push(JoinInst::Jump { - cont: loop_body_id.as_cont(), - args: loop_step_args.clone(), - cond: Some(cond_vid), - }); - loop_step_func.body.push(JoinInst::Jump { - cont: k_exit_id.as_cont(), + loop_step_func.body.push(JoinInst::Call { + func: loop_body_id, args: loop_step_args, - cond: None, + k_next: None, + dst: None, }); // loop_body(env): → TailCall(k_exit, env) let loop_body_params = alloc_env_params(&env_fields, &mut next_value_id); let mut env_loop_body = build_env_map(&env_fields, &loop_body_params); - let mut loop_body_func = JoinFunction::new(loop_body_id, "loop_body".to_string(), loop_body_params); + let env_loop_body_before = env_loop_body.clone(); + let mut loop_body_func = + JoinFunction::new(loop_body_id, "join_func_3".to_string(), loop_body_params); // Lower body statements for n in body_prefix { @@ -234,6 +230,47 @@ impl LoopTrueBreakOnceBuilderBox { // loop_body → k_exit tailcall let loop_body_args = collect_env_args(&env_fields, &env_loop_body)?; + if crate::config::env::joinir_strict_enabled() { + for n in body_prefix { + let StepNode::Stmt { kind, .. } = n else { continue }; + let StepStmtKind::Assign { target, .. } = kind else { continue }; + let Some(target_name) = target.as_ref() else { continue }; + if !env_layout.writes.iter().any(|w| w == target_name) { + continue; + } + + let before = env_loop_body_before.get(target_name).copied(); + let after = env_loop_body.get(target_name).copied(); + if let (Some(before), Some(after)) = (before, after) { + if before == after { + continue; + } + + let idx = env_fields + .iter() + .position(|f| f == target_name) + .ok_or_else(|| { + error_tags::freeze_with_hint( + "phase131/loop_true/env_field_missing", + &format!("env_fields missing updated target '{target_name}'"), + "ensure EnvLayout.env_fields() is the SSOT used to build both env maps and call args", + ) + })?; + + let passed = loop_body_args.get(idx).copied().unwrap_or(ValueId(0)); + if passed == before { + return Err(error_tags::freeze_with_hint( + "phase131/env_not_propagated", + &format!( + "loop_body updated '{target_name}' from {:?} to {:?}, but k_exit args still use the old ValueId {:?}", + before, after, passed + ), + "update env map before collecting k_exit args; use collect_env_args(env_fields, env) after assignments", + )); + } + } + } + } loop_body_func.body.push(JoinInst::Call { func: k_exit_id, args: loop_body_args, @@ -241,10 +278,30 @@ impl LoopTrueBreakOnceBuilderBox { dst: None, }); + // Phase 131 P2: ExitMeta SSOT (DirectValue) + // + // For Normalized shadow, the host variable_map reconnection must use the *final values* + // produced by the loop body (defined ValueIds), not the k_exit parameter placeholders. + // + // Contract: exit_values keys == env_layout.writes, values == final JoinIR-side ValueIds. + use crate::mir::join_ir::lowering::carrier_info::ExitMeta; + let mut exit_values_for_meta: Vec<(String, ValueId)> = Vec::new(); + for var_name in &env_layout.writes { + let final_vid = env_loop_body.get(var_name).copied().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase131/exit_meta/missing_final_value", + &format!("env missing final value for write '{var_name}'"), + "ensure loop body assignments update the env map before exit meta is computed", + ) + })?; + exit_values_for_meta.push((var_name.clone(), final_vid)); + } + // k_exit(env): handle post-loop or return let k_exit_params = alloc_env_params(&env_fields, &mut next_value_id); let env_k_exit = build_env_map(&env_fields, &k_exit_params); - let mut k_exit_func = JoinFunction::new(k_exit_id, "k_exit".to_string(), k_exit_params); + let mut k_exit_func = + JoinFunction::new(k_exit_id, "join_func_2".to_string(), k_exit_params); // Handle post-loop statements or return if post_nodes.is_empty() { @@ -290,9 +347,15 @@ impl LoopTrueBreakOnceBuilderBox { module.add_function(loop_body_func); module.add_function(k_exit_func); module.entry = Some(main_id); - module.mark_normalized(); + // Phase 131 P1: Keep as Structured for execution via bridge + // (Normalized is only for dev observation/verification) - Ok(Some((module, JoinFragmentMeta::empty()))) + let exit_meta = ExitMeta { + exit_values: exit_values_for_meta, + }; + let meta = JoinFragmentMeta::carrier_only(exit_meta); + + Ok(Some((module, meta))) } /// Extract loop(true) pattern from StepTree root @@ -394,3 +457,218 @@ impl LoopTrueBreakOnceBuilderBox { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, LiteralValue, Span}; + use crate::mir::control_tree::step_tree::StepTreeBuilderBox; + + #[test] + fn test_loop_true_break_once_passes_updated_env_to_k_exit() { + let span = Span::unknown(); + let ast = ASTNode::Program { + statements: vec![ + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(0), + span: span.clone(), + }), + span: span.clone(), + }, + ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: span.clone(), + }), + body: vec![ + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: span.clone(), + }), + span: span.clone(), + }, + ASTNode::Break { span: span.clone() }, + ], + span: span.clone(), + }, + ASTNode::Return { + value: Some(Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + })), + span: span.clone(), + }, + ], + span, + }; + + let step_tree = StepTreeBuilderBox::build_from_ast(&ast); + let env_layout = EnvLayout::from_contract(&step_tree.contract, &BTreeMap::new()); + + let Some((module, _meta)) = + LoopTrueBreakOnceBuilderBox::lower(&step_tree, &env_layout).expect("lower failed") else { + panic!("expected loop_true_break_once pattern to be in-scope"); + }; + + let loop_body_id = JoinFuncId::new(3); + let k_exit_id = JoinFuncId::new(2); + let loop_body = module + .functions + .get(&loop_body_id) + .expect("phase131 test: missing loop_body function"); + let k_exit = module + .functions + .get(&k_exit_id) + .expect("phase131 test: missing k_exit function"); + + // Find the const 1 emitted in loop_body. + let const_one_dst = loop_body + .body + .iter() + .find_map(|inst| match inst { + JoinInst::Compute(MirLikeInst::Const { + dst, + value: ConstValue::Integer(1), + }) => Some(*dst), + _ => None, + }) + .expect("missing const 1 in loop_body"); + + // Find the tail call to k_exit and check that x argument is the updated value. + let call_args = loop_body + .body + .iter() + .find_map(|inst| match inst { + JoinInst::Call { + func, args, k_next, .. + } if *func == k_exit.id && k_next.is_none() => Some(args.clone()), + _ => None, + }) + .expect("missing tail call to k_exit from loop_body"); + + assert!(!call_args.is_empty(), "k_exit args must include env fields"); + assert_eq!( + call_args[0], const_one_dst, + "k_exit must receive updated x value" + ); + assert_ne!( + call_args[0], loop_body.params[0], + "k_exit must not receive the pre-update x param" + ); + + // Sanity: k_exit returns its x param in this phase. + assert!( + k_exit.body.iter().any(|inst| matches!(inst, JoinInst::Ret { value: Some(_) })), + "k_exit must return Some(value)" + ); + + // Bridge sanity: tail-call blocks must carry jump_args metadata for merge collection. + // This is required for DirectValue mode (no PHI) to reconnect carriers safely. + let mir_module = crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir(&module) + .expect("bridge_joinir_to_mir failed"); + let mir_loop_body_name = join_func_name(loop_body_id); + let mir_loop_body = mir_module + .functions + .values() + .find(|f| f.signature.name == mir_loop_body_name) + .expect("missing loop_body in bridged MirModule"); + let entry = mir_loop_body.entry_block; + let entry_block = mir_loop_body + .blocks + .get(&entry) + .expect("missing loop_body entry block"); + assert!( + entry_block.jump_args.is_some(), + "loop_body entry block must have jump_args metadata in bridged MIR" + ); + + // Loop-only (the routing path in real lowering): still must encode loop_step as a tail-call. + let loop_only_ast = ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + body: vec![ + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }; + let loop_only_tree = StepTreeBuilderBox::build_from_ast(&loop_only_ast); + let loop_only_layout = EnvLayout::from_contract(&loop_only_tree.contract, &BTreeMap::new()); + let Some((loop_only_module, _)) = + LoopTrueBreakOnceBuilderBox::lower(&loop_only_tree, &loop_only_layout).expect("lower failed") else { + panic!("expected loop-only pattern to be in-scope"); + }; + let loop_step_id = JoinFuncId::new(1); + let loop_step = loop_only_module + .functions + .get(&loop_step_id) + .expect("phase131 test (loop-only): missing loop_step function"); + assert!( + matches!(loop_step.body.first(), Some(JoinInst::Call { .. })), + "loop_step must be a tail-call to loop_body (not Jump/Ret)" + ); + + let mut k_exit_call_count = 0usize; + for f in loop_only_module.functions.values() { + for inst in &f.body { + if matches!(inst, JoinInst::Call { func, .. } if *func == k_exit_id) { + k_exit_call_count += 1; + } + } + } + assert_eq!( + k_exit_call_count, 1, + "loop_only module must have exactly 1 tail-call to k_exit" + ); + + // Bridge sanity: loop-only lowered MIR should contain exactly one call site to k_exit + // and it must pass a single consistent first argument (x). + let loop_only_mir = crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir(&loop_only_module) + .expect("bridge_joinir_to_mir failed (loop-only)"); + const FUNC_NAME_ID_BASE: u32 = 90000; + let k_exit_func_id = crate::mir::ValueId(FUNC_NAME_ID_BASE + k_exit_id.0); + let mut args0 = std::collections::BTreeSet::new(); + for f in loop_only_mir.functions.values() { + for bb in f.blocks.values() { + for inst in &bb.instructions { + if let crate::mir::MirInstruction::Call { func, args, .. } = inst { + if *func == k_exit_func_id { + if let Some(a0) = args.first().copied() { + args0.insert(a0); + } + } + } + } + } + } + assert_eq!( + args0.len(), + 1, + "loop-only bridged MIR must have a single consistent k_exit arg[0]" + ); + } +} diff --git a/src/mir/control_tree/normalized_shadow/mod.rs b/src/mir/control_tree/normalized_shadow/mod.rs index 1e40c430..ff91c657 100644 --- a/src/mir/control_tree/normalized_shadow/mod.rs +++ b/src/mir/control_tree/normalized_shadow/mod.rs @@ -40,8 +40,10 @@ pub mod legacy; pub mod dev_pipeline; pub mod parity_contract; pub mod available_inputs_collector; // Phase 126: available_inputs SSOT +pub mod exit_reconnector; // Phase 131 P1.5: Direct variable_map reconnection (Option B) pub use builder::StepTreeNormalizedShadowLowererBox; pub use contracts::{CapabilityCheckResult, UnsupportedCapability}; pub use parity_contract::{MismatchKind, ShadowParityResult}; pub use env_layout::EnvLayout; +pub use exit_reconnector::ExitReconnectorBox; // Phase 131 P1.5 diff --git a/src/mir/join_ir/lowering/carrier_info.rs b/src/mir/join_ir/lowering/carrier_info.rs index 9240d0b8..e98c48e9 100644 --- a/src/mir/join_ir/lowering/carrier_info.rs +++ b/src/mir/join_ir/lowering/carrier_info.rs @@ -91,6 +91,48 @@ pub enum CarrierInit { LoopLocalZero, } +/// Phase 131 P1.5: Exit reconnection mode for JoinInlineBoundary +/// +/// Controls whether exit values are reconnected via PHI generation or direct assignment. +/// This separates Normalized shadow (DirectValue) from existing loop patterns (Phi). +/// +/// # Design Principle (SSOT) +/// +/// - **DirectValue**: Normalized loops prohibit PHI generation. Exit values are directly +/// wired to variable_map using remapped_exit_values from MergeResult. +/// - **Phi**: Existing loop patterns use PHI generation for exit value merging. +/// +/// # Example +/// +/// ```ignore +/// // Normalized shadow: loop(true) { x = 1; break } → DirectValue +/// JoinInlineBoundary { exit_reconnect_mode: ExitReconnectMode::DirectValue, .. } +/// +/// // Traditional loop: loop(i < 3) { sum = sum + i } → Phi +/// JoinInlineBoundary { exit_reconnect_mode: ExitReconnectMode::Phi, .. } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExitReconnectMode { + /// Existing loop patterns: PHI generation for exit value merging + /// + /// Used by Pattern 1-4 loops with multiple exit paths. + /// Exit values are collected into exit PHIs. + Phi, + + /// Normalized shadow: Direct variable_map update, no PHI generation + /// + /// Used by loop(true) { *; break } pattern. + /// Exit values are directly wired using MergeResult.remapped_exit_values. + DirectValue, +} + +impl Default for ExitReconnectMode { + /// Default to Phi mode for backward compatibility + fn default() -> Self { + Self::Phi + } +} + // Phase 229: ConditionAlias removed - redundant with promoted_loopbodylocals // The naming convention (old_name → "is_" or "is__match") // is sufficient to resolve promoted variables dynamically. diff --git a/src/mir/join_ir/lowering/inline_boundary.rs b/src/mir/join_ir/lowering/inline_boundary.rs index a10b398b..600d6a26 100644 --- a/src/mir/join_ir/lowering/inline_boundary.rs +++ b/src/mir/join_ir/lowering/inline_boundary.rs @@ -43,7 +43,7 @@ //! ... //! ``` -use super::carrier_info::CarrierRole; +use super::carrier_info::{CarrierRole, ExitReconnectMode}; use crate::mir::ValueId; /// Explicit binding between JoinIR exit value and host variable @@ -257,6 +257,15 @@ pub struct JoinInlineBoundary { /// - `Some(CarrierInfo)`: Full carrier metadata available /// - `None`: Legacy path (derive carriers from exit_bindings) pub carrier_info: Option, + + /// Phase 131 P1.5: Exit reconnection mode + /// + /// Controls whether exit values are reconnected via PHI generation (Phi) + /// or direct variable_map update (DirectValue). + /// + /// - `Phi` (default): Existing loop patterns use exit PHI generation + /// - `DirectValue`: Normalized shadow uses direct value wiring + pub exit_reconnect_mode: ExitReconnectMode, } impl JoinInlineBoundary { @@ -284,6 +293,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14: Default to carrier-only pattern loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228: Default to None + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } @@ -327,6 +337,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14 loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228 + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } @@ -387,6 +398,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14 loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228 + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } @@ -433,6 +445,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14 loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228 + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } @@ -483,6 +496,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14 loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228 + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } @@ -540,6 +554,7 @@ impl JoinInlineBoundary { expr_result: None, // Phase 33-14 loop_var_name: None, // Phase 33-16 carrier_info: None, // Phase 228 + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi } } } diff --git a/src/mir/join_ir/lowering/inline_boundary_builder.rs b/src/mir/join_ir/lowering/inline_boundary_builder.rs index 1ce5f800..080efef3 100644 --- a/src/mir/join_ir/lowering/inline_boundary_builder.rs +++ b/src/mir/join_ir/lowering/inline_boundary_builder.rs @@ -82,6 +82,7 @@ pub struct JoinInlineBoundaryBuilder { impl JoinInlineBoundaryBuilder { /// Create a new builder with default empty boundary pub fn new() -> Self { + use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode; Self { boundary: JoinInlineBoundary { join_inputs: vec![], @@ -96,6 +97,7 @@ impl JoinInlineBoundaryBuilder { expr_result: None, loop_var_name: None, carrier_info: None, // Phase 228: Initialize as None + exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5 }, } } diff --git a/src/mir/join_ir_vm_bridge/joinir_block_converter.rs b/src/mir/join_ir_vm_bridge/joinir_block_converter.rs index 678e77eb..3a697d00 100644 --- a/src/mir/join_ir_vm_bridge/joinir_block_converter.rs +++ b/src/mir/join_ir_vm_bridge/joinir_block_converter.rs @@ -21,10 +21,6 @@ pub struct JoinIrBlockConverter { current_block_id: BasicBlockId, current_instructions: Vec, next_block_id: u32, - // Phase 189: Stable function name → ValueId mapping for tail call detection - // Ensures the same function name always gets the same ValueId - func_name_to_value_id: std::collections::HashMap, - next_func_name_value_id: u32, } impl JoinIrBlockConverter { @@ -33,8 +29,6 @@ impl JoinIrBlockConverter { current_block_id: BasicBlockId(0), // entry block current_instructions: Vec::new(), next_block_id: 1, // start from 1 (0 is entry) - func_name_to_value_id: std::collections::HashMap::new(), - next_func_name_value_id: 99990, // Use 99990+ range for function names } } @@ -313,17 +307,19 @@ impl JoinIrBlockConverter { let func_name = join_func_name(*func); - // Phase 189: Use stable function name → ValueId mapping - // This ensures the same function name always gets the same ValueId, - // which is critical for tail call detection in merge_joinir_mir_blocks - let func_name_id = *self - .func_name_to_value_id - .entry(func_name.clone()) - .or_insert_with(|| { - let id = ValueId(self.next_func_name_value_id); - self.next_func_name_value_id += 1; - id - }); + // Phase 131 P2: Stable function name ValueId (module-global SSOT) + // + // The merge pipeline relies on `Const(String("join_func_N"))` to detect tail calls. + // The ValueId used for that const MUST be stable across *all* functions in the module. + // + // IMPORTANT: avoid collisions with `call_result_id = ValueId(99991)`. + const FUNC_NAME_ID_BASE: u32 = 90000; + let func_name_id = ValueId(FUNC_NAME_ID_BASE + func.0); + if func_name_id == ValueId(99991) { + return Err(JoinIrVmBridgeError::new( + "[joinir_block] func_name_id collided with call_result_id (99991)".to_string(), + )); + } self.current_instructions.push(MirInstruction::Const { dst: func_name_id, @@ -352,6 +348,17 @@ impl JoinIrBlockConverter { effects: EffectMask::PURE, }); + // Phase 131 P2: Preserve tail-call args as jump_args metadata (SSOT for exit wiring) + // + // Merge/ExitArgsCollector uses BasicBlock.jump_args to recover carrier/env values. + // Without this, tail-call blocks look like "no args", forcing fallbacks that can + // produce undefined ValueIds in DirectValue mode. + if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) { + if block.jump_args.is_none() { + block.jump_args = Some(args.to_vec()); + } + } + let terminator = MirInstruction::Return { value: Some(call_result_id), }; diff --git a/tools/build_llvm.sh b/tools/build_llvm.sh index a6f16cab..50c56b90 100644 --- a/tools/build_llvm.sh +++ b/tools/build_llvm.sh @@ -46,6 +46,15 @@ fi # Use the cargo target dir when set (helps LLVM EXE smokes that build under /tmp). CARGO_TARGET_DIR_EFFECTIVE="${CARGO_TARGET_DIR:-$PWD/target}" +# Rust builds (especially rmeta/rlib finalization) may fail with EXDEV when the temp dir +# is not compatible with the output directory. Prefer a temp dir under the final output +# folder so rustc can atomically persist artifacts without cross-device rename issues. +# +# NOTE: release/deps may not exist yet on first build, so create it eagerly. +TMPDIR_EFFECTIVE="${TMPDIR:-$CARGO_TARGET_DIR_EFFECTIVE/release/deps}" +mkdir -p "$TMPDIR_EFFECTIVE" +export TMPDIR="$TMPDIR_EFFECTIVE" + BIN_DEFAULT="$CARGO_TARGET_DIR_EFFECTIVE/release/hakorune" BIN="${NYASH_BIN:-$BIN_DEFAULT}" @@ -58,10 +67,10 @@ if [[ "$LLVM_FEATURE" == "llvm-inkwell-legacy" ]]; then # Legacy inkwell需要LLVM_SYS_180_PREFIX _LLVMPREFIX=$(llvm-config-18 --prefix) LLVM_SYS_181_PREFIX="${_LLVMPREFIX}" LLVM_SYS_180_PREFIX="${_LLVMPREFIX}" \ - CARGO_INCREMENTAL=1 cargo build --release -j 24 -p nyash-rust --features "$LLVM_FEATURE" >/dev/null + CARGO_INCREMENTAL=0 cargo build --release -j 24 -p nyash-rust --features "$LLVM_FEATURE" >/dev/null else # llvm-harness(デフォルト)はLLVM_SYS_180_PREFIX不要 - CARGO_INCREMENTAL=1 cargo build --release -j 24 -p nyash-rust --features "$LLVM_FEATURE" >/dev/null + CARGO_INCREMENTAL=0 cargo build --release -j 24 -p nyash-rust --features "$LLVM_FEATURE" >/dev/null fi if [[ ! -x "$BIN" ]]; then diff --git a/tools/smokes/v2/lib/llvm_exe_runner.sh b/tools/smokes/v2/lib/llvm_exe_runner.sh index aa9f00df..bae66359 100644 --- a/tools/smokes/v2/lib/llvm_exe_runner.sh +++ b/tools/smokes/v2/lib/llvm_exe_runner.sh @@ -191,3 +191,62 @@ llvm_exe_build_and_run_numeric_smoke() { printf "%s\n" "$EXPECTED" return 1 } + +llvm_exe_build_and_run_expect_exit_code() { + # Required globals: + # - INPUT_HAKO + # - OUTPUT_EXE + # - EXPECTED_EXIT_CODE + # + # Optional: + # - LLVM_BUILD_LOG + # - RUN_TIMEOUT_SECS + + if [ -z "${INPUT_HAKO:-}" ] || [ -z "${OUTPUT_EXE:-}" ] || [ -z "${EXPECTED_EXIT_CODE:-}" ]; then + echo "[FAIL] llvm_exe_build_and_run_expect_exit_code: missing INPUT_HAKO/OUTPUT_EXE/EXPECTED_EXIT_CODE" + return 1 + fi + + mkdir -p "$(dirname "$OUTPUT_EXE")" + + echo "[INFO] Building: $INPUT_HAKO → $OUTPUT_EXE" + + local cargo_target_dir + cargo_target_dir="$(llvm_exe_cargo_target_dir)" + + # Ensure we use the compiler binary built in that target dir. + local nyash_bin="$cargo_target_dir/release/hakorune" + local obj_out="$cargo_target_dir/aot_objects/$(basename "$INPUT_HAKO").o" + mkdir -p "$(dirname "$obj_out")" + + local build_log="${LLVM_BUILD_LOG:-/tmp/llvm_exe_build.log}" + if ! env CARGO_TARGET_DIR="$cargo_target_dir" NYASH_BIN="$nyash_bin" NYASH_LLVM_OBJ_OUT="$obj_out" NYASH_DISABLE_PLUGINS=0 \ + "$NYASH_ROOT/tools/build_llvm.sh" "$INPUT_HAKO" -o "$OUTPUT_EXE" 2>&1 | tee "$build_log"; then + echo "[FAIL] build_llvm.sh failed" + tail -n 80 "$build_log" + return 1 + fi + + if [ ! -x "$OUTPUT_EXE" ]; then + echo "[FAIL] Executable not created or not executable: $OUTPUT_EXE" + ls -la "$OUTPUT_EXE" 2>/dev/null || echo "File does not exist" + return 1 + fi + + echo "[INFO] Executing: $OUTPUT_EXE" + + set +e + local output + output=$(timeout "${RUN_TIMEOUT_SECS:-10}" env NYASH_DISABLE_PLUGINS=0 "$OUTPUT_EXE" 2>&1) + local exit_code=$? + set -e + + if [ "$exit_code" -ne "$EXPECTED_EXIT_CODE" ]; then + echo "[FAIL] Execution exit code mismatch: got $exit_code, expected $EXPECTED_EXIT_CODE" + echo "[INFO] Raw output (tail):" + echo "$output" | tail -n 80 + return 1 + fi + + return 0 +} diff --git a/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_llvm_exe.sh index 25821713..965dd67d 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_llvm_exe.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_llvm_exe.sh @@ -9,6 +9,11 @@ require_env || exit 2 llvm_exe_preflight_or_skip || exit 0 +# Phase 131 is a dev-only Normalized shadow loop case. +# LLVM EXE emission must run with JoinIR dev/strict enabled, otherwise it will freeze. +export NYASH_JOINIR_DEV=1 +export HAKO_JOINIR_STRICT=1 + # Phase 131: minimal plugin set (StringBox, ConsoleBox, IntegerBox only) STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" CONSOLEBOX_SO="$NYASH_ROOT/plugins/nyash-console-plugin/libnyash_console_plugin.so" @@ -25,11 +30,10 @@ llvm_exe_ensure_plugins_or_fail || exit 1 INPUT_HAKO="$NYASH_ROOT/apps/tests/phase131_loop_true_break_once_min.hako" OUTPUT_EXE="$NYASH_ROOT/tmp/phase131_loop_true_break_once_llvm_exe" -EXPECTED=$'1' -EXPECTED_LINES=1 +EXPECTED_EXIT_CODE=1 LLVM_BUILD_LOG="/tmp/phase131_loop_true_break_once_build.log" -if llvm_exe_build_and_run_numeric_smoke; then - test_pass "phase131_loop_true_break_once_llvm_exe: output matches expected (1)" +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase131_loop_true_break_once_llvm_exe: exit code matches expected (1)" else exit 1 fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh index f73aff3a..c52f0f98 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh @@ -6,7 +6,6 @@ # - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 source "$(dirname "$0")/../../../lib/test_runner.sh" -source "$(dirname "$0")/../../../lib/output_validator.sh" export SMOKES_USE_PYVM=0 require_env || exit 2 @@ -34,15 +33,8 @@ if [ "$EXIT_CODE" -eq 124 ]; then FAIL_COUNT=$((FAIL_COUNT + 1)) elif [ "$EXIT_CODE" -eq 1 ]; then # Phase 131: expected output is exit code 1 (return value) - EXPECTED=$'1' - if validate_numeric_output 1 "$EXPECTED" "$OUTPUT"; then - echo "[PASS] Output verified: 1 (exit code: $EXIT_CODE)" - PASS_COUNT=$((PASS_COUNT + 1)) - else - echo "[INFO] output (tail):" - echo "$OUTPUT" | tail -n 50 || true - FAIL_COUNT=$((FAIL_COUNT + 1)) - fi + echo "[PASS] exit code verified: 1" + PASS_COUNT=$((PASS_COUNT + 1)) else echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 1)" echo "[INFO] output (tail):"