feat(control_tree): Phase 133 P0 - Multiple post-loop assigns support
Extend Phase 132's loop(true) + post-loop to accept multiple assignments:
Goal: `x=0; loop(true){ x=1; break }; x=x+2; x=x+3; return x` → exit code 6
Implementation:
- Extended loop_true_break_once.rs pattern detection (len() == 2 → len() >= 2)
- Added iterative assignment lowering (for loop over post_nodes)
- Reused Phase 130's lower_assign_stmt for each assignment
- Maintained ExitMeta DirectValue mode (PHI-free)
Changes:
- apps/tests/phase133_loop_true_break_once_post_multi_add_min.hako (new fixture)
- tools/smokes/v2/profiles/integration/apps/phase133_*_multi_add_*.sh (new smokes)
- src/mir/control_tree/normalized_shadow/loop_true_break_once.rs (+30 lines)
- docs/development/current/main/phases/phase-133/README.md (new documentation)
- docs/development/current/main/10-Now.md (Phase 133 entry added)
Scope (Phase 130 baseline):
- ✅ x = <int literal>
- ✅ x = y (variable copy)
- ✅ x = x + <int literal> (increment)
- ❌ Function calls / general expressions (future phases)
Design principles:
- Minimal change: ~30 lines added
- SSOT preservation: env_post_k remains single source of truth
- Reuse: Leveraged existing lower_assign_stmt
- Fail-Fast: Contract violations trigger freeze_with_hint
Test results:
- cargo test --lib: 1176 PASS
- Phase 133 VM: PASS (exit code 6)
- Phase 133 LLVM EXE: PASS (exit code 6)
- Phase 132 regression: PASS (exit code 3)
- Phase 131 regression: PASS (exit code 1)
- Phase 97 regression: PASS
Architecture maintained:
- 5-function structure unchanged (main/loop_step/loop_body/k_exit/post_k)
- PHI-free DirectValue mode
- Zero changes to ExitMeta, merge logic, or JoinIR contracts
Related: Phase 133 loop(true) + multiple post-loop assignments
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -24,6 +24,7 @@
|
||||
//! - Exit: Break at end of body only (no continue, no return in body)
|
||||
//! - Post-loop (Phase 131): Simple return only
|
||||
//! - Post-loop (Phase 132-P4): One assignment + return (reuse Phase 130's lower_assign_stmt)
|
||||
//! - Post-loop (Phase 133-P0): Multiple assignments + return (extend Phase 132-P4)
|
||||
//!
|
||||
//! ## Fail-Fast
|
||||
//!
|
||||
@ -301,28 +302,45 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
// - Phase 132-P4: Use env_post_k values (computed after post-loop lowering)
|
||||
use crate::mir::join_ir::lowering::carrier_info::ExitMeta;
|
||||
|
||||
// Phase 132-P4: Detect post-loop pattern
|
||||
// Phase 132-P4/133-P0: Detect post-loop pattern
|
||||
// - post_nodes.is_empty() → Phase 131: k_exit → Ret(void)
|
||||
// - post_nodes == [Return(var)] → Phase 131: k_exit → Ret(env[var])
|
||||
// - post_nodes == [Assign, Return(var)] → Phase 132-P4: k_exit → TailCall(post_k)
|
||||
// - post_nodes == [Assign+, Return(var)] → Phase 133-P0: k_exit → TailCall(post_k)
|
||||
|
||||
// DEBUG: Log post_nodes structure
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase132/debug] post_nodes.len() = {}", post_nodes.len());
|
||||
eprintln!("[phase133/debug] post_nodes.len() = {}", post_nodes.len());
|
||||
for (i, node) in post_nodes.iter().enumerate() {
|
||||
match node {
|
||||
StepNode::Stmt { kind, .. } => eprintln!("[phase132/debug] post_nodes[{}] = Stmt({:?})", i, kind),
|
||||
_ => eprintln!("[phase132/debug] post_nodes[{}] = {:?}", i, node),
|
||||
StepNode::Stmt { kind, .. } => eprintln!("[phase133/debug] post_nodes[{}] = Stmt({:?})", i, kind),
|
||||
_ => eprintln!("[phase133/debug] post_nodes[{}] = {:?}", i, node),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_post_computation = post_nodes.len() == 2
|
||||
&& matches!(&post_nodes[0], StepNode::Stmt { kind: StepStmtKind::Assign { .. }, .. })
|
||||
&& matches!(&post_nodes[1], StepNode::Stmt { kind: StepStmtKind::Return { .. }, .. });
|
||||
// Phase 133-P0: Detect multi-assign + return pattern (generalize Phase 132-P4)
|
||||
let has_post_computation = if post_nodes.is_empty() {
|
||||
false
|
||||
} else if post_nodes.len() >= 2 {
|
||||
// Check if all nodes except the last are Assign statements
|
||||
let all_assigns = post_nodes[..post_nodes.len() - 1]
|
||||
.iter()
|
||||
.all(|n| matches!(n, StepNode::Stmt { kind: StepStmtKind::Assign { .. }, .. }));
|
||||
|
||||
// Check if the last node is a Return statement
|
||||
let ends_with_return = matches!(
|
||||
post_nodes.last(),
|
||||
Some(StepNode::Stmt { kind: StepStmtKind::Return { .. }, .. })
|
||||
);
|
||||
|
||||
all_assigns && ends_with_return
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase132/debug] has_post_computation = {}", has_post_computation);
|
||||
eprintln!("[phase133/debug] has_post_computation = {}", has_post_computation);
|
||||
}
|
||||
|
||||
// k_exit(env): handle post-loop or return
|
||||
@ -332,7 +350,7 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
JoinFunction::new(k_exit_id, "join_func_2".to_string(), k_exit_params);
|
||||
|
||||
if has_post_computation {
|
||||
// Phase 132-P4: k_exit → TailCall(post_k, env)
|
||||
// Phase 132-P4/133-P0: k_exit → TailCall(post_k, env)
|
||||
let post_k_id = JoinFuncId::new(4);
|
||||
let k_exit_args = collect_env_args(&env_fields, &env_k_exit)?;
|
||||
k_exit_func.body.push(JoinInst::Call {
|
||||
@ -342,30 +360,37 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
dst: None,
|
||||
});
|
||||
|
||||
// post_k(env): <post assign> → Ret(env[x])
|
||||
// post_k(env): <post assign>* → Ret(env[x])
|
||||
let post_k_params = alloc_env_params(&env_fields, &mut next_value_id);
|
||||
let mut env_post_k = build_env_map(&env_fields, &post_k_params);
|
||||
let mut post_k_func =
|
||||
JoinFunction::new(post_k_id, "join_func_4".to_string(), post_k_params);
|
||||
|
||||
// Lower post-loop assignment
|
||||
let StepNode::Stmt { kind: StepStmtKind::Assign { target, value_ast }, .. } = &post_nodes[0] else {
|
||||
return Ok(None);
|
||||
};
|
||||
if LegacyLowerer::lower_assign_stmt(
|
||||
target,
|
||||
value_ast,
|
||||
&mut post_k_func.body,
|
||||
&mut next_value_id,
|
||||
&mut env_post_k,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Ok(None);
|
||||
// Phase 133-P0: Lower multiple post-loop assignments
|
||||
// Split post_nodes into assigns and return (last element is return)
|
||||
let assign_nodes = &post_nodes[..post_nodes.len() - 1];
|
||||
let return_node = post_nodes.last().unwrap();
|
||||
|
||||
// Lower all assignment statements
|
||||
for node in assign_nodes {
|
||||
let StepNode::Stmt { kind: StepStmtKind::Assign { target, value_ast }, .. } = node else {
|
||||
return Ok(None);
|
||||
};
|
||||
if LegacyLowerer::lower_assign_stmt(
|
||||
target,
|
||||
value_ast,
|
||||
&mut post_k_func.body,
|
||||
&mut next_value_id,
|
||||
&mut env_post_k,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
// Lower post-loop return
|
||||
let StepNode::Stmt { kind: StepStmtKind::Return { value_ast }, .. } = &post_nodes[1] else {
|
||||
let StepNode::Stmt { kind: StepStmtKind::Return { value_ast }, .. } = return_node else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(ast_handle) = value_ast {
|
||||
@ -382,12 +407,12 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
post_k_func.body.push(JoinInst::Ret { value: None });
|
||||
}
|
||||
|
||||
// Phase 132-P4: ExitMeta must use post_k's final env values
|
||||
// Phase 132-P4/133-P0: ExitMeta must use post_k's final env values
|
||||
let mut exit_values_for_meta: Vec<(String, ValueId)> = Vec::new();
|
||||
for var_name in &env_layout.writes {
|
||||
let final_vid = env_post_k.get(var_name).copied().ok_or_else(|| {
|
||||
error_tags::freeze_with_hint(
|
||||
"phase132/exit_meta/missing_final_value",
|
||||
"phase133/exit_meta/missing_final_value",
|
||||
&format!("post_k env missing final value for write '{var_name}'"),
|
||||
"ensure post-loop assignments update the env map before exit meta is computed",
|
||||
)
|
||||
@ -404,11 +429,11 @@ impl LoopTrueBreakOnceBuilderBox {
|
||||
module.add_function(post_k_func);
|
||||
module.entry = Some(main_id);
|
||||
|
||||
// Phase 132-P4 DEBUG: Verify all 5 functions are added
|
||||
// Phase 132-P4/133-P0 DEBUG: Verify all 5 functions are added
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase132/debug] JoinModule has {} functions (expected 5)", module.functions.len());
|
||||
eprintln!("[phase133/debug] JoinModule has {} functions (expected 5)", module.functions.len());
|
||||
for (id, func) in &module.functions {
|
||||
eprintln!("[phase132/debug] Function {}: {} ({} instructions)", id.0, func.name, func.body.len());
|
||||
eprintln!("[phase133/debug] Function {}: {} ({} instructions)", id.0, func.name, func.body.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user