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 84a164db..d4cce578 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -18,8 +18,103 @@ 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 crate::mir::types::ConstValue; +use crate::mir::{BasicBlock, BasicBlockId, MirFunction, MirInstruction, MirModule, MirType, ValueId}; use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism +use std::collections::BTreeSet; + +/// Phase 132-R0 Task 3: Structural check for skippable continuation functions +/// +/// A continuation function is skippable if it is a pure exit stub: +/// - 1 block only +/// - No instructions +/// - Return terminator only +/// +/// This is a structural check (no by-name/by-id inference). +pub(super) fn is_skippable_continuation(func: &MirFunction) -> bool { + if func.blocks.len() != 1 { + return false; + } + let Some(block) = func.blocks.get(&func.entry_block) else { + return false; + }; + if !block.instructions.is_empty() { + return false; + } + matches!(block.terminator, Some(MirInstruction::Return { .. })) +} + +fn propagate_value_type_for_inst( + builder: &mut crate::mir::builder::MirBuilder, + source_func: &MirFunction, + old_inst: &MirInstruction, + new_inst: &MirInstruction, +) { + let Some(_) = builder.scope_ctx.current_function else { + return; + }; + + let (old_dst, new_dst) = match (old_inst, new_inst) { + (MirInstruction::Const { dst: o, .. }, MirInstruction::Const { dst: n, .. }) => { + (o, n) + } + (MirInstruction::BinOp { dst: o, .. }, MirInstruction::BinOp { dst: n, .. }) => (o, n), + (MirInstruction::UnaryOp { dst: o, .. }, MirInstruction::UnaryOp { dst: n, .. }) => (o, n), + (MirInstruction::Compare { dst: o, .. }, MirInstruction::Compare { dst: n, .. }) => (o, n), + (MirInstruction::Load { dst: o, .. }, MirInstruction::Load { dst: n, .. }) => (o, n), + (MirInstruction::Cast { dst: o, .. }, MirInstruction::Cast { dst: n, .. }) => (o, n), + (MirInstruction::TypeOp { dst: o, .. }, MirInstruction::TypeOp { dst: n, .. }) => (o, n), + (MirInstruction::ArrayGet { dst: o, .. }, MirInstruction::ArrayGet { dst: n, .. }) => (o, n), + (MirInstruction::Copy { dst: o, .. }, MirInstruction::Copy { dst: n, .. }) => (o, n), + (MirInstruction::NewBox { dst: o, .. }, MirInstruction::NewBox { dst: n, .. }) => (o, n), + (MirInstruction::TypeCheck { dst: o, .. }, MirInstruction::TypeCheck { dst: n, .. }) => (o, n), + (MirInstruction::Phi { dst: o, .. }, MirInstruction::Phi { dst: n, .. }) => (o, n), + _ => return, + }; + + // Prefer SSOT from the source MIR (JoinIR→MIR bridge may have hints). + if let Some(ty) = source_func.metadata.value_types.get(old_dst).cloned() { + builder.type_ctx.value_types.insert(*new_dst, ty); + return; + } + + // Fallback inference from the rewritten instruction itself. + // + // This is important for the LLVM harness: it relies on `dst_type` hints to keep `+` + // as numeric add instead of `concat_hh_mixed` when types are otherwise unknown. + match new_inst { + MirInstruction::Const { dst, value } if dst == new_dst => { + let ty = match value { + ConstValue::Integer(_) => Some(MirType::Integer), + ConstValue::Float(_) => Some(MirType::Float), + ConstValue::Bool(_) => Some(MirType::Bool), + ConstValue::String(_) => Some(MirType::String), + ConstValue::Void => Some(MirType::Void), + _ => None, + }; + if let Some(ty) = ty { + builder.type_ctx.value_types.insert(*dst, ty); + } + } + MirInstruction::Copy { dst, src } if dst == new_dst => { + if let Some(src_ty) = builder.type_ctx.value_types.get(src).cloned() { + builder.type_ctx.value_types.insert(*dst, src_ty); + } + } + MirInstruction::BinOp { dst, lhs, rhs, .. } if dst == new_dst => { + let lhs_ty = builder.type_ctx.value_types.get(lhs); + let rhs_ty = builder.type_ctx.value_types.get(rhs); + if matches!(lhs_ty, Some(MirType::Integer)) && matches!(rhs_ty, Some(MirType::Integer)) + { + builder.type_ctx.value_types.insert(*dst, MirType::Integer); + } + } + MirInstruction::Compare { dst, .. } if dst == new_dst => { + builder.type_ctx.value_types.insert(*dst, MirType::Bool); + } + _ => {} + } +} /// Phase 4: Merge ALL functions and rewrite instructions /// @@ -61,8 +156,31 @@ 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(); + let continuation_candidates: BTreeSet = boundary + .map(|b| { + b.continuation_func_ids + .iter() + .copied() + .map(join_func_name) + .collect() + }) + .unwrap_or_default(); + + let skippable_continuation_func_names: BTreeSet = mir_module + .functions + .iter() + .filter_map(|(func_name, func)| { + if continuation_candidates.contains(func_name) && is_skippable_continuation(func) { + Some(func_name.clone()) + } else { + None + } + }) + .collect(); + + // Phase 132 P1: Tail call lowering is driven by the continuation contract (SSOT) + // Only structurally-skippable continuation functions are lowered to exit jumps. + let tail_call_policy = TailCallLoweringPolicyBox::new(skippable_continuation_func_names.clone()); // Phase 177-3: exit_block_id is now passed in from block_allocator log!( @@ -109,36 +227,36 @@ 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()); - // 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 131 Task 2: k_exit function name still used for continuation function detection - let is_continuation_func = func_name == &k_exit_func_name; + let is_continuation_candidate = continuation_candidates.contains(func_name); + let is_skippable_continuation = skippable_continuation_func_names.contains(func_name); if debug { log!( true, - "[cf_loop/joinir] Merging function '{}' with {} blocks, entry={:?} (is_continuation={})", + "[cf_loop/joinir] Merging function '{}' with {} blocks, entry={:?} (continuation_candidate={}, skippable_continuation={})", func_name, func.blocks.len(), func.entry_block, - is_continuation_func + is_continuation_candidate, + is_skippable_continuation ); } - // Phase 33-15: Skip continuation function blocks entirely - // Continuation functions (k_exit) are just exit points; their parameters are - // passed via Jump args which become Return values in other functions. - // Processing continuation functions would add undefined ValueIds to PHI. - if is_continuation_func { + // Phase 132 P1: Skip only *structurally skippable* continuation functions. + // + // Continuation functions can be real code (e.g. k_exit tailcalls post_k). + // Merge must follow the explicit continuation contract and then decide + // skippability by structure only. + if is_skippable_continuation { if debug { log!( true, - "[cf_loop/joinir] Phase 33-15: Skipping continuation function '{}' blocks", + "[cf_loop/joinir] Phase 132 P1: Skipping skippable continuation function '{}' blocks", func_name ); } @@ -401,6 +519,7 @@ pub(super) fn merge_and_rewrite( } } + propagate_value_type_for_inst(builder, func, inst, &remapped_with_blocks); new_block.instructions.push(remapped_with_blocks); } diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index 87119666..1575db54 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -28,6 +28,9 @@ 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; diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index dee625c2..725be5c8 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -395,7 +395,7 @@ impl MirBuilder { "Pattern router found no match, trying legacy whitelist", ); - // Delegate to legacy binding path (routing_legacy_binding.rs) + // Phase 132-R0 Task 4: Delegate to legacy binding path (legacy/routing_legacy_binding.rs) self.cf_loop_joinir_legacy_binding(condition, body, func_name, debug) } @@ -495,6 +495,7 @@ impl MirBuilder { exit_bindings, ); boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; // Phase 131 P1.5: No PHI + boundary.continuation_func_ids = join_meta.continuation_funcs.clone(); // Merge with boundary - this will populate MergeResult.remapped_exit_values use crate::mir::builder::control_flow::joinir::merge; 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 3a697d00..7c5e056b 100644 --- a/src/mir/join_ir_vm_bridge/joinir_block_converter.rs +++ b/src/mir/join_ir_vm_bridge/joinir_block_converter.rs @@ -7,7 +7,8 @@ use crate::ast::Span; use crate::mir::join_ir::{JoinInst, MirLikeInst}; -use crate::mir::{BasicBlockId, EffectMask, MirFunction, MirInstruction, ValueId}; +use crate::mir::{BasicBlockId, EffectMask, MirFunction, MirInstruction, MirType, ValueId}; +use crate::mir::types::ConstValue; use super::{convert_mir_like_inst, join_func_name, JoinIrVmBridgeError}; @@ -71,6 +72,7 @@ impl JoinIrBlockConverter { mir_like )); let mir_inst = convert_mir_like_inst(mir_like)?; + Self::annotate_value_types_for_inst(mir_func, &mir_inst); self.current_instructions.push(mir_inst); } JoinInst::MethodCall { @@ -154,6 +156,48 @@ impl JoinIrBlockConverter { Ok(()) } + fn annotate_value_types_for_inst(mir_func: &mut MirFunction, inst: &MirInstruction) { + match inst { + MirInstruction::Const { dst, value } => { + let ty = match value { + ConstValue::Integer(_) => Some(MirType::Integer), + ConstValue::Float(_) => Some(MirType::Float), + ConstValue::Bool(_) => Some(MirType::Bool), + ConstValue::String(_) => Some(MirType::String), + ConstValue::Void => Some(MirType::Void), + _ => None, + }; + if let Some(ty) = ty { + mir_func.metadata.value_types.insert(*dst, ty); + } + } + MirInstruction::BinOp { dst, lhs, rhs, .. } => { + // Conservative typing: only mark integer when both operands are already known integers. + // + // This avoids forcing numeric semantics for `+` in cases where the operands are not + // known to be integers (e.g. string concatenation patterns). + let lhs_ty = mir_func.metadata.value_types.get(lhs); + let rhs_ty = mir_func.metadata.value_types.get(rhs); + if matches!(lhs_ty, Some(MirType::Integer)) && matches!(rhs_ty, Some(MirType::Integer)) + { + mir_func.metadata.value_types.insert(*dst, MirType::Integer); + } + } + MirInstruction::Compare { dst, .. } => { + mir_func.metadata.value_types.insert(*dst, MirType::Bool); + } + MirInstruction::TypeCheck { dst, .. } => { + mir_func.metadata.value_types.insert(*dst, MirType::Bool); + } + MirInstruction::Phi { dst, type_hint, .. } => { + if let Some(ty) = type_hint.clone() { + mir_func.metadata.value_types.insert(*dst, ty); + } + } + _ => {} + } + } + // === Instruction Handlers === fn handle_method_call(