refactor(joinir): make jump_args layout explicit (Phase 256)

This commit is contained in:
2025-12-20 13:04:24 +09:00
parent 1028bd419c
commit 4439d64da3
21 changed files with 715 additions and 195 deletions

View File

@ -45,6 +45,9 @@ impl MirBuilder {
// 関数スコープ SlotRegistry は元の関数側から退避しておくよ。
let saved_slot_registry = self.comp_ctx.current_slot_registry.take();
// Phase 201-A: Clear reserved ValueIds at function entry (function-local).
self.comp_ctx.clear_reserved_value_ids();
// BoxCompilationContext mode: clear()で完全独立化
if context_active {
self.variable_ctx.variable_map.clear();

View File

@ -19,13 +19,14 @@
//! &remapped_args,
//! block_id,
//! strict_mode,
//! boundary.jump_args_layout,
//! )?;
//! exit_phi_inputs.push((result.block_id, result.expr_result_value));
//! carrier_inputs.extend(result.carrier_values);
//! ```
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding;
use crate::mir::join_ir::lowering::inline_boundary::{JumpArgsLayout, LoopExitBinding};
use crate::mir::{BasicBlockId, ValueId};
use std::collections::BTreeMap;
@ -51,8 +52,9 @@ pub struct ExitArgsCollectionResult {
/// # Phase 118 P2 Contract
///
/// - Every LoopState carrier in exit_bindings must have an exit PHI input
/// - jump_args order is assumed to match exit_bindings order, with at most one
/// leading extra slot (legacy layouts)
/// - jump_args order is assumed to match exit_bindings order
/// - Layout is enforced via JumpArgsLayout (no inference)
/// - Extra trailing args are treated as invariants and ignored
/// - This avoids Pattern3-specific assumptions such as "jump_args[0] is loop_var"
///
/// # Fail-Fast Guarantee
@ -76,6 +78,7 @@ impl ExitArgsCollectorBox {
/// * `remapped_args` - Remapped jump_args from JoinIR block (already in host value space)
/// * `block_id` - Source block ID for PHI inputs
/// * `strict_exit` - If true, Fail-Fast on any validation error
/// * `layout` - jump_args layout policy (SSOT from boundary)
///
/// # Returns
///
@ -84,16 +87,17 @@ impl ExitArgsCollectorBox {
///
/// # Phase 118 P2: SSOT Offset Calculation
///
/// The offset is determined by comparing jump_args length with exit_bindings:
/// - `len(jump_args) == len(exit_phi_bindings)`: offset = 0
/// - `len(jump_args) == len(exit_phi_bindings) + 1`: offset = 1 (legacy layout)
/// - Otherwise: error (or warning in non-strict mode)
/// The offset is determined by JumpArgsLayout:
/// - `CarriersOnly`: offset = 0
/// - `ExprResultPlusCarriers`: offset = 1
/// - Length mismatches are errors (or warnings in non-strict mode)
pub fn collect(
&self,
exit_bindings: &[LoopExitBinding],
remapped_args: &[ValueId],
block_id: BasicBlockId,
strict_exit: bool,
layout: JumpArgsLayout,
) -> Result<ExitArgsCollectionResult, String> {
// Filter exit bindings to get only LoopState carriers (skip ConditionOnly)
let exit_phi_bindings: Vec<_> = exit_bindings
@ -111,15 +115,11 @@ impl ExitArgsCollectorBox {
}
// Phase 118 P2: Calculate offset (SSOT)
let offset = self.calculate_offset(
remapped_args.len(),
exit_phi_bindings.len(),
block_id,
strict_exit,
)?;
let offset =
self.calculate_offset(remapped_args.len(), exit_phi_bindings.len(), block_id, strict_exit, layout)?;
// Collect expr result (first jump arg if offset > 0)
let expr_result_value = if offset > 0 {
let expr_result_value = if layout == JumpArgsLayout::ExprResultPlusCarriers && offset > 0 {
remapped_args.first().copied()
} else {
None
@ -168,14 +168,9 @@ impl ExitArgsCollectorBox {
exit_phi_bindings_len: usize,
block_id: BasicBlockId,
strict_exit: bool,
layout: JumpArgsLayout,
) -> Result<usize, String> {
if jump_args_len == exit_phi_bindings_len {
// Direct mapping: jump_args[i] = exit_phi_bindings[i]
Ok(0)
} else if jump_args_len == exit_phi_bindings_len + 1 {
// Legacy layout: jump_args[0] is expr result, jump_args[1..] are carriers
Ok(1)
} else if jump_args_len < exit_phi_bindings_len {
if jump_args_len < exit_phi_bindings_len {
// Too short - missing carriers
let msg = format!(
"[joinir/exit-line] jump_args too short: need {} carrier args (from exit_bindings) but got {} in block {:?}",
@ -188,15 +183,43 @@ impl ExitArgsCollectorBox {
Ok(0) // Best effort: try direct mapping
}
} else {
// Too long - extra args beyond carriers (e.g., invariants in Pattern 7)
// Phase 256 P1.8: Allow excess args as long as we have enough for carriers
// Direct mapping: jump_args[0..N] = exit_phi_bindings[0..N], rest ignored
#[cfg(debug_assertions)]
eprintln!(
"[joinir/exit-line] jump_args has {} extra args (block {:?}), ignoring invariants",
jump_args_len - exit_phi_bindings_len, block_id
);
Ok(0) // Direct mapping: first N args are carriers
match layout {
JumpArgsLayout::CarriersOnly => {
if jump_args_len > exit_phi_bindings_len {
#[cfg(debug_assertions)]
eprintln!(
"[joinir/exit-line] jump_args has {} extra args (block {:?}), ignoring invariants",
jump_args_len - exit_phi_bindings_len,
block_id
);
}
Ok(0)
}
JumpArgsLayout::ExprResultPlusCarriers => {
if jump_args_len >= exit_phi_bindings_len + 1 {
if jump_args_len > exit_phi_bindings_len + 1 {
#[cfg(debug_assertions)]
eprintln!(
"[joinir/exit-line] jump_args has {} extra args (block {:?}), ignoring invariants after expr_result",
jump_args_len - (exit_phi_bindings_len + 1),
block_id
);
}
Ok(1)
} else {
let msg = format!(
"[joinir/exit-line] expr_result layout requires leading slot: carriers={} args={} in block {:?}",
exit_phi_bindings_len, jump_args_len, block_id
);
if strict_exit {
Err(msg)
} else {
eprintln!("[DEBUG-177] {}", msg);
Ok(0)
}
}
}
}
}
}
@ -245,7 +268,13 @@ mod tests {
make_binding("count", CarrierRole::LoopState),
];
let args = vec![ValueId(10), ValueId(20)];
let result = collector.collect(&bindings, &args, BasicBlockId(1), true);
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
true,
JumpArgsLayout::CarriersOnly,
);
assert!(result.is_ok());
let res = result.unwrap();
@ -261,7 +290,13 @@ mod tests {
let collector = ExitArgsCollectorBox::new();
let bindings = vec![make_binding("sum", CarrierRole::LoopState)];
let args = vec![ValueId(5), ValueId(10)]; // [expr_result, carrier]
let result = collector.collect(&bindings, &args, BasicBlockId(1), true);
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
true,
JumpArgsLayout::ExprResultPlusCarriers,
);
assert!(result.is_ok());
let res = result.unwrap();
@ -278,7 +313,13 @@ mod tests {
make_binding("is_digit", CarrierRole::ConditionOnly), // Skip
];
let args = vec![ValueId(10)]; // Only one arg for LoopState carrier
let result = collector.collect(&bindings, &args, BasicBlockId(1), true);
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
true,
JumpArgsLayout::CarriersOnly,
);
assert!(result.is_ok());
let res = result.unwrap();
@ -293,7 +334,13 @@ mod tests {
make_binding("count", CarrierRole::LoopState),
];
let args = vec![ValueId(10)]; // Missing one carrier
let result = collector.collect(&bindings, &args, BasicBlockId(1), true);
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
true,
JumpArgsLayout::CarriersOnly,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too short"));
@ -307,7 +354,13 @@ mod tests {
make_binding("count", CarrierRole::LoopState),
];
let args = vec![ValueId(10)]; // Missing one carrier
let result = collector.collect(&bindings, &args, BasicBlockId(1), false);
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
false,
JumpArgsLayout::CarriersOnly,
);
// Non-strict mode: succeeds with warning
assert!(result.is_ok());
@ -328,4 +381,30 @@ mod tests {
assert_eq!(map["sum"].len(), 2);
assert_eq!(map["count"].len(), 1);
}
#[test]
fn test_collect_extra_invariants_no_expr_result() {
let collector = ExitArgsCollectorBox::new();
let bindings = vec![
make_binding("i", CarrierRole::LoopState),
make_binding("start", CarrierRole::LoopState),
make_binding("result", CarrierRole::LoopState),
];
let args = vec![ValueId(1), ValueId(2), ValueId(3), ValueId(99)]; // extra invariant
let result = collector.collect(
&bindings,
&args,
BasicBlockId(1),
true,
JumpArgsLayout::CarriersOnly,
);
assert!(result.is_ok());
let res = result.unwrap();
assert_eq!(res.expr_result_value, None);
assert_eq!(res.carrier_values.len(), 3);
assert_eq!(res.carrier_values[0].1, (BasicBlockId(1), ValueId(1)));
assert_eq!(res.carrier_values[1].1, (BasicBlockId(1), ValueId(2)));
assert_eq!(res.carrier_values[2].1, (BasicBlockId(1), ValueId(3)));
}
}

View File

@ -366,6 +366,7 @@ pub(super) fn merge_and_rewrite(
// 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_lowering_decision: Option<LoweringDecision> = None;
let mut k_exit_jump_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 =
@ -462,6 +463,7 @@ pub(super) fn merge_and_rewrite(
if let Some(decision) = tail_call_policy.classify_tail_call(callee_name, &remapped_args) {
// This is a k_exit tail call - policy says normalize to exit jump
k_exit_lowering_decision = Some(decision);
k_exit_jump_args = old_block.jump_args.clone();
found_tail_call = true;
if debug {
log!(
@ -595,14 +597,17 @@ pub(super) fn merge_and_rewrite(
.map(|name| continuation_candidates.contains(name))
.unwrap_or(false);
let is_recursive_call = target_func_name
.as_ref()
.map(|name| name == func_name)
.unwrap_or(false);
if let Some(ref target_func_name) = target_func_name {
if let Some(target_params) = function_params.get(target_func_name) {
// Phase 256 P1.10: Detect call type for param binding strategy
// - Recursive call (loop_step → loop_step): Skip all param bindings (PHI handles via latch edges)
// - Exit call (loop_step → k_exit): Skip all param bindings (exit PHI handles via exit edges)
// - Header entry (main → loop_step at header): Skip all (PHI handles via entry edges)
let is_recursive_call = target_func_name == func_name;
// Phase 256 P1.10: Detect if target is the loop entry function
// When calling the loop entry function, header PHIs will define the carriers.
// We should skip param bindings in this case.
@ -753,97 +758,99 @@ pub(super) fn merge_and_rewrite(
// At this point, args[0] contains the updated loop variable value (i_next).
// We record this as the latch incoming so that the header PHI can reference
// the correct SSA value at loop continuation time.
if let Some(b) = boundary {
if let Some(loop_var_name) = &b.loop_var_name {
if !args.is_empty() {
// The first arg is the loop variable's updated value
let latch_value = args[0];
// The current block (new_block_id) is the latch block
loop_header_phi_info.set_latch_incoming(
loop_var_name,
new_block_id, // latch block
latch_value, // updated loop var value (i_next)
);
if debug {
log!(
true,
"[cf_loop/joinir] Phase 33-16: Set latch incoming for '{}': block={:?}, value={:?}",
loop_var_name, new_block_id, latch_value
if is_recursive_call {
if let Some(b) = boundary {
if let Some(loop_var_name) = &b.loop_var_name {
if !args.is_empty() {
// The first arg is the loop variable's updated value
let latch_value = args[0];
// The current block (new_block_id) is the latch block
loop_header_phi_info.set_latch_incoming(
loop_var_name,
new_block_id, // latch block
latch_value, // updated loop var value (i_next)
);
}
}
}
// Phase 33-20: Also set latch incoming for other carriers from exit_bindings
//
// args layout depends on whether we have a dedicated loop variable:
// - loop_var_name = Some(..): args[0] is the loop variable, args[1..] are other carriers
// - loop_var_name = None: args[0..] are carriers (no reserved loop-var slot)
//
// Phase 176-4 FIX: exit_bindings may include the loop variable itself; skip it when loop_var_name is set.
let mut carrier_arg_idx = if b.loop_var_name.is_some() { 1 } else { 0 };
for binding in b.exit_bindings.iter() {
// Skip if this binding is for the loop variable (already handled)
if let Some(ref loop_var) = b.loop_var_name {
if &binding.carrier_name == loop_var {
if debug {
log!(
true,
"[cf_loop/joinir] Phase 176-4: Skipping loop variable '{}' in exit_bindings (handled separately)",
binding.carrier_name
"[cf_loop/joinir] Phase 33-16: Set latch incoming for '{}': block={:?}, value={:?}",
loop_var_name, new_block_id, latch_value
);
}
continue; // Skip loop variable
}
}
// Process non-loop-variable carrier
if carrier_arg_idx < args.len() {
let latch_value = args[carrier_arg_idx];
loop_header_phi_info.set_latch_incoming(
&binding.carrier_name,
new_block_id,
latch_value,
);
// Phase 33-20: Also set latch incoming for other carriers from exit_bindings
//
// args layout depends on whether we have a dedicated loop variable:
// - loop_var_name = Some(..): args[0] is the loop variable, args[1..] are other carriers
// - loop_var_name = None: args[0..] are carriers (no reserved loop-var slot)
//
// Phase 176-4 FIX: exit_bindings may include the loop variable itself; skip it when loop_var_name is set.
let mut carrier_arg_idx = if b.loop_var_name.is_some() { 1 } else { 0 };
for binding in b.exit_bindings.iter() {
// Skip if this binding is for the loop variable (already handled)
if let Some(ref loop_var) = b.loop_var_name {
if &binding.carrier_name == loop_var {
if debug {
log!(
true,
"[cf_loop/joinir] Phase 176-4: Skipping loop variable '{}' in exit_bindings (handled separately)",
binding.carrier_name
);
}
continue; // Skip loop variable
}
}
if debug {
// Process non-loop-variable carrier
if carrier_arg_idx < args.len() {
let latch_value = args[carrier_arg_idx];
loop_header_phi_info.set_latch_incoming(
&binding.carrier_name,
new_block_id,
latch_value,
);
if debug {
log!(
true,
"[cf_loop/joinir] Phase 176-4: Set latch incoming for carrier '{}': block={:?}, value={:?} (arg[{}])",
binding.carrier_name, new_block_id, latch_value, carrier_arg_idx
);
}
carrier_arg_idx += 1;
} else if debug {
log!(
true,
"[cf_loop/joinir] Phase 176-4: Set latch incoming for carrier '{}': block={:?}, value={:?} (arg[{}])",
binding.carrier_name, new_block_id, latch_value, carrier_arg_idx
"[cf_loop/joinir] Phase 33-20 WARNING: No arg for carrier '{}' at index {}",
binding.carrier_name, carrier_arg_idx
);
}
carrier_arg_idx += 1;
} else if debug {
log!(
true,
"[cf_loop/joinir] Phase 33-20 WARNING: No arg for carrier '{}' at index {}",
binding.carrier_name, carrier_arg_idx
);
}
}
// Phase 255 P2: Set latch incoming for loop invariants
//
// Loop invariants don't have corresponding tail call args because they
// are not modified by the loop. Their latch incoming is the PHI dst itself
// (same value across all iterations).
for (inv_name, _inv_host_id) in b.loop_invariants.iter() {
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
// For invariants, latch incoming is the PHI dst (same value)
loop_header_phi_info.set_latch_incoming(
inv_name,
new_block_id, // latch block
phi_dst, // same as PHI dst (invariant value)
);
if debug {
log!(
true,
"[cf_loop/joinir] Phase 255 P2: Set latch incoming for loop invariant '{}': block={:?}, value={:?} (PHI dst itself)",
inv_name, new_block_id, phi_dst
// Phase 255 P2: Set latch incoming for loop invariants
//
// Loop invariants don't have corresponding tail call args because they
// are not modified by the loop. Their latch incoming is the PHI dst itself
// (same value across all iterations).
for (inv_name, _inv_host_id) in b.loop_invariants.iter() {
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
// For invariants, latch incoming is the PHI dst (same value)
loop_header_phi_info.set_latch_incoming(
inv_name,
new_block_id, // latch block
phi_dst, // same as PHI dst (invariant value)
);
if debug {
log!(
true,
"[cf_loop/joinir] Phase 255 P2: Set latch incoming for loop invariant '{}': block={:?}, value={:?} (PHI dst itself)",
inv_name, new_block_id, phi_dst
);
}
}
}
}
@ -963,6 +970,7 @@ pub(super) fn merge_and_rewrite(
&remapped_args,
new_block_id,
strict_exit,
b.jump_args_layout,
)?;
// Add expr_result to exit_phi_inputs (if present)
@ -1080,8 +1088,22 @@ pub(super) fn merge_and_rewrite(
// Collect exit values from k_exit arguments
if let Some(b) = boundary {
let collector = ExitArgsCollectorBox::new();
let exit_args: Vec<ValueId> = if let Some(ref jump_args) = k_exit_jump_args {
jump_args
.iter()
.map(|&arg| remapper.remap_value(arg))
.collect()
} else {
args.clone()
};
let collection_result =
collector.collect(&b.exit_bindings, &args, new_block_id, strict_exit)?;
collector.collect(
&b.exit_bindings,
&exit_args,
new_block_id,
strict_exit,
b.jump_args_layout,
)?;
if let Some(expr_result_value) = collection_result.expr_result_value {
exit_phi_inputs.push((collection_result.block_id, expr_result_value));
}

View File

@ -40,6 +40,7 @@ pub use merge_result::MergeContracts;
use super::trace;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::join_ir::lowering::error_tags;
use crate::mir::{MirModule, ValueId};
use std::collections::BTreeMap;
@ -144,6 +145,16 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
debug,
);
if let Some(boundary) = boundary {
if let Err(msg) = boundary.validate_jump_args_layout() {
return Err(error_tags::freeze_with_hint(
"phase256/jump_args_layout",
&msg,
"set JoinInlineBoundary.jump_args_layout via builder and avoid expr_result/carrier mismatch",
));
}
}
if verbose {
if let Some(boundary) = boundary {
let exit_summary: Vec<String> = boundary
@ -404,9 +415,17 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
debug && !reserved_phi_dsts.is_empty(),
);
// Phase 201-A: Also reserve JoinIR parameter ValueIds to avoid collisions
let mut reserved_value_ids = reserved_phi_dsts.clone();
for params in function_params.values() {
for &param in params {
reserved_value_ids.insert(param);
}
}
// Phase 201-A: Set reserved IDs in MirBuilder so next_value_id() skips them
// This protects against carrier corruption when break conditions emit Const instructions
builder.comp_ctx.reserved_value_ids = reserved_phi_dsts.clone();
builder.comp_ctx.reserved_value_ids = reserved_value_ids.clone();
trace.stderr_if(
&format!(
"[cf_loop/joinir] Phase 201-A: Set builder.comp_ctx.reserved_value_ids = {:?}",
@ -420,7 +439,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
builder,
&used_values,
&mut remapper,
&reserved_phi_dsts,
&reserved_value_ids,
debug,
)?;
@ -533,8 +552,18 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
}
}
let main_func_name = "join_func_0";
let loop_step_func_name = "join_func_1";
let canonical_main = crate::mir::join_ir::lowering::canonical_names::MAIN;
let canonical_loop_step = crate::mir::join_ir::lowering::canonical_names::LOOP_STEP;
let main_func_name = if function_params.contains_key(canonical_main) {
canonical_main
} else {
"join_func_0"
};
let loop_step_func_name = if function_params.contains_key(canonical_loop_step) {
canonical_loop_step
} else {
"join_func_1"
};
if function_params.get(main_func_name).is_none() {
trace.stderr_if(
@ -1046,19 +1075,6 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
}
}
// Phase 201-A: Clear reserved ValueIds after merge completes
// Future loops will set their own reserved IDs
if !builder.comp_ctx.reserved_value_ids.is_empty() {
trace.stderr_if(
&format!(
"[cf_loop/joinir] Phase 201-A: Clearing reserved_value_ids (was {:?})",
builder.comp_ctx.reserved_value_ids
),
debug,
);
builder.comp_ctx.reserved_value_ids.clear();
}
// Phase 246-EX-FIX: Handle loop variable expr_result separately from carrier expr_result
//
// The loop variable (e.g., 'i') is returned via exit_phi_result_id, not carrier_phis.

View File

@ -381,6 +381,7 @@ mod tests {
condition_inputs: vec![], // Phase 171: Add missing field
condition_bindings: vec![], // Phase 171-fix: Add missing field
expr_result: None, // Phase 33-14: Add missing field
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
loop_var_name: None, // Phase 33-16: Add missing field
carrier_info: None, // Phase 228: Add missing field
loop_invariants: vec![], // Phase 255 P2: Add missing field

View File

@ -135,6 +135,7 @@ mod tests {
condition_inputs: vec![], // Phase 171: Add missing field
condition_bindings: vec![], // Phase 171-fix: Add missing field
expr_result: None, // Phase 33-14: Add missing field
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
loop_var_name: None, // Phase 33-16: Add missing field
carrier_info: None, // Phase 228: Add missing field
loop_invariants: vec![], // Phase 255 P2: Add missing field

View File

@ -365,7 +365,7 @@ impl MirBuilder {
/// - 2 carriers: i (loop index), start (segment start)
/// - 3 invariants: s (haystack), sep (separator), result (accumulator)
/// - Conditional step via Select (P0 pragmatic approach)
/// - Post-loop segment push in k_exit
/// - Post-loop segment push stays in host AST (k_exit is pure return)
pub(crate) fn cf_loop_pattern7_split_scan_impl(
&mut self,
condition: &ASTNode,

View File

@ -104,6 +104,15 @@ pub struct LoopExitBinding {
pub role: CarrierRole,
}
/// Layout policy for JoinIR jump_args (SSOT)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JumpArgsLayout {
/// jump_args = [carriers...]
CarriersOnly,
/// jump_args = [expr_result, carriers...]
ExprResultPlusCarriers,
}
/// Boundary information for inlining a JoinIR fragment into a host function
///
/// This structure captures the "interface" between a JoinIR fragment and the
@ -288,6 +297,12 @@ pub struct JoinInlineBoundary {
/// Here, `expr_result = None` because the loop doesn't return a value.
pub expr_result: Option<crate::mir::ValueId>,
/// Phase 256 P1.12: jump_args layout (SSOT)
///
/// This prevents merge from guessing whether jump_args contains a leading
/// expr_result slot.
pub jump_args_layout: JumpArgsLayout,
/// Phase 33-16: Loop variable name (for LoopHeaderPhiBuilder)
///
/// The name of the loop control variable (e.g., "i" in `loop(i < 3)`).
@ -330,6 +345,39 @@ pub struct JoinInlineBoundary {
}
impl JoinInlineBoundary {
/// Decide jump_args layout from boundary inputs (SSOT)
pub fn decide_jump_args_layout(
expr_result: Option<ValueId>,
exit_bindings: &[LoopExitBinding],
) -> JumpArgsLayout {
if let Some(expr_result_id) = expr_result {
let expr_is_carrier = exit_bindings.iter().any(|binding| {
binding.role != CarrierRole::ConditionOnly
&& binding.join_exit_value == expr_result_id
});
if expr_is_carrier {
JumpArgsLayout::CarriersOnly
} else {
JumpArgsLayout::ExprResultPlusCarriers
}
} else {
JumpArgsLayout::CarriersOnly
}
}
/// Validate jump_args layout against boundary contract (Fail-Fast)
pub fn validate_jump_args_layout(&self) -> Result<(), String> {
let expected =
Self::decide_jump_args_layout(self.expr_result, self.exit_bindings.as_slice());
if self.jump_args_layout != expected {
return Err(format!(
"joinir/jump_args_layout_mismatch: expr_result={:?} layout={:?} expected={:?}",
self.expr_result, self.jump_args_layout, expected
));
}
Ok(())
}
/// Phase 132-R0 Task 1: SSOT for default continuation function names
/// Phase 256 P1.7: Changed from JoinFuncIds to function names (Strings)
///
@ -386,6 +434,7 @@ impl JoinInlineBoundary {
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
condition_bindings: vec![], // Phase 171-fix: Default to empty
expr_result: None, // Phase 33-14: Default to carrier-only pattern
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228: Default to None
continuation_func_ids: Self::default_continuations(),
@ -432,6 +481,7 @@ impl JoinInlineBoundary {
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
condition_bindings: vec![], // Phase 171-fix: Default to empty
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
@ -495,6 +545,7 @@ impl JoinInlineBoundary {
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
condition_bindings: vec![], // Phase 171-fix: Default to empty
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
@ -544,6 +595,7 @@ impl JoinInlineBoundary {
condition_inputs,
condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
@ -597,6 +649,7 @@ impl JoinInlineBoundary {
condition_inputs,
condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
@ -657,6 +710,7 @@ impl JoinInlineBoundary {
condition_inputs: vec![], // Deprecated, use condition_bindings instead
condition_bindings,
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
@ -692,4 +746,36 @@ mod tests {
fn test_boundary_mismatched_inputs() {
JoinInlineBoundary::new_inputs_only(vec![ValueId(0), ValueId(1)], vec![ValueId(4)]);
}
#[test]
fn test_jump_args_layout_rejects_expr_result_carrier_mismatch() {
let boundary = JoinInlineBoundary {
join_inputs: vec![],
host_inputs: vec![],
join_outputs: vec![],
#[allow(deprecated)]
host_outputs: vec![],
loop_invariants: vec![],
exit_bindings: vec![LoopExitBinding {
carrier_name: "result".to_string(),
join_exit_value: ValueId(10),
host_slot: ValueId(1),
role: CarrierRole::LoopState,
}],
#[allow(deprecated)]
condition_inputs: vec![],
condition_bindings: vec![],
expr_result: Some(ValueId(10)),
jump_args_layout: JumpArgsLayout::ExprResultPlusCarriers,
loop_var_name: None,
carrier_info: None,
continuation_func_ids: JoinInlineBoundary::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(),
};
let err = boundary
.validate_jump_args_layout()
.expect_err("layout mismatch must fail-fast");
assert!(err.contains("jump_args_layout_mismatch"));
}
}

View File

@ -25,7 +25,7 @@
//! ```
use super::condition_to_joinir::ConditionBinding;
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
use super::inline_boundary::{JoinInlineBoundary, JumpArgsLayout, LoopExitBinding};
use crate::mir::ValueId;
/// Role of a parameter in JoinIR lowering (Phase 200-A)
@ -96,6 +96,7 @@ impl JoinInlineBoundaryBuilder {
condition_inputs: vec![],
condition_bindings: vec![],
expr_result: None,
jump_args_layout: JumpArgsLayout::CarriersOnly,
loop_var_name: None,
carrier_info: None, // Phase 228: Initialize as None
continuation_func_ids: JoinInlineBoundary::default_continuations(),
@ -194,7 +195,12 @@ impl JoinInlineBoundaryBuilder {
/// Build the final JoinInlineBoundary
pub fn build(self) -> JoinInlineBoundary {
self.boundary
let mut boundary = self.boundary;
boundary.jump_args_layout = JoinInlineBoundary::decide_jump_args_layout(
boundary.expr_result,
boundary.exit_bindings.as_slice(),
);
boundary
}
/// Add a parameter with explicit role (Phase 200-A)

View File

@ -51,17 +51,14 @@
//! start_next = Select(is_match, start_next_if, start)
//! i_next = Select(is_match, i_next_if, i_next_else)
//!
//! // 4. Conditional push (Phase 256 P0: Simple approach - always push, but with conditional computation)
//! // For P0, we keep the push simple and rely on Boundary to handle result management
//! // 4. Conditional push (Phase 256 P1: ConditionalMethodCall)
//! // Push the matched segment only when is_match is true
//!
//! // 5. Tail recursion
//! Call(loop_step, [s, sep, result, i_next, start_next])
//!
//! fn k_exit(result, start, s):
//! // Post-loop: push final segment
//! len = s.length()
//! tail = s.substring(start, len)
//! result.push(tail)
//! // Post-loop tail push stays in host AST; JoinIR exit is a pure return.
//! return result
//! ```
//!
@ -73,7 +70,7 @@
//! - 3 invariants: s, sep, result (managed via loop_invariants)
//! - substring and push are BoxCall operations
//! - Select for conditional step (safer than Branch for P0)
//! - Post-loop segment push in k_exit
//! - Post-loop segment push stays in host AST (k_exit is a pure return)
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::{
@ -90,7 +87,7 @@ use crate::mir::join_ir::{
///
/// - **main()**: Entry point, calls loop_step
/// - **loop_step(s, sep, result, i, start)**: Loop body with conditional step
/// - **k_exit(result, start, s)**: Post-loop segment push + return
/// - **k_exit(result, start, s)**: Pure return (post-loop push stays in host AST)
///
/// ## Design Philosophy
///
@ -144,6 +141,8 @@ pub(crate) fn lower_split_scan_minimal(
let i_plus_sep = join_value_space.alloc_local(); // i + sep_len
let window = join_value_space.alloc_local(); // s.substring(i, i_plus_sep)
let is_match = join_value_space.alloc_local(); // window == sep
let segment = join_value_space.alloc_local(); // s.substring(start, i)
let result_next = join_value_space.alloc_local(); // updated result (conditional push)
let start_next_if = join_value_space.alloc_local(); // i_plus_sep (match case)
let i_next_if = join_value_space.alloc_local(); // start_next_if (match case)
let i_next_else = join_value_space.alloc_local(); // i + 1 (no-match case)
@ -154,8 +153,6 @@ pub(crate) fn lower_split_scan_minimal(
let result_exit_param = join_value_space.alloc_param(); // accumulator
let start_exit_param = join_value_space.alloc_param(); // segment start
let s_exit_param = join_value_space.alloc_param(); // haystack
let s_len = join_value_space.alloc_local(); // s.length()
let tail = join_value_space.alloc_local(); // final segment
// ==================================================================
// main() function
@ -261,7 +258,26 @@ pub(crate) fn lower_split_scan_minimal(
rhs: sep_step_param,
}));
// 7. Match case variable computation: start_next = i_plus_sep, i_next = start_next
// 7. Compute segment for conditional push: s.substring(start, i)
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(segment),
box_name: "StringBox".to_string(),
method: "substring".to_string(),
args: vec![s_step_param, start_step_param, i_step_param],
}));
// 8. Conditional push when separator matches
loop_step_func.body.push(JoinInst::ConditionalMethodCall {
cond: is_match,
dst: result_next,
receiver: result_step_param,
method: "push".to_string(),
args: vec![segment],
});
// 9. Match case variable computation: start_next = i_plus_sep, i_next = start_next
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Const {
@ -290,7 +306,7 @@ pub(crate) fn lower_split_scan_minimal(
value: ConstValue::Integer(1),
}));
// 8. No-match case: i_next_else = i + 1
// 10. No-match case: i_next_else = i + 1
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BinOp {
@ -300,7 +316,7 @@ pub(crate) fn lower_split_scan_minimal(
rhs: const_1,
}));
// 9. Select for start_next: Select(is_match, i_plus_sep, start)
// 11. Select for start_next: Select(is_match, i_plus_sep, start)
loop_step_func
.body
.push(JoinInst::Select {
@ -311,7 +327,7 @@ pub(crate) fn lower_split_scan_minimal(
type_hint: None,
});
// 10. Select for i_next: Select(is_match, i_plus_sep, i + 1)
// 12. Select for i_next: Select(is_match, i_plus_sep, i + 1)
loop_step_func
.body
.push(JoinInst::Select {
@ -322,10 +338,10 @@ pub(crate) fn lower_split_scan_minimal(
type_hint: None,
});
// 11. Tail recursion: Call(loop_step, [i_next, start_next, result, s, sep]) - Carriers-First!
// 13. Tail recursion: Call(loop_step, [i_next, start_next, result, s, sep]) - Carriers-First!
loop_step_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_next, start_next, result_step_param, s_step_param, sep_step_param],
args: vec![i_next, start_next, result_next, s_step_param, sep_step_param],
k_next: None,
dst: None,
});
@ -344,38 +360,8 @@ pub(crate) fn lower_split_scan_minimal(
vec![i_exit_param, start_exit_param, result_exit_param, s_exit_param],
);
// 1. s_len = s.length()
k_exit_func
.body
.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(s_len),
box_name: "StringBox".to_string(),
method: "length".to_string(),
args: vec![s_exit_param],
}));
// 2. tail = s.substring(start, s_len)
k_exit_func
.body
.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(tail),
box_name: "StringBox".to_string(),
method: "substring".to_string(),
args: vec![s_exit_param, start_exit_param, s_len],
}));
// 3. result.push(tail) - BoxCall with side effect
k_exit_func
.body
.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: None,
box_name: "ArrayBox".to_string(), // Assuming result is ArrayBox
method: "push".to_string(),
args: vec![result_exit_param, tail],
}));
// 4. Return result (main return value)
// Phase 256 P0: Also return start and i for carrier PHI setup
// Return result (main return value).
// Post-loop tail push stays in host AST (avoid double-push).
k_exit_func.body.push(JoinInst::Ret {
value: Some(result_exit_param),
});
@ -437,7 +423,7 @@ mod tests {
}
#[test]
fn test_k_exit_has_push_box_call() {
fn test_k_exit_is_pure_return() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_split_scan_minimal(&mut join_value_space);
@ -447,15 +433,31 @@ mod tests {
.get(&JoinFuncId::new(2))
.expect("k_exit function should exist");
// BoxCall(push) が含まれることを確認
let has_push = k_exit.body.iter().any(|inst| {
assert_eq!(k_exit.body.len(), 1);
assert!(matches!(k_exit.body[0], JoinInst::Ret { .. }));
}
#[test]
fn test_loop_step_has_conditional_push() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_split_scan_minimal(&mut join_value_space);
let loop_step = join_module
.functions
.get(&JoinFuncId::new(1))
.expect("loop_step function should exist");
let has_conditional_push = loop_step.body.iter().any(|inst| {
matches!(
inst,
JoinInst::Compute(MirLikeInst::BoxCall { method, .. })
if method == "push"
JoinInst::ConditionalMethodCall { method, .. } if method == "push"
)
});
assert!(has_push, "k_exit should contain push BoxCall");
assert!(
has_conditional_push,
"loop_step should contain ConditionalMethodCall push"
);
}
}

View File

@ -276,15 +276,18 @@ impl JoinIrBlockConverter {
branch_terminator,
);
let then_value = mir_func.next_value_id();
let else_value = mir_func.next_value_id();
// then block: method call
let mut then_block_obj = crate::mir::BasicBlock::new(then_block);
then_block_obj.instructions.push(MirInstruction::BoxCall {
dst: Some(*dst),
dst: Some(then_value),
box_val: *receiver,
method: method.to_string(),
method_id: None,
args: args.to_vec(),
effects: EffectMask::PURE,
effects: EffectMask::WRITE,
});
then_block_obj.instruction_spans.push(Span::unknown());
then_block_obj.terminator = Some(MirInstruction::Jump {
@ -295,7 +298,7 @@ impl JoinIrBlockConverter {
// else block: copy receiver
let mut else_block_obj = crate::mir::BasicBlock::new(else_block);
else_block_obj.instructions.push(MirInstruction::Copy {
dst: *dst,
dst: else_value,
src: *receiver,
});
else_block_obj.instruction_spans.push(Span::unknown());
@ -304,8 +307,14 @@ impl JoinIrBlockConverter {
});
mir_func.blocks.insert(else_block, else_block_obj);
// merge block
let merge_block_obj = crate::mir::BasicBlock::new(merge_block);
// merge block: phi for dst
let mut merge_block_obj = crate::mir::BasicBlock::new(merge_block);
merge_block_obj.instructions.push(MirInstruction::Phi {
dst: *dst,
inputs: vec![(then_block, then_value), (else_block, else_value)],
type_hint: None,
});
merge_block_obj.instruction_spans.push(Span::unknown());
mir_func.blocks.insert(merge_block, merge_block_obj);
self.current_block_id = merge_block;

View File

@ -22,7 +22,8 @@ fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize {
// Mark values used by side-effecting instructions and terminators
for (_bid, block) in &function.blocks {
for instruction in &block.instructions {
if !instruction.effects().is_pure() {
let has_dst = instruction.dst_value().is_some();
if !instruction.effects().is_pure() || !has_dst {
if let Some(dst) = instruction.dst_value() {
used_values.insert(dst);
}
@ -99,3 +100,76 @@ fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize {
}
eliminated
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
use crate::mir::{BasicBlockId, ConstValue, EffectMask, FunctionSignature, MirInstruction, MirType};
#[test]
fn test_dce_keeps_jump_args_values() {
let mut module = MirModule::new("dce_test".to_string());
let sig = FunctionSignature {
name: "test/0".to_string(),
params: vec![],
return_type: MirType::Void,
effects: EffectMask::PURE,
};
let mut func = MirFunction::new(sig, BasicBlockId(0));
let v1 = ValueId(1);
let v2 = ValueId(2);
let v_dead = ValueId(3);
{
let bb0 = func.blocks.get_mut(&BasicBlockId(0)).unwrap();
bb0.instructions.push(MirInstruction::Const {
dst: v1,
value: ConstValue::Integer(123),
});
bb0.instruction_spans.push(Span::unknown());
bb0.instructions.push(MirInstruction::Copy { dst: v2, src: v1 });
bb0.instruction_spans.push(Span::unknown());
// This pure instruction should be eliminated.
bb0.instructions.push(MirInstruction::Const {
dst: v_dead,
value: ConstValue::Integer(999),
});
bb0.instruction_spans.push(Span::unknown());
// SSOT: jump_args is a semantic use-site (ExitLine, continuation args).
bb0.jump_args = Some(vec![v2]);
}
module.add_function(func);
let eliminated = eliminate_dead_code(&mut module);
assert_eq!(eliminated, 1);
let func = module.get_function("test/0").unwrap();
let bb0 = func.blocks.get(&BasicBlockId(0)).unwrap();
// Contract: spans must stay aligned with instructions.
assert_eq!(bb0.instructions.len(), bb0.instruction_spans.len());
// Contract: values that appear only in jump_args must be kept (and their deps).
assert!(bb0
.instructions
.iter()
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == v2)));
assert!(bb0
.instructions
.iter()
.any(|inst| matches!(inst, MirInstruction::Const { dst, .. } if *dst == v1)));
// The unused const must be eliminated.
assert!(!bb0
.instructions
.iter()
.any(|inst| matches!(inst, MirInstruction::Const { dst, .. } if *dst == v_dead)));
}
}