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:
@ -1,5 +1,5 @@
|
|||||||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||||||
use crate::mir::ValueId;
|
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -7,10 +7,73 @@ use super::LoopHeaderPhiInfo;
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
|
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction};
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
use std::collections::HashMap;
|
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.
|
/// 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
|
/// This prevents latent "Carrier '<name>' not found in carrier_phis" failures later in
|
||||||
|
|||||||
@ -70,6 +70,7 @@ impl ExitLineOrchestrator {
|
|||||||
/// - builder: MirBuilder with variable_map to update
|
/// - builder: MirBuilder with variable_map to update
|
||||||
/// - boundary: JoinInlineBoundary with exit_bindings
|
/// - boundary: JoinInlineBoundary with exit_bindings
|
||||||
/// - carrier_phis: Map from carrier name to PHI dst ValueId (Phase 33-13)
|
/// - 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
|
/// - debug: Debug logging enabled
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
@ -77,11 +78,15 @@ impl ExitLineOrchestrator {
|
|||||||
///
|
///
|
||||||
/// # Process
|
/// # Process
|
||||||
/// 1. Validate exit_bindings (empty case)
|
/// 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(
|
pub fn execute(
|
||||||
builder: &mut crate::mir::builder::MirBuilder,
|
builder: &mut crate::mir::builder::MirBuilder,
|
||||||
boundary: &crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary,
|
boundary: &crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary,
|
||||||
carrier_phis: &BTreeMap<String, ValueId>,
|
carrier_phis: &BTreeMap<String, ValueId>,
|
||||||
|
remapped_exit_values: &BTreeMap<String, ValueId>, // Phase 131 P1.5
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
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
|
// Phase 33-13 + Phase 131 P1.5: Delegate to ExitLineReconnector with carrier_phis and remapped_exit_values
|
||||||
ExitLineReconnector::reconnect(builder, boundary, carrier_phis, debug)?;
|
ExitLineReconnector::reconnect(builder, boundary, carrier_phis, remapped_exit_values, debug)?;
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
trace.stderr_if("[joinir/exit-line] orchestrator complete", true);
|
trace.stderr_if("[joinir/exit-line] orchestrator complete", true);
|
||||||
|
|||||||
@ -67,16 +67,23 @@ impl ExitLineReconnector {
|
|||||||
/// Now, we use the carrier_phis map from exit_phi_builder, which contains
|
/// 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.
|
/// 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
|
/// # Algorithm
|
||||||
///
|
///
|
||||||
/// For each exit_binding:
|
/// For each exit_binding:
|
||||||
/// 1. Look up the PHI dst for this carrier in carrier_phis
|
/// 1. Look up the PHI dst (Phi mode) or remapped value (DirectValue mode)
|
||||||
/// 2. Update variable_ctx.variable_map[binding.carrier_name] with PHI dst
|
/// 2. Update variable_ctx.variable_map[binding.carrier_name] with the value
|
||||||
/// 3. Log each update (if debug enabled)
|
/// 3. Log each update (if debug enabled)
|
||||||
pub fn reconnect(
|
pub fn reconnect(
|
||||||
builder: &mut MirBuilder,
|
builder: &mut MirBuilder,
|
||||||
boundary: &JoinInlineBoundary,
|
boundary: &JoinInlineBoundary,
|
||||||
carrier_phis: &BTreeMap<String, ValueId>,
|
carrier_phis: &BTreeMap<String, ValueId>,
|
||||||
|
remapped_exit_values: &BTreeMap<String, ValueId>, // Phase 131 P1.5
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
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
|
// Process each exit binding
|
||||||
for binding in &boundary.exit_bindings {
|
for binding in &boundary.exit_bindings {
|
||||||
// Phase 228-8: Skip ConditionOnly carriers (no variable_ctx.variable_map update needed)
|
// 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 binding.role == CarrierRole::ConditionOnly {
|
||||||
if verbose {
|
if verbose {
|
||||||
trace.stderr_if(
|
trace.stderr_if(
|
||||||
@ -144,21 +164,27 @@ impl ExitLineReconnector {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 33-13: Look up the PHI dst for this carrier
|
// Phase 131 P1.5: Choose value source based on mode
|
||||||
let phi_dst = carrier_phis.get(&binding.carrier_name);
|
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 {
|
if verbose {
|
||||||
trace.stderr_if(
|
trace.stderr_if(
|
||||||
&format!(
|
&format!(
|
||||||
"[joinir/exit-line] carrier '{}' → phi_dst={:?}",
|
"[joinir/exit-line] carrier '{}' → final_value={:?} (mode={:?})",
|
||||||
binding.carrier_name, phi_dst
|
binding.carrier_name, final_value, boundary.exit_reconnect_mode
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update variable_ctx.variable_map with PHI dst
|
// Update variable_ctx.variable_map with final value
|
||||||
if let Some(&phi_value) = phi_dst {
|
if let Some(phi_value) = final_value {
|
||||||
if let Some(var_vid) = builder
|
if let Some(var_vid) = builder
|
||||||
.variable_ctx
|
.variable_ctx
|
||||||
.variable_map
|
.variable_map
|
||||||
|
|||||||
@ -13,13 +13,15 @@ use super::merge_result::MergeResult;
|
|||||||
use super::tail_call_classifier::{classify_tail_call, TailCallKind};
|
use super::tail_call_classifier::{classify_tail_call, TailCallKind};
|
||||||
use super::super::trace;
|
use super::super::trace;
|
||||||
use crate::mir::builder::joinir_id_remapper::JoinIrIdRemapper;
|
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::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::{BasicBlock, BasicBlockId, MirInstruction, MirModule, ValueId};
|
||||||
use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism
|
use std::collections::BTreeMap; // Phase 222.5-E: HashMap → BTreeMap for determinism
|
||||||
|
|
||||||
/// Phase 179-A: Exit continuation function name (MIR convention)
|
fn k_exit_function_name() -> String {
|
||||||
/// This is the standard name for k_exit continuations in JoinIR → MIR lowering
|
join_func_name(crate::mir::join_ir::JoinFuncId::new(2))
|
||||||
const K_EXIT_FUNC_NAME: &str = "join_func_2";
|
}
|
||||||
|
|
||||||
/// Phase 4: Merge ALL functions and rewrite instructions
|
/// 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());
|
functions_merge.sort_by_key(|(name, _)| name.as_str());
|
||||||
|
|
||||||
let entry_func_name = functions_merge.first().map(|(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 {
|
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
|
// Continuation functions receive values from Jump args, not as independent sources
|
||||||
// We should NOT collect their Return values for exit_phi_inputs
|
// We should NOT collect their Return values for exit_phi_inputs
|
||||||
// Phase 179-A: Use named constant for k_exit function name
|
// 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 {
|
if debug {
|
||||||
log!(
|
log!(
|
||||||
@ -206,6 +209,10 @@ pub(super) fn merge_and_rewrite(
|
|||||||
// Remap instructions (Phase 189: Convert inter-function Calls to control flow)
|
// Remap instructions (Phase 189: Convert inter-function Calls to control flow)
|
||||||
let mut found_tail_call = false;
|
let mut found_tail_call = false;
|
||||||
let mut tail_call_target: Option<(BasicBlockId, Vec<ValueId>)> = None;
|
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
|
// Phase 177-3: Check if this block is the loop header with PHI nodes
|
||||||
let is_loop_header_with_phi =
|
let is_loop_header_with_phi =
|
||||||
@ -289,8 +296,29 @@ pub(super) fn merge_and_rewrite(
|
|||||||
|
|
||||||
// Phase 189: Detect tail calls and save parameters
|
// Phase 189: Detect tail calls and save parameters
|
||||||
if let MirInstruction::Call { func, args, .. } = inst {
|
if let MirInstruction::Call { func, args, .. } = inst {
|
||||||
if let Some(func_name) = value_to_func_name.get(func) {
|
if let Some(callee_name) = value_to_func_name.get(func) {
|
||||||
if let Some(&target_block) = function_entry_map.get(func_name) {
|
// 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
|
// This is a tail call - save info and skip the Call instruction itself
|
||||||
let remapped_args: Vec<ValueId> = args
|
let remapped_args: Vec<ValueId> = args
|
||||||
.iter()
|
.iter()
|
||||||
@ -306,6 +334,7 @@ pub(super) fn merge_and_rewrite(
|
|||||||
func_name, args
|
func_name, args
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue; // Skip the Call instruction itself
|
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 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 {
|
for binding in &b.exit_bindings {
|
||||||
if binding.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::ConditionOnly {
|
if binding.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::ConditionOnly {
|
||||||
continue;
|
continue;
|
||||||
@ -719,6 +749,18 @@ pub(super) fn merge_and_rewrite(
|
|||||||
.entry(binding.carrier_name.clone())
|
.entry(binding.carrier_name.clone())
|
||||||
.or_insert_with(Vec::new)
|
.or_insert_with(Vec::new)
|
||||||
.push((new_block_id, phi_dst));
|
.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
|
// Phase 189 FIX: Ensure instruction_spans matches instructions length
|
||||||
// The original spans may not cover all instructions after remapping/adding
|
// The original spans may not cover all instructions after remapping/adding
|
||||||
// (PHI instructions, tail call parameter bindings, etc.)
|
// (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 {
|
Ok(MergeResult {
|
||||||
exit_block_id,
|
exit_block_id,
|
||||||
exit_phi_inputs,
|
exit_phi_inputs,
|
||||||
carrier_inputs,
|
carrier_inputs,
|
||||||
|
remapped_exit_values, // Phase 131 P1.5
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,16 @@ pub struct MergeResult {
|
|||||||
pub exit_phi_inputs: Vec<(BasicBlockId, ValueId)>,
|
pub exit_phi_inputs: Vec<(BasicBlockId, ValueId)>,
|
||||||
/// Map of carrier_name → Vec of (from_block, exit_value) for carrier PHI generation
|
/// Map of carrier_name → Vec of (from_block, exit_value) for carrier PHI generation
|
||||||
pub carrier_inputs: BTreeMap<String, Vec<(BasicBlockId, ValueId)>>,
|
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 {
|
impl MergeResult {
|
||||||
@ -27,6 +37,7 @@ impl MergeResult {
|
|||||||
exit_block_id,
|
exit_block_id,
|
||||||
exit_phi_inputs: Vec::new(),
|
exit_phi_inputs: Vec::new(),
|
||||||
carrier_inputs: BTreeMap::new(),
|
carrier_inputs: BTreeMap::new(),
|
||||||
|
remapped_exit_values: BTreeMap::new(), // Phase 131 P1.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ pub use loop_header_phi_info::LoopHeaderPhiInfo;
|
|||||||
use super::trace;
|
use super::trace;
|
||||||
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
|
||||||
use crate::mir::{MirModule, ValueId};
|
use crate::mir::{MirModule, ValueId};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
/// Phase 49-3.2: Merge JoinIR-generated MIR blocks into current_function
|
/// 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)?;
|
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 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 33-20: Carrier PHIs are now taken from header PHI info, not exit block
|
||||||
// Phase 246-EX: REVERT Phase 33-20 - Use EXIT PHI dsts, not header PHI dsts!
|
// Phase 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(
|
// Phase 131 P1.5: DirectValue mode completely skips PHI generation
|
||||||
builder,
|
trace.stderr_if(
|
||||||
merge_result.exit_block_id,
|
&format!(
|
||||||
&merge_result.exit_phi_inputs,
|
"[cf_loop/joinir] Phase 131 P1.5 DEBUG: boundary={:?}, mode={:?}",
|
||||||
&merge_result.carrier_inputs,
|
boundary.is_some(),
|
||||||
|
boundary.map(|b| b.exit_reconnect_mode)
|
||||||
|
),
|
||||||
debug,
|
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 118 P2: Contract check (Fail-Fast) - exit_bindings LoopState carriers must have exit PHIs.
|
||||||
|
// Phase 131 P1.5: Skip this check in DirectValue mode
|
||||||
if let Some(boundary) = boundary {
|
if 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)
|
// 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 197-B: Pass remapper to enable per-carrier exit value lookup
|
||||||
// Phase 33-10-Refactor-P3: Delegate to ExitLineOrchestrator
|
// Phase 33-10-Refactor-P3: Delegate to ExitLineOrchestrator
|
||||||
// Phase 246-EX: Now uses EXIT PHI dsts (reverted Phase 33-20)
|
// 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 {
|
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;
|
let exit_block_id = merge_result.exit_block_id;
|
||||||
|
|||||||
@ -383,6 +383,7 @@ mod tests {
|
|||||||
expr_result: None, // Phase 33-14: Add missing field
|
expr_result: None, // Phase 33-14: Add missing field
|
||||||
loop_var_name: None, // Phase 33-16: Add missing field
|
loop_var_name: None, // Phase 33-16: Add missing field
|
||||||
carrier_info: None, // Phase 228: 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
|
builder
|
||||||
|
|||||||
@ -137,6 +137,7 @@ mod tests {
|
|||||||
expr_result: None, // Phase 33-14: Add missing field
|
expr_result: None, // Phase 33-14: Add missing field
|
||||||
loop_var_name: None, // Phase 33-16: Add missing field
|
loop_var_name: None, // Phase 33-16: Add missing field
|
||||||
carrier_info: None, // Phase 228: 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)
|
apply_exit_bindings_to_boundary(&carrier_info, &exit_meta, &variable_map, &mut boundary)
|
||||||
|
|||||||
@ -251,8 +251,9 @@ impl MirBuilder {
|
|||||||
/// Phase 49-3: JoinIR Frontend integration implementation
|
/// Phase 49-3: JoinIR Frontend integration implementation
|
||||||
///
|
///
|
||||||
/// Routes loop compilation through either:
|
/// Routes loop compilation through either:
|
||||||
/// 1. Pattern-based router (Phase 194+) - preferred for new patterns
|
/// 1. Normalized shadow (Phase 131 P1) - dev-only for loop(true) break-once
|
||||||
/// 2. Legacy binding path (Phase 49-3) - for whitelisted functions only
|
/// 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(
|
pub(in crate::mir::builder) fn cf_loop_joinir_impl(
|
||||||
&mut self,
|
&mut self,
|
||||||
condition: &ASTNode,
|
condition: &ASTNode,
|
||||||
@ -260,6 +261,13 @@ impl MirBuilder {
|
|||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<Option<ValueId>, String> {
|
) -> 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
|
// Phase 137-2/137-4: Dev-only observation via Loop Canonicalizer
|
||||||
if crate::config::env::joinir_dev_enabled() {
|
if crate::config::env::joinir_dev_enabled() {
|
||||||
use crate::ast::Span;
|
use crate::ast::Span;
|
||||||
@ -390,4 +398,169 @@ impl MirBuilder {
|
|||||||
// Delegate to legacy binding path (routing_legacy_binding.rs)
|
// Delegate to legacy binding path (routing_legacy_binding.rs)
|
||||||
self.cf_loop_joinir_legacy_binding(condition, body, func_name, debug)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::carrier_info::JoinFragmentMeta;
|
||||||
use crate::mir::join_ir::lowering::error_tags;
|
use crate::mir::join_ir::lowering::error_tags;
|
||||||
use crate::mir::join_ir::{ConstValue, JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst};
|
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 crate::mir::ValueId;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@ -126,16 +127,18 @@ impl LoopTrueBreakOnceBuilderBox {
|
|||||||
|
|
||||||
let mut next_value_id: u32 = 1;
|
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 main_id = JoinFuncId::new(0);
|
||||||
let loop_step_id = JoinFuncId::new(1);
|
let loop_step_id = JoinFuncId::new(1);
|
||||||
let loop_body_id = JoinFuncId::new(2);
|
let k_exit_id = JoinFuncId::new(2);
|
||||||
let k_exit_id = JoinFuncId::new(3);
|
let loop_body_id = JoinFuncId::new(3);
|
||||||
|
|
||||||
// main(env): <prefix> → TailCall(loop_step, env)
|
// main(env): <prefix> → TailCall(loop_step, env)
|
||||||
let main_params = alloc_env_params(&env_fields, &mut next_value_id);
|
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 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
|
// Lower prefix (pre-loop) statements into main
|
||||||
for n in prefix_nodes {
|
for n in prefix_nodes {
|
||||||
@ -174,35 +177,28 @@ impl LoopTrueBreakOnceBuilderBox {
|
|||||||
dst: None,
|
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 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 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);
|
let mut loop_step_func =
|
||||||
|
JoinFunction::new(loop_step_id, "join_func_1".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 loop_step_args = collect_env_args(&env_fields, &env_loop_step)?;
|
let loop_step_args = collect_env_args(&env_fields, &env_loop_step)?;
|
||||||
loop_step_func.body.push(JoinInst::Jump {
|
loop_step_func.body.push(JoinInst::Call {
|
||||||
cont: loop_body_id.as_cont(),
|
func: loop_body_id,
|
||||||
args: loop_step_args.clone(),
|
|
||||||
cond: Some(cond_vid),
|
|
||||||
});
|
|
||||||
loop_step_func.body.push(JoinInst::Jump {
|
|
||||||
cont: k_exit_id.as_cont(),
|
|
||||||
args: loop_step_args,
|
args: loop_step_args,
|
||||||
cond: None,
|
k_next: None,
|
||||||
|
dst: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// loop_body(env): <assign statements> → TailCall(k_exit, env)
|
// loop_body(env): <assign statements> → TailCall(k_exit, env)
|
||||||
let loop_body_params = alloc_env_params(&env_fields, &mut next_value_id);
|
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 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
|
// Lower body statements
|
||||||
for n in body_prefix {
|
for n in body_prefix {
|
||||||
@ -234,6 +230,47 @@ impl LoopTrueBreakOnceBuilderBox {
|
|||||||
|
|
||||||
// loop_body → k_exit tailcall
|
// loop_body → k_exit tailcall
|
||||||
let loop_body_args = collect_env_args(&env_fields, &env_loop_body)?;
|
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 {
|
loop_body_func.body.push(JoinInst::Call {
|
||||||
func: k_exit_id,
|
func: k_exit_id,
|
||||||
args: loop_body_args,
|
args: loop_body_args,
|
||||||
@ -241,10 +278,30 @@ impl LoopTrueBreakOnceBuilderBox {
|
|||||||
dst: None,
|
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
|
// k_exit(env): handle post-loop or return
|
||||||
let k_exit_params = alloc_env_params(&env_fields, &mut next_value_id);
|
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 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
|
// Handle post-loop statements or return
|
||||||
if post_nodes.is_empty() {
|
if post_nodes.is_empty() {
|
||||||
@ -290,9 +347,15 @@ impl LoopTrueBreakOnceBuilderBox {
|
|||||||
module.add_function(loop_body_func);
|
module.add_function(loop_body_func);
|
||||||
module.add_function(k_exit_func);
|
module.add_function(k_exit_func);
|
||||||
module.entry = Some(main_id);
|
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
|
/// 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]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -40,8 +40,10 @@ pub mod legacy;
|
|||||||
pub mod dev_pipeline;
|
pub mod dev_pipeline;
|
||||||
pub mod parity_contract;
|
pub mod parity_contract;
|
||||||
pub mod available_inputs_collector; // Phase 126: available_inputs SSOT
|
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 builder::StepTreeNormalizedShadowLowererBox;
|
||||||
pub use contracts::{CapabilityCheckResult, UnsupportedCapability};
|
pub use contracts::{CapabilityCheckResult, UnsupportedCapability};
|
||||||
pub use parity_contract::{MismatchKind, ShadowParityResult};
|
pub use parity_contract::{MismatchKind, ShadowParityResult};
|
||||||
pub use env_layout::EnvLayout;
|
pub use env_layout::EnvLayout;
|
||||||
|
pub use exit_reconnector::ExitReconnectorBox; // Phase 131 P1.5
|
||||||
|
|||||||
@ -91,6 +91,48 @@ pub enum CarrierInit {
|
|||||||
LoopLocalZero,
|
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) { <assign>*; 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
|
// Phase 229: ConditionAlias removed - redundant with promoted_loopbodylocals
|
||||||
// The naming convention (old_name → "is_<old_name>" or "is_<old_name>_match")
|
// The naming convention (old_name → "is_<old_name>" or "is_<old_name>_match")
|
||||||
// is sufficient to resolve promoted variables dynamically.
|
// is sufficient to resolve promoted variables dynamically.
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
//! ...
|
//! ...
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use super::carrier_info::CarrierRole;
|
use super::carrier_info::{CarrierRole, ExitReconnectMode};
|
||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
|
|
||||||
/// Explicit binding between JoinIR exit value and host variable
|
/// Explicit binding between JoinIR exit value and host variable
|
||||||
@ -257,6 +257,15 @@ pub struct JoinInlineBoundary {
|
|||||||
/// - `Some(CarrierInfo)`: Full carrier metadata available
|
/// - `Some(CarrierInfo)`: Full carrier metadata available
|
||||||
/// - `None`: Legacy path (derive carriers from exit_bindings)
|
/// - `None`: Legacy path (derive carriers from exit_bindings)
|
||||||
pub carrier_info: Option<super::carrier_info::CarrierInfo>,
|
pub carrier_info: Option<super::carrier_info::CarrierInfo>,
|
||||||
|
|
||||||
|
/// 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 {
|
impl JoinInlineBoundary {
|
||||||
@ -284,6 +293,7 @@ impl JoinInlineBoundary {
|
|||||||
expr_result: None, // Phase 33-14: Default to carrier-only pattern
|
expr_result: None, // Phase 33-14: Default to carrier-only pattern
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228: Default to None
|
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
|
expr_result: None, // Phase 33-14
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228
|
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
|
expr_result: None, // Phase 33-14
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228
|
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
|
expr_result: None, // Phase 33-14
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228
|
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
|
expr_result: None, // Phase 33-14
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228
|
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
|
expr_result: None, // Phase 33-14
|
||||||
loop_var_name: None, // Phase 33-16
|
loop_var_name: None, // Phase 33-16
|
||||||
carrier_info: None, // Phase 228
|
carrier_info: None, // Phase 228
|
||||||
|
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,7 @@ pub struct JoinInlineBoundaryBuilder {
|
|||||||
impl JoinInlineBoundaryBuilder {
|
impl JoinInlineBoundaryBuilder {
|
||||||
/// Create a new builder with default empty boundary
|
/// Create a new builder with default empty boundary
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode;
|
||||||
Self {
|
Self {
|
||||||
boundary: JoinInlineBoundary {
|
boundary: JoinInlineBoundary {
|
||||||
join_inputs: vec![],
|
join_inputs: vec![],
|
||||||
@ -96,6 +97,7 @@ impl JoinInlineBoundaryBuilder {
|
|||||||
expr_result: None,
|
expr_result: None,
|
||||||
loop_var_name: None,
|
loop_var_name: None,
|
||||||
carrier_info: None, // Phase 228: Initialize as None
|
carrier_info: None, // Phase 228: Initialize as None
|
||||||
|
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,6 @@ pub struct JoinIrBlockConverter {
|
|||||||
current_block_id: BasicBlockId,
|
current_block_id: BasicBlockId,
|
||||||
current_instructions: Vec<MirInstruction>,
|
current_instructions: Vec<MirInstruction>,
|
||||||
next_block_id: u32,
|
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<String, ValueId>,
|
|
||||||
next_func_name_value_id: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JoinIrBlockConverter {
|
impl JoinIrBlockConverter {
|
||||||
@ -33,8 +29,6 @@ impl JoinIrBlockConverter {
|
|||||||
current_block_id: BasicBlockId(0), // entry block
|
current_block_id: BasicBlockId(0), // entry block
|
||||||
current_instructions: Vec::new(),
|
current_instructions: Vec::new(),
|
||||||
next_block_id: 1, // start from 1 (0 is entry)
|
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);
|
let func_name = join_func_name(*func);
|
||||||
|
|
||||||
// Phase 189: Use stable function name → ValueId mapping
|
// Phase 131 P2: Stable function name ValueId (module-global SSOT)
|
||||||
// This ensures the same function name always gets the same ValueId,
|
//
|
||||||
// which is critical for tail call detection in merge_joinir_mir_blocks
|
// The merge pipeline relies on `Const(String("join_func_N"))` to detect tail calls.
|
||||||
let func_name_id = *self
|
// The ValueId used for that const MUST be stable across *all* functions in the module.
|
||||||
.func_name_to_value_id
|
//
|
||||||
.entry(func_name.clone())
|
// IMPORTANT: avoid collisions with `call_result_id = ValueId(99991)`.
|
||||||
.or_insert_with(|| {
|
const FUNC_NAME_ID_BASE: u32 = 90000;
|
||||||
let id = ValueId(self.next_func_name_value_id);
|
let func_name_id = ValueId(FUNC_NAME_ID_BASE + func.0);
|
||||||
self.next_func_name_value_id += 1;
|
if func_name_id == ValueId(99991) {
|
||||||
id
|
return Err(JoinIrVmBridgeError::new(
|
||||||
});
|
"[joinir_block] func_name_id collided with call_result_id (99991)".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
self.current_instructions.push(MirInstruction::Const {
|
self.current_instructions.push(MirInstruction::Const {
|
||||||
dst: func_name_id,
|
dst: func_name_id,
|
||||||
@ -352,6 +348,17 @@ impl JoinIrBlockConverter {
|
|||||||
effects: EffectMask::PURE,
|
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 {
|
let terminator = MirInstruction::Return {
|
||||||
value: Some(call_result_id),
|
value: Some(call_result_id),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -46,6 +46,15 @@ fi
|
|||||||
# Use the cargo target dir when set (helps LLVM EXE smokes that build under /tmp).
|
# Use the cargo target dir when set (helps LLVM EXE smokes that build under /tmp).
|
||||||
CARGO_TARGET_DIR_EFFECTIVE="${CARGO_TARGET_DIR:-$PWD/target}"
|
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_DEFAULT="$CARGO_TARGET_DIR_EFFECTIVE/release/hakorune"
|
||||||
BIN="${NYASH_BIN:-$BIN_DEFAULT}"
|
BIN="${NYASH_BIN:-$BIN_DEFAULT}"
|
||||||
|
|
||||||
@ -58,10 +67,10 @@ if [[ "$LLVM_FEATURE" == "llvm-inkwell-legacy" ]]; then
|
|||||||
# Legacy inkwell需要LLVM_SYS_180_PREFIX
|
# Legacy inkwell需要LLVM_SYS_180_PREFIX
|
||||||
_LLVMPREFIX=$(llvm-config-18 --prefix)
|
_LLVMPREFIX=$(llvm-config-18 --prefix)
|
||||||
LLVM_SYS_181_PREFIX="${_LLVMPREFIX}" LLVM_SYS_180_PREFIX="${_LLVMPREFIX}" \
|
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
|
else
|
||||||
# llvm-harness(デフォルト)はLLVM_SYS_180_PREFIX不要
|
# 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
|
fi
|
||||||
|
|
||||||
if [[ ! -x "$BIN" ]]; then
|
if [[ ! -x "$BIN" ]]; then
|
||||||
|
|||||||
@ -191,3 +191,62 @@ llvm_exe_build_and_run_numeric_smoke() {
|
|||||||
printf "%s\n" "$EXPECTED"
|
printf "%s\n" "$EXPECTED"
|
||||||
return 1
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,11 @@ require_env || exit 2
|
|||||||
|
|
||||||
llvm_exe_preflight_or_skip || exit 0
|
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)
|
# Phase 131: minimal plugin set (StringBox, ConsoleBox, IntegerBox only)
|
||||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||||
CONSOLEBOX_SO="$NYASH_ROOT/plugins/nyash-console-plugin/libnyash_console_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"
|
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"
|
OUTPUT_EXE="$NYASH_ROOT/tmp/phase131_loop_true_break_once_llvm_exe"
|
||||||
|
|
||||||
EXPECTED=$'1'
|
EXPECTED_EXIT_CODE=1
|
||||||
EXPECTED_LINES=1
|
|
||||||
LLVM_BUILD_LOG="/tmp/phase131_loop_true_break_once_build.log"
|
LLVM_BUILD_LOG="/tmp/phase131_loop_true_break_once_build.log"
|
||||||
if llvm_exe_build_and_run_numeric_smoke; then
|
if llvm_exe_build_and_run_expect_exit_code; then
|
||||||
test_pass "phase131_loop_true_break_once_llvm_exe: output matches expected (1)"
|
test_pass "phase131_loop_true_break_once_llvm_exe: exit code matches expected (1)"
|
||||||
else
|
else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||||
|
|
||||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||||
source "$(dirname "$0")/../../../lib/output_validator.sh"
|
|
||||||
export SMOKES_USE_PYVM=0
|
export SMOKES_USE_PYVM=0
|
||||||
require_env || exit 2
|
require_env || exit 2
|
||||||
|
|
||||||
@ -34,15 +33,8 @@ if [ "$EXIT_CODE" -eq 124 ]; then
|
|||||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||||
elif [ "$EXIT_CODE" -eq 1 ]; then
|
elif [ "$EXIT_CODE" -eq 1 ]; then
|
||||||
# Phase 131: expected output is exit code 1 (return value)
|
# Phase 131: expected output is exit code 1 (return value)
|
||||||
EXPECTED=$'1'
|
echo "[PASS] exit code verified: 1"
|
||||||
if validate_numeric_output 1 "$EXPECTED" "$OUTPUT"; then
|
PASS_COUNT=$((PASS_COUNT + 1))
|
||||||
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
|
|
||||||
else
|
else
|
||||||
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 1)"
|
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 1)"
|
||||||
echo "[INFO] output (tail):"
|
echo "[INFO] output (tail):"
|
||||||
|
|||||||
Reference in New Issue
Block a user