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:
@ -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::error_tags;
|
||||
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 std::collections::BTreeMap;
|
||||
|
||||
@ -126,16 +127,18 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
|
||||
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 loop_step_id = JoinFuncId::new(1);
|
||||
let loop_body_id = JoinFuncId::new(2);
|
||||
let k_exit_id = JoinFuncId::new(3);
|
||||
let k_exit_id = JoinFuncId::new(2);
|
||||
let loop_body_id = JoinFuncId::new(3);
|
||||
|
||||
// main(env): <prefix> → TailCall(loop_step, env)
|
||||
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 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
|
||||
for n in prefix_nodes {
|
||||
@ -174,35 +177,28 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
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 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);
|
||||
|
||||
// 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 mut loop_step_func =
|
||||
JoinFunction::new(loop_step_id, "join_func_1".to_string(), loop_step_params);
|
||||
let loop_step_args = collect_env_args(&env_fields, &env_loop_step)?;
|
||||
loop_step_func.body.push(JoinInst::Jump {
|
||||
cont: loop_body_id.as_cont(),
|
||||
args: loop_step_args.clone(),
|
||||
cond: Some(cond_vid),
|
||||
});
|
||||
loop_step_func.body.push(JoinInst::Jump {
|
||||
cont: k_exit_id.as_cont(),
|
||||
loop_step_func.body.push(JoinInst::Call {
|
||||
func: loop_body_id,
|
||||
args: loop_step_args,
|
||||
cond: None,
|
||||
k_next: None,
|
||||
dst: None,
|
||||
});
|
||||
|
||||
// loop_body(env): <assign statements> → TailCall(k_exit, env)
|
||||
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 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
|
||||
for n in body_prefix {
|
||||
@ -234,6 +230,47 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
|
||||
// loop_body → k_exit tailcall
|
||||
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 {
|
||||
func: k_exit_id,
|
||||
args: loop_body_args,
|
||||
@ -241,10 +278,30 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
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
|
||||
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 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
|
||||
if post_nodes.is_empty() {
|
||||
@ -290,9 +347,15 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
module.add_function(loop_body_func);
|
||||
module.add_function(k_exit_func);
|
||||
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
|
||||
@ -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 parity_contract;
|
||||
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 contracts::{CapabilityCheckResult, UnsupportedCapability};
|
||||
pub use parity_contract::{MismatchKind, ShadowParityResult};
|
||||
pub use env_layout::EnvLayout;
|
||||
pub use exit_reconnector::ExitReconnectorBox; // Phase 131 P1.5
|
||||
|
||||
Reference in New Issue
Block a user