feat(control_tree): Phase 131 P1.5-P2 DirectValue exit reconnection

Implement DirectValue mode for Normalized shadow exit handling:

**P1.5 Changes**:
- Add ExitReconnectMode::DirectValue (skip exit PHI generation)
- Carry remapped_exit_values through merge result
- Update host variable_map directly with exit values
- Fix loop(true) { x = 1; break }; return x to return 1 correctly

**P2 Changes**:
- Normalize k_exit continuation entry/exit edges
- Rewrite TailCall(k_exit) → Jump(exit_block) for proper merge
- Add verify_all_terminator_targets_exist contract check
- Extend ExitLineReconnector to handle DirectValue mode

**Infrastructure**:
- tools/build_llvm.sh: Force TMPDIR under target/ (EXDEV mitigation)
- llvm_exe_runner.sh: Add exit_code verification support
- Phase 131 smokes: Update for dev-only + exit code validation

**Contracts**:
- PHI-free: Normalized path uses continuations only
- Exit values reconnect via remapped ValueIds
- Existing patterns unaffected (既定挙動不変)

Related: Phase 131 loop(true) break-once Normalized support

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-18 17:48:05 +09:00
parent bfac188732
commit 02c4c313e5
19 changed files with 973 additions and 93 deletions

View File

@ -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 <id> 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 '<name>' not found in carrier_phis" failures later in

View File

@ -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<String, ValueId>,
remapped_exit_values: &BTreeMap<String, ValueId>, // 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);

View File

@ -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<String, ValueId>,
remapped_exit_values: &BTreeMap<String, ValueId>, // 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

View File

@ -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<ValueId>)> = 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<Vec<ValueId>> = 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<ValueId> = 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<ValueId> = 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<String, ValueId> = 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
})
}

View File

@ -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<String, Vec<(BasicBlockId, ValueId)>>,
/// 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<String, ValueId>,
}
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
}
}

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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<Option<ValueId>, 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<Option<ValueId>, 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<LoopExitBinding> = 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::<Vec<_>>()
)
});
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
}
}
}
}