refactor(mir): phase260 p0 edge-args plumbing (strangler) + ssot api + docs

This commit is contained in:
2025-12-21 04:34:22 +09:00
parent 4496b6243d
commit 4dfe3349bf
42 changed files with 1044 additions and 187 deletions

View File

@ -0,0 +1,14 @@
//! Entry function helpers (SSOT)
use crate::mir::join_ir::{JoinFunction, JoinModule};
/// Extract entry function from JoinModule (SSOT).
///
/// Priority: `join_module.entry` → fallback to `"main"`.
pub(in crate::mir::builder) fn get_entry_function<'a>(
join_module: &'a JoinModule,
context: &str,
) -> Result<&'a JoinFunction, String> {
// Re-exported from patterns/common as the SSOT entry point.
super::super::patterns::common::get_entry_function(join_module, context)
}

View File

@ -0,0 +1,14 @@
//! JoinIR integration API (SSOT entry points)
//!
//! Purpose: provide a small, stable surface for pattern lowerers and merge code.
//! This reduces "where should I call this from?" drift and avoids re-implementing
//! contract logic (SSOT, fail-fast checks) in each pattern.
//!
//! Policy:
//! - Prefer SSOT helpers over ad-hoc logic in patterns.
//! - Avoid guessing (order/layout/name) in callers; callers pass explicit intent.
//! - Keep this module thin: mostly wrappers/re-exports with clear naming.
pub(in crate::mir::builder) mod entry;
pub(in crate::mir::builder) mod pipeline_contracts;
pub(in crate::mir::builder) mod receiver;

View File

@ -0,0 +1,16 @@
//! Pipeline contract helpers (SSOT)
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::join_ir::JoinModule;
/// Run all JoinIR pipeline contract checks (SSOT).
///
/// This is the preferred entry point for patterns that need to validate boundary
/// assumptions before bridge/merge.
pub(in crate::mir::builder) fn run_all_pipeline_checks(
join_module: &JoinModule,
boundary: &JoinInlineBoundary,
) -> Result<(), String> {
super::super::merge::contract_checks::run_all_pipeline_checks(join_module, boundary)
}

View File

@ -0,0 +1,20 @@
//! Receiver helpers (SSOT)
//!
//! Motivation: surface syntax uses `this` / `me`, but the MIR builder's `variable_map`
//! registers the receiver under a specific key. Patterns must not guess that key.
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
/// Return the host ValueId for `me` receiver (SSOT).
///
/// Patterns should use this instead of hardcoding `"me"` / `"this"`.
pub(in crate::mir::builder) fn get_me_host_id(builder: &MirBuilder, context: &str) -> Result<ValueId, String> {
builder
.variable_ctx
.variable_map
.get("me")
.copied()
.ok_or_else(|| format!("[{}] receiver 'me' not found in variable_map", context))
}

View File

@ -33,7 +33,7 @@ pub(super) fn verify_all_terminator_targets_exist(
let Some(term) = &block.terminator else { continue };
match term {
MirInstruction::Jump { target } => {
MirInstruction::Jump { target, .. } => {
if !func.blocks.contains_key(target)
&& !contracts.allowed_missing_jump_targets.contains(target)
{
@ -749,13 +749,21 @@ mod tests {
let bb2 = BasicBlockId(2);
let func = create_test_function(vec![
(bb0, Some(MirInstruction::Jump { target: bb1 })),
(
bb0,
Some(MirInstruction::Jump {
target: bb1,
edge_args: None,
}),
),
(
bb1,
Some(MirInstruction::Branch {
condition: ValueId(0),
then_bb: bb2,
else_bb: bb2,
then_edge_args: None,
else_edge_args: None,
}),
),
(bb2, Some(MirInstruction::Return { value: None })),
@ -775,7 +783,13 @@ mod tests {
let bb0 = BasicBlockId(0);
let bb99 = BasicBlockId(99); // Missing block
let func = create_test_function(vec![(bb0, Some(MirInstruction::Jump { target: bb99 }))]);
let func = create_test_function(vec![(
bb0,
Some(MirInstruction::Jump {
target: bb99,
edge_args: None,
}),
)]);
let contracts = MergeContracts {
allowed_missing_jump_targets: vec![],
@ -795,7 +809,13 @@ mod tests {
let bb0 = BasicBlockId(0);
let bb_exit = BasicBlockId(100); // Missing but allowed
let func = create_test_function(vec![(bb0, Some(MirInstruction::Jump { target: bb_exit }))]);
let func = create_test_function(vec![(
bb0,
Some(MirInstruction::Jump {
target: bb_exit,
edge_args: None,
}),
)]);
let contracts = MergeContracts {
allowed_missing_jump_targets: vec![bb_exit],

View File

@ -391,7 +391,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;
let mut k_exit_edge_args: Option<crate::mir::EdgeArgs> = None;
// Phase 177-3: Check if this block is the loop header with PHI nodes
let is_loop_header_with_phi =
@ -488,7 +488,10 @@ 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();
if let Some(b) = boundary {
k_exit_edge_args =
old_block.legacy_edge_args_with_layout(b.jump_args_layout);
}
found_tail_call = true;
if debug {
log!(
@ -554,6 +557,8 @@ pub(super) fn merge_and_rewrite(
condition,
then_bb,
else_bb,
then_edge_args,
else_edge_args,
} => {
let remapped_then = skipped_entry_redirects
.get(&then_bb)
@ -569,6 +574,8 @@ pub(super) fn merge_and_rewrite(
condition,
then_bb: remapped_then,
else_bb: remapped_else,
then_edge_args,
else_edge_args,
}
}
MirInstruction::Phi { dst, inputs, type_hint } => {
@ -950,8 +957,9 @@ pub(super) fn merge_and_rewrite(
}
};
new_block.terminator = Some(MirInstruction::Jump {
new_block.set_terminator(MirInstruction::Jump {
target: actual_target,
edge_args: None,
});
// DEBUG: Print final state after adding parameter bindings
@ -979,7 +987,18 @@ pub(super) fn merge_and_rewrite(
// Remap terminator (convert Return → Jump to exit) if not already set by tail call
if !found_tail_call {
if let Some(ref term) = old_block.terminator {
let remapped_term = match term {
let remap_edge_args = |edge_args: &Option<crate::mir::EdgeArgs>| {
edge_args.as_ref().map(|args| crate::mir::EdgeArgs {
layout: args.layout,
values: args
.values
.iter()
.map(|&v| remapper.remap_value(v))
.collect(),
})
};
let mut remapped_term: Option<MirInstruction> = None;
match term {
MirInstruction::Return { value } => {
// Convert Return to Jump to exit block
// All functions return to same exit block (Phase 189)
@ -988,43 +1007,54 @@ pub(super) fn merge_and_rewrite(
//
// The JoinIR Jump instruction passes ALL carrier values in its args,
// but the JoinIR→MIR conversion in joinir_block_converter only preserved
// the first arg in the Return value. We now use the jump_args metadata
// the first arg in the Return value. We now use the legacy jump_args metadata
// to recover all the original Jump args.
//
if let Some(_ret_val) = value {
// Phase 246-EX: Check if this block has jump_args metadata
if let Some(ref jump_args) = old_block.jump_args {
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Block {:?} has jump_args metadata: {:?}",
old_block.id, jump_args
);
let mut exit_edge_args: Option<crate::mir::EdgeArgs> = None;
if value.is_some() {
if let Some(b) = boundary {
// Phase 246-EX: Check if this block has legacy jump_args metadata
if let Some(edge_args) =
old_block.legacy_edge_args_with_layout(b.jump_args_layout)
{
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Block {:?} has legacy jump_args metadata: {:?}",
old_block.id, edge_args.values
);
// The jump_args are in JoinIR value space, remap them to HOST
let remapped_args: Vec<ValueId> = jump_args
.iter()
.map(|&arg| remapper.remap_value(arg))
.collect();
// The jump_args are in JoinIR value space, remap them to HOST
let remapped_args: Vec<ValueId> = edge_args
.values
.iter()
.map(|&arg| remapper.remap_value(arg))
.collect();
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Remapped jump_args: {:?}",
remapped_args
);
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Remapped jump_args: {:?}",
remapped_args
);
// Phase 118 P2: Use ExitArgsCollectorBox to collect exit values
let edge_args = crate::mir::EdgeArgs {
layout: edge_args.layout,
values: remapped_args,
};
exit_edge_args = Some(edge_args.clone());
// Phase 118 P2: Use ExitArgsCollectorBox to collect exit values
if let Some(b) = boundary {
let collector = ExitArgsCollectorBox::new();
let collection_result = collector.collect(
&b.exit_bindings,
&remapped_args,
&edge_args.values,
new_block_id,
strict_exit,
b.jump_args_layout,
edge_args.layout,
)?;
// Add expr_result to exit_phi_inputs (if present)
if let Some(expr_result_val) = collection_result.expr_result_value {
if let Some(expr_result_val) =
collection_result.expr_result_value
{
exit_phi_inputs.push((new_block_id, expr_result_val));
log!(
verbose,
@ -1034,7 +1064,9 @@ pub(super) fn merge_and_rewrite(
}
// Add carrier values to carrier_inputs
for (carrier_name, (block_id, value_id)) in collection_result.carrier_values {
for (carrier_name, (block_id, value_id)) in
collection_result.carrier_values
{
carrier_inputs
.entry(carrier_name.clone())
.or_insert_with(Vec::new)
@ -1045,16 +1077,14 @@ pub(super) fn merge_and_rewrite(
carrier_name, block_id, value_id
);
}
}
} else {
// Fallback: Use header PHI dst (old behavior for blocks without jump_args)
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Block {:?} has NO jump_args, using header PHI fallback",
old_block.id
);
} else {
// Fallback: Use header PHI dst (old behavior for blocks without jump_args)
log!(
verbose,
"[DEBUG-177] Phase 246-EX: Block {:?} has NO jump_args, using header PHI fallback",
old_block.id
);
if let Some(b) = boundary {
if let Some(loop_var_name) = &b.loop_var_name {
if let Some(phi_dst) =
loop_header_phi_info.get_carrier_phi(loop_var_name)
@ -1102,11 +1132,16 @@ pub(super) fn merge_and_rewrite(
}
}
MirInstruction::Jump {
target: exit_block_id,
if let Some(edge_args) = exit_edge_args {
new_block.set_jump_with_edge_args(exit_block_id, Some(edge_args));
} else {
new_block.set_terminator(MirInstruction::Jump {
target: exit_block_id,
edge_args: None,
});
}
}
MirInstruction::Jump { target } => {
MirInstruction::Jump { target, edge_args } => {
// Phase 189 FIX: Remap block ID for Jump
// Phase 259 P0 FIX: Check skipped_entry_redirects first (for k_exit blocks)
let remapped_target = skipped_entry_redirects
@ -1114,14 +1149,17 @@ pub(super) fn merge_and_rewrite(
.or_else(|| local_block_map.get(target))
.copied()
.unwrap_or(*target);
MirInstruction::Jump {
remapped_term = Some(MirInstruction::Jump {
target: remapped_target,
}
edge_args: remap_edge_args(edge_args),
});
}
MirInstruction::Branch {
condition,
then_bb,
else_bb,
then_edge_args,
else_edge_args,
} => {
// Phase 189 FIX: Remap block IDs AND condition ValueId for Branch
// Phase 259 P0 FIX: Check skipped_entry_redirects first (for k_exit blocks)
@ -1135,15 +1173,46 @@ pub(super) fn merge_and_rewrite(
.or_else(|| local_block_map.get(else_bb))
.copied()
.unwrap_or(*else_bb);
MirInstruction::Branch {
remapped_term = Some(MirInstruction::Branch {
condition: remapper.remap_value(*condition),
then_bb: remapped_then,
else_bb: remapped_else,
}
then_edge_args: remap_edge_args(then_edge_args),
else_edge_args: remap_edge_args(else_edge_args),
});
}
_ => remapper.remap_instruction(term),
};
new_block.terminator = Some(remapped_term);
_ => remapped_term = Some(remapper.remap_instruction(term)),
}
if let Some(term) = remapped_term {
match term {
MirInstruction::Jump { target, edge_args } => {
if edge_args.is_some() {
new_block.set_jump_with_edge_args(target, edge_args);
} else {
new_block.set_terminator(MirInstruction::Jump {
target,
edge_args: None,
});
}
}
MirInstruction::Branch {
condition,
then_bb,
then_edge_args,
else_bb,
else_edge_args,
} => {
new_block.set_branch_with_edge_args(
condition,
then_bb,
then_edge_args,
else_bb,
else_edge_args,
);
}
_ => new_block.set_terminator(term),
}
}
}
}
@ -1153,23 +1222,31 @@ pub(super) fn merge_and_rewrite(
match decision {
LoweringDecision::NormalizeToExitJump { args } => {
// Collect exit values from k_exit arguments
let mut exit_edge_args: Option<crate::mir::EdgeArgs> = None;
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 exit_values: Vec<ValueId> =
if let Some(ref edge_args) = k_exit_edge_args {
edge_args
.values
.iter()
.map(|&arg| remapper.remap_value(arg))
.collect()
} else {
args.clone()
};
let edge_args = crate::mir::EdgeArgs {
layout: b.jump_args_layout,
values: exit_values,
};
exit_edge_args = Some(edge_args.clone());
let collection_result =
collector.collect(
&b.exit_bindings,
&exit_args,
&edge_args.values,
new_block_id,
strict_exit,
b.jump_args_layout,
edge_args.layout,
)?;
if let Some(expr_result_value) = collection_result.expr_result_value {
exit_phi_inputs.push((collection_result.block_id, expr_result_value));
@ -1189,8 +1266,17 @@ pub(super) fn merge_and_rewrite(
}
// Generate exit jump using policy box
let exit_jump = tail_call_policy.rewrite_to_exit_jump(exit_block_id);
new_block.terminator = Some(exit_jump.clone());
let exit_jump = if let Some(edge_args) = exit_edge_args {
new_block.set_jump_with_edge_args(exit_block_id, Some(edge_args));
new_block.terminator.clone().unwrap_or(MirInstruction::Jump {
target: exit_block_id,
edge_args: None,
})
} else {
let exit_jump = tail_call_policy.rewrite_to_exit_jump(exit_block_id);
new_block.set_terminator(exit_jump.clone());
exit_jump
};
// Strict mode: verify the generated terminator matches contract
if strict_exit {

View File

@ -361,7 +361,7 @@ impl LoopHeaderPhiBuilder {
);
}
}
crate::mir::MirInstruction::Jump { target } => {
crate::mir::MirInstruction::Jump { target, .. } => {
block.successors.insert(*target);
if dev_debug {
trace.stderr_if(

View File

@ -96,6 +96,7 @@ impl TailCallLoweringPolicyBox {
pub fn rewrite_to_exit_jump(&self, exit_block_id: BasicBlockId) -> MirInstruction {
MirInstruction::Jump {
target: exit_block_id,
edge_args: None,
}
}
@ -121,8 +122,8 @@ impl TailCallLoweringPolicyBox {
exit_block_id: BasicBlockId,
) -> Result<(), String> {
match terminator {
MirInstruction::Jump { target } if *target == exit_block_id => Ok(()),
MirInstruction::Jump { target } => Err(crate::mir::join_ir::lowering::error_tags::freeze_with_hint(
MirInstruction::Jump { target, .. } if *target == exit_block_id => Ok(()),
MirInstruction::Jump { target, .. } => Err(crate::mir::join_ir::lowering::error_tags::freeze_with_hint(
"phase131/k_exit/wrong_jump_target",
&format!(
"skippable continuation tail call lowered to Jump {:?}, expected exit_block_id {:?}",
@ -184,7 +185,7 @@ mod tests {
let jump = policy.rewrite_to_exit_jump(exit_block);
assert!(matches!(
jump,
MirInstruction::Jump { target } if target == exit_block
MirInstruction::Jump { target, .. } if target == exit_block
));
}
@ -194,6 +195,7 @@ mod tests {
let exit_block = BasicBlockId(42);
let jump = MirInstruction::Jump {
target: exit_block,
edge_args: None,
};
let result = policy.verify_exit_jump(&jump, exit_block);
@ -206,6 +208,7 @@ mod tests {
let exit_block = BasicBlockId(42);
let wrong_jump = MirInstruction::Jump {
target: BasicBlockId(99),
edge_args: None,
};
let result = policy.verify_exit_jump(&wrong_jump, exit_block);

View File

@ -33,7 +33,7 @@ fn case_a_pure_k_exit_return_is_skippable() {
let mut func = make_function("join_func_2");
let block = func.blocks.get_mut(&func.entry_block).unwrap();
block.instructions.clear();
block.terminator = Some(MirInstruction::Return { value: None });
block.set_terminator(MirInstruction::Return { value: None });
assert!(is_skippable_continuation(&func));
}
@ -52,8 +52,9 @@ fn case_b_k_exit_tailcall_post_k_is_not_skippable() {
args: vec![],
effects: EffectMask::CONTROL,
});
block.terminator = Some(MirInstruction::Jump {
block.set_terminator(MirInstruction::Jump {
target: BasicBlockId(1),
edge_args: None,
});
assert!(!is_skippable_continuation(&func));
}
@ -67,14 +68,15 @@ fn case_c_multi_block_continuation_is_not_skippable() {
// Add a second block
let second_block_id = BasicBlockId(1);
let mut second_block = crate::mir::BasicBlock::new(second_block_id);
second_block.terminator = Some(MirInstruction::Return { value: None });
second_block.set_terminator(MirInstruction::Return { value: None });
func.blocks.insert(second_block_id, second_block);
// Entry block jumps to second block
let entry_block = func.blocks.get_mut(&func.entry_block).unwrap();
entry_block.instructions.clear();
entry_block.terminator = Some(MirInstruction::Jump {
entry_block.set_terminator(MirInstruction::Jump {
target: second_block_id,
edge_args: None,
});
assert!(!is_skippable_continuation(&func));
@ -91,6 +93,6 @@ fn case_d_continuation_with_instructions_is_not_skippable() {
dst: ValueId(0),
value: crate::mir::types::ConstValue::Integer(42),
});
block.terminator = Some(MirInstruction::Return { value: None });
block.set_terminator(MirInstruction::Return { value: None });
assert!(!is_skippable_continuation(&func));
}

View File

@ -10,6 +10,7 @@
//! - Control tree capability guard (control_tree_capability_guard.rs) ✅ Phase 112
pub(in crate::mir::builder) mod control_tree_capability_guard;
pub(in crate::mir::builder) mod api;
pub(in crate::mir::builder) mod legacy; // Phase 132-R0 Task 4: Legacy routing isolation
pub(in crate::mir::builder) mod loop_context;
pub(in crate::mir::builder) mod merge;

View File

@ -18,6 +18,8 @@ pub fn emit_conditional(
condition: cond,
then_bb,
else_bb,
then_edge_args: None,
else_edge_args: None,
})
}
}
@ -28,6 +30,9 @@ pub fn emit_jump(b: &mut MirBuilder, target: BasicBlockId) -> Result<(), String>
crate::mir::ssot::cf_common::set_jump(func, cur_bb, target);
Ok(())
} else {
b.emit_instruction(MirInstruction::Jump { target })
b.emit_instruction(MirInstruction::Jump {
target,
edge_args: None,
})
}
}

View File

@ -102,7 +102,25 @@ impl JoinIrIdRemapper {
vals.extend(args.iter().copied());
vals
}
Branch { condition, .. } => vec![*condition],
Branch {
condition,
then_edge_args,
else_edge_args,
..
} => {
let mut vals = vec![*condition];
if let Some(args) = then_edge_args {
vals.extend(args.values.iter().copied());
}
if let Some(args) = else_edge_args {
vals.extend(args.values.iter().copied());
}
vals
}
Jump { edge_args, .. } => edge_args
.as_ref()
.map(|args| args.values.clone())
.unwrap_or_default(),
Return { value } => value.iter().copied().collect(),
Phi { dst, inputs, .. } => {
let mut vals = vec![*dst];
@ -177,8 +195,15 @@ impl JoinIrIdRemapper {
/// 命令を新しい ID空間にリマップ
pub fn remap_instruction(&self, inst: &MirInstruction) -> MirInstruction {
use crate::mir::MirInstruction::*;
use crate::mir::EdgeArgs;
let remap = |v: ValueId| self.value_map.get(&v).copied().unwrap_or(v);
let remap_edge_args = |edge_args: &Option<EdgeArgs>| {
edge_args.as_ref().map(|args| EdgeArgs {
layout: args.layout,
values: args.values.iter().map(|&v| remap(v)).collect(),
})
};
match inst {
Const { dst, value } => Const {
@ -431,8 +456,28 @@ impl JoinIrIdRemapper {
then_val: remap(*then_val),
else_val: remap(*else_val),
},
// Pass through unchanged (Branch/Jump/Return handled separately)
Branch { .. } | Jump { .. } | Return { .. } | Nop | Safepoint => inst.clone(),
Branch {
condition,
then_bb,
else_bb,
then_edge_args,
else_edge_args,
} => Branch {
condition: remap(*condition),
then_bb: *then_bb,
else_bb: *else_bb,
then_edge_args: remap_edge_args(then_edge_args),
else_edge_args: remap_edge_args(else_edge_args),
},
Jump { target, edge_args } => Jump {
target: *target,
edge_args: remap_edge_args(edge_args),
},
Return { value } => Return {
value: value.map(remap),
},
// Pass through unchanged
Nop | Safepoint => inst.clone(),
}
}