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:
nyash-codex
2025-12-18 22:11:08 +09:00
parent 9fb35bbc05
commit ef71ea955c
6 changed files with 378 additions and 81 deletions

View File

@ -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());
}
}