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 b57650e7..3e250bda 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs @@ -2,6 +2,8 @@ use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; use std::collections::BTreeMap; +use super::merge_result::MergeContracts; + #[cfg(debug_assertions)] use super::LoopHeaderPhiInfo; #[cfg(debug_assertions)] @@ -17,9 +19,11 @@ use std::collections::HashMap; /// 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 +/// +/// Phase 131 P1 Task 1: Now accepts MergeContracts instead of raw slice for SSOT visibility. pub(super) fn verify_all_terminator_targets_exist( func: &MirFunction, - allowed_missing_targets: &[BasicBlockId], + contracts: &MergeContracts, ) -> Result<(), String> { use crate::mir::join_ir::lowering::error_tags; @@ -28,7 +32,9 @@ pub(super) fn verify_all_terminator_targets_exist( match term { MirInstruction::Jump { target } => { - if !func.blocks.contains_key(target) && !allowed_missing_targets.contains(target) { + if !func.blocks.contains_key(target) + && !contracts.allowed_missing_jump_targets.contains(target) + { return Err(error_tags::freeze_with_hint( "joinir/merge/contract/missing_jump_target", &format!( @@ -43,7 +49,7 @@ pub(super) fn verify_all_terminator_targets_exist( then_bb, else_bb, .. } => { if !func.blocks.contains_key(then_bb) - && !allowed_missing_targets.contains(then_bb) + && !contracts.allowed_missing_jump_targets.contains(then_bb) { return Err(error_tags::freeze_with_hint( "joinir/merge/contract/missing_branch_target", @@ -55,7 +61,7 @@ pub(super) fn verify_all_terminator_targets_exist( )); } if !func.blocks.contains_key(else_bb) - && !allowed_missing_targets.contains(else_bb) + && !contracts.allowed_missing_jump_targets.contains(else_bb) { return Err(error_tags::freeze_with_hint( "joinir/merge/contract/missing_branch_target", @@ -489,3 +495,119 @@ pub(super) fn verify_header_phi_dsts_not_redefined( } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::{ + BasicBlock, BasicBlockId, FunctionSignature, MirFunction, MirInstruction, MirType, ValueId, + }; + + /// Helper: Create a minimal test function with given blocks + fn create_test_function(blocks: Vec<(BasicBlockId, Option)>) -> MirFunction { + use crate::mir::EffectMask; + + let entry_block = blocks.first().map(|(id, _)| *id).unwrap_or(BasicBlockId(0)); + + let mut func = MirFunction::new( + FunctionSignature { + name: "test_func".to_string(), + params: vec![], + return_type: MirType::Void, + effects: EffectMask::default(), + }, + entry_block, + ); + + // Remove the entry block that was auto-created + func.blocks.clear(); + + for (block_id, terminator) in blocks { + let mut block = BasicBlock::new(block_id); + block.terminator = terminator; + func.add_block(block); + } + + func + } + + #[test] + fn test_verify_all_terminator_targets_exist_all_present() { + // case 1: すべてのターゲットが存在 → OK + let bb0 = BasicBlockId(0); + let bb1 = BasicBlockId(1); + let bb2 = BasicBlockId(2); + + let func = create_test_function(vec![ + (bb0, Some(MirInstruction::Jump { target: bb1 })), + ( + bb1, + Some(MirInstruction::Branch { + condition: ValueId(0), + then_bb: bb2, + else_bb: bb2, + }), + ), + (bb2, Some(MirInstruction::Return { value: None })), + ]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!(result.is_ok(), "All targets exist, should pass"); + } + + #[test] + fn test_verify_all_terminator_targets_exist_missing_disallowed() { + // case 2: 許可ターゲット以外が missing → FAIL + let bb0 = BasicBlockId(0); + let bb99 = BasicBlockId(99); // Missing block + + let func = create_test_function(vec![(bb0, Some(MirInstruction::Jump { target: bb99 }))]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!(result.is_err(), "Missing disallowed target should fail"); + assert!( + result.unwrap_err().contains("Jump target"), + "Error should mention Jump target" + ); + } + + #[test] + fn test_verify_all_terminator_targets_exist_missing_allowed() { + // case 3: 許可ターゲットが missing → OK(許可) + let bb0 = BasicBlockId(0); + let bb_exit = BasicBlockId(100); // Missing but allowed + + let func = create_test_function(vec![(bb0, Some(MirInstruction::Jump { target: bb_exit }))]); + + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![bb_exit], + }; + + let result = verify_all_terminator_targets_exist(&func, &contracts); + assert!( + result.is_ok(), + "Missing allowed target should pass: {:?}", + result + ); + } + + #[test] + fn test_merge_contracts_creation() { + // MergeContracts の生成と値確認 + let exit_block = BasicBlockId(42); + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![exit_block], + }; + + assert_eq!(contracts.allowed_missing_jump_targets.len(), 1); + assert_eq!(contracts.allowed_missing_jump_targets[0], exit_block); + } +} 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 3366f0a1..84a164db 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -6,11 +6,13 @@ //! Phase 4 Extraction: Separated from merge_joinir_mir_blocks (lines 260-546) //! Phase 33-17: Further modularization - extracted TailCallClassifier and MergeResult //! Phase 179-A Step 3: Named constants for magic values +//! Phase 131 Task 2: Extracted TailCallLoweringPolicyBox for k_exit normalization use super::exit_args_collector::ExitArgsCollectorBox; use super::loop_header_phi_info::LoopHeaderPhiInfo; use super::merge_result::MergeResult; use super::tail_call_classifier::{classify_tail_call, TailCallKind}; +use super::tail_call_lowering_policy::{LoweringDecision, TailCallLoweringPolicyBox}; use super::super::trace; use crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper; use crate::mir::join_ir::lowering::error_tags; @@ -19,10 +21,6 @@ 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 -fn k_exit_function_name() -> String { - join_func_name(crate::mir::join_ir::JoinFuncId::new(2)) -} - /// Phase 4: Merge ALL functions and rewrite instructions /// /// Returns: @@ -63,6 +61,9 @@ pub(super) fn merge_and_rewrite( }; } + // Phase 131 Task 2: Create tail call lowering policy for k_exit normalization + let tail_call_policy = TailCallLoweringPolicyBox::new(); + // Phase 177-3: exit_block_id is now passed in from block_allocator log!( verbose, @@ -108,13 +109,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(); + // Phase 131 Task 2: k_exit detection now handled by TailCallLoweringPolicyBox + let k_exit_func_name = join_func_name(crate::mir::join_ir::JoinFuncId::new(2)); for (func_name, func) in functions_merge { // 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 + // Phase 131 Task 2: k_exit function name still used for continuation function detection let is_continuation_func = func_name == &k_exit_func_name; if debug { @@ -209,10 +211,11 @@ 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. + // Phase 131 Task 2: Use TailCallLoweringPolicyBox to detect k_exit tail calls + // 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; + let mut k_exit_lowering_decision: Option = None; // Phase 177-3: Check if this block is the loop header with PHI nodes let is_loop_header_with_phi = @@ -297,20 +300,23 @@ pub(super) fn merge_and_rewrite( // Phase 189: Detect tail calls and save parameters if let MirInstruction::Call { func, args, .. } = inst { 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). + // Phase 131 Task 2: Use TailCallLoweringPolicyBox to classify tail calls + // k_exit calls are "exit edges" (not normal tail calls). // 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); + let remapped_args: Vec = args + .iter() + .map(|&v| remapper.get_value(v).unwrap_or(v)) + .collect(); + + if let Some(decision) = tail_call_policy.classify_tail_call(callee_name, &remapped_args) { + // This is a k_exit tail call - policy says normalize to exit jump + k_exit_lowering_decision = Some(decision); 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={:?}", + "[cf_loop/joinir] Phase 131 Task 2: Detected k_exit tail call '{}' (args={:?}), will normalize to Jump(exit_block_id={:?})", callee_name, args, exit_block_id @@ -318,12 +324,9 @@ pub(super) fn merge_and_rewrite( } continue; // Skip the Call instruction itself } + // Not a k_exit call - check if it's a normal tail call (intra-module jump) 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() - .map(|&v| remapper.get_value(v).unwrap_or(v)) - .collect(); tail_call_target = Some((target_block, remapped_args)); found_tail_call = true; @@ -795,48 +798,47 @@ 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 { + // Phase 131 Task 2: If this block tail-calls k_exit, normalize to exit jump + // Use TailCallLoweringPolicyBox to generate the correct terminator + if let Some(decision) = k_exit_lowering_decision { + match decision { + LoweringDecision::NormalizeToExitJump { args } => { + // Collect exit values from k_exit arguments + 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/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", + "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", )); } + + // Generate exit jump using policy box + let exit_jump = tail_call_policy.rewrite_to_exit_jump(exit_block_id); + new_block.terminator = Some(exit_jump.clone()); + + // Strict mode: verify the generated terminator matches contract + if strict_exit { + tail_call_policy.verify_exit_jump(&exit_jump, exit_block_id)?; + } + } + LoweringDecision::NormalTailCall => { + // Should never happen (policy only returns NormalizeToExitJump for k_exit) + unreachable!("TailCallLoweringPolicyBox returned NormalTailCall for k_exit"); } } - new_block.terminator = Some(MirInstruction::Jump { - target: exit_block_id, - }); } // Phase 189 FIX: Ensure instruction_spans matches instructions length 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 ed397690..b18b1dea 100644 --- a/src/mir/builder/control_flow/joinir/merge/merge_result.rs +++ b/src/mir/builder/control_flow/joinir/merge/merge_result.rs @@ -7,6 +7,19 @@ use crate::mir::{BasicBlockId, ValueId}; use std::collections::BTreeMap; +/// Phase 131 P1 Task 1: Contract requirements for JoinIR merge operations +/// +/// This is the SSOT for merge contracts and invariants that must be verified. +/// Consolidating these in a single type makes the contract requirements visible +/// and reduces the need to pass individual slices/vectors around. +#[derive(Debug, Clone)] +pub struct MergeContracts { + /// Block IDs that are allowed to be missing from the function when verifying + /// terminator targets. This typically includes the exit_block_id which is + /// allocated but may not be inserted yet at verification time. + pub allowed_missing_jump_targets: Vec, +} + /// Phase 33-13: Return type for merge_and_rewrite /// /// Contains all information needed for exit PHI construction. diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index 5d8ade1d..87119666 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -25,17 +25,66 @@ 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; // 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 @@ -79,7 +128,9 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( boundary: Option<&JoinInlineBoundary>, debug: bool, ) -> Result, String> { - let verbose = debug || crate::config::env::joinir_dev_enabled(); + // 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( @@ -729,16 +780,16 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( } // 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() { + // 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). - contract_checks::verify_all_terminator_targets_exist( - current_func, - &[merge_result.exit_block_id], - )?; + let contracts = MergeContracts { + allowed_missing_jump_targets: vec![merge_result.exit_block_id], + }; + contract_checks::verify_all_terminator_targets_exist(current_func, &contracts)?; } } @@ -767,7 +818,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( boundary.map(|b| b.exit_reconnect_mode), is_direct_value_mode ), - debug || crate::config::env::joinir_dev_enabled(), + debug || config.dev_log, ); let (exit_phi_result_id, exit_carrier_phis) = if is_direct_value_mode { @@ -801,7 +852,8 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( } // Phase 118 P1: Dev-only carrier-phi SSOT logs (exit_bindings vs carrier_inputs vs exit_carrier_phis) - if crate::config::env::joinir_dev_enabled() { + // 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 diff --git a/src/mir/builder/control_flow/joinir/merge/tail_call_lowering_policy.rs b/src/mir/builder/control_flow/joinir/merge/tail_call_lowering_policy.rs new file mode 100644 index 00000000..154c78d4 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/tail_call_lowering_policy.rs @@ -0,0 +1,212 @@ +//! Tail Call Lowering Policy Box +//! +//! Phase 131 Task 2: Extracted from instruction_rewriter.rs +//! +//! This box encapsulates the policy for lowering tail calls to k_exit (continuation functions). +//! In JoinIR, k_exit represents exit points from control flow fragments. These must be +//! normalized to Jump instructions targeting the shared exit block. +//! +//! # Responsibility +//! +//! - Detect tail calls to k_exit (continuation function) +//! - Convert them to Jump(exit_block_id) instructions +//! - This is "exit edge normalization" - ensuring continuation exits become proper jumps +//! +//! # Non-Responsibility (handled by caller) +//! +//! - Exit value collection (handled by ExitArgsCollectorBox) +//! - Remapping of ValueIds (handled by JoinIrIdRemapper) +//! - Instruction rewriting of non-k_exit instructions (handled by instruction_rewriter) + +use crate::mir::join_ir_vm_bridge::join_func_name; +use crate::mir::{BasicBlockId, MirInstruction, ValueId}; + +/// Policy box for tail call lowering (k_exit special case) +/// +/// # Phase 131 Task 2 Contract +/// +/// - **Input**: Call instruction, function name, args +/// - **Output**: `Option` - None if not k_exit, Some(decision) if k_exit +/// - **Invariant**: Stateless (no mutable state) +pub struct TailCallLoweringPolicyBox { + k_exit_func_name: String, +} + +/// Decision for how to lower a tail call +#[derive(Debug, Clone)] +pub enum LoweringDecision { + /// This is a k_exit tail call - normalize to exit jump + NormalizeToExitJump { + /// Arguments passed to k_exit (will be used for exit value collection) + args: Vec, + }, + /// Not a k_exit call - handle normally (convert to Jump to target block) + NormalTailCall, +} + +impl TailCallLoweringPolicyBox { + /// Create new policy box + pub fn new() -> Self { + Self { + k_exit_func_name: join_func_name(crate::mir::join_ir::JoinFuncId::new(2)), + } + } + + /// Classify a tail call instruction + /// + /// # Arguments + /// + /// - `callee_name`: Name of the called function + /// - `args`: Arguments passed to the call + /// + /// # Returns + /// + /// - `Some(LoweringDecision)` if this is a special tail call requiring policy decision + /// - `None` if this is not a tail call or not relevant to this policy + pub fn classify_tail_call( + &self, + callee_name: &str, + args: &[ValueId], + ) -> Option { + if callee_name == self.k_exit_func_name { + // This is a k_exit tail call - must be normalized to exit jump + Some(LoweringDecision::NormalizeToExitJump { + args: args.to_vec(), + }) + } else { + // Not a k_exit call - caller will handle as normal tail call (Jump to entry block) + None + } + } + + /// Generate exit jump instruction for k_exit tail call + /// + /// # Arguments + /// + /// - `exit_block_id`: The shared exit block ID (from block_allocator) + /// + /// # Returns + /// + /// - MirInstruction::Jump targeting the exit block + /// + /// # Contract + /// + /// - This is the ONLY instruction type that should be emitted for k_exit tail calls + /// - Caller must collect exit values separately (via ExitArgsCollectorBox) + pub fn rewrite_to_exit_jump(&self, exit_block_id: BasicBlockId) -> MirInstruction { + MirInstruction::Jump { + target: exit_block_id, + } + } + + /// Verify that the generated terminator matches the expected contract (strict mode only) + /// + /// # Arguments + /// + /// - `terminator`: The terminator instruction to verify + /// - `exit_block_id`: Expected exit block ID + /// + /// # Returns + /// + /// - `Ok(())` if verification passes + /// - `Err(msg)` if contract violation detected + /// + /// # Contract + /// + /// In strict mode, k_exit tail calls MUST become Jump(exit_block_id). + /// Any other terminator is a contract violation. + pub fn verify_exit_jump( + &self, + terminator: &MirInstruction, + exit_block_id: BasicBlockId, + ) -> Result<(), String> { + match terminator { + MirInstruction::Jump { target } if *target == exit_block_id => Ok(()), + MirInstruction::Jump { target } => Err(crate::mir::join_ir::lowering::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", + )), + other => Err(crate::mir::join_ir::lowering::error_tags::freeze_with_hint( + "phase131/k_exit/not_jump", + &format!( + "k_exit tail call resulted in unexpected terminator: {:?}", + other + ), + "k_exit must be lowered to Jump(exit_block_id)", + )), + } + } +} + +impl Default for TailCallLoweringPolicyBox { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_classify_k_exit_call() { + let policy = TailCallLoweringPolicyBox::new(); + let k_exit_name = join_func_name(crate::mir::join_ir::JoinFuncId::new(2)); + let args = vec![ValueId(1), ValueId(2)]; + + let decision = policy.classify_tail_call(&k_exit_name, &args); + assert!(matches!( + decision, + Some(LoweringDecision::NormalizeToExitJump { .. }) + )); + } + + #[test] + fn test_classify_normal_call() { + let policy = TailCallLoweringPolicyBox::new(); + let args = vec![ValueId(1)]; + + let decision = policy.classify_tail_call("some_other_function", &args); + assert!(decision.is_none()); + } + + #[test] + fn test_rewrite_to_exit_jump() { + let policy = TailCallLoweringPolicyBox::new(); + let exit_block = BasicBlockId(42); + + let jump = policy.rewrite_to_exit_jump(exit_block); + assert!(matches!( + jump, + MirInstruction::Jump { target } if target == exit_block + )); + } + + #[test] + fn test_verify_exit_jump_success() { + let policy = TailCallLoweringPolicyBox::new(); + let exit_block = BasicBlockId(42); + let jump = MirInstruction::Jump { + target: exit_block, + }; + + let result = policy.verify_exit_jump(&jump, exit_block); + assert!(result.is_ok()); + } + + #[test] + fn test_verify_exit_jump_wrong_target() { + let policy = TailCallLoweringPolicyBox::new(); + let exit_block = BasicBlockId(42); + let wrong_jump = MirInstruction::Jump { + target: BasicBlockId(99), + }; + + let result = policy.verify_exit_jump(&wrong_jump, exit_block); + assert!(result.is_err()); + } +}