diff --git a/apps/tests/phase132_loop_true_break_once_post_add_min.hako b/apps/tests/phase132_loop_true_break_once_post_add_min.hako new file mode 100644 index 00000000..64a06350 --- /dev/null +++ b/apps/tests/phase132_loop_true_break_once_post_add_min.hako @@ -0,0 +1,26 @@ +// Phase 132-P4: loop(true) break-once with minimal post-loop computation +// +// Purpose: Test loop(true) { * ; break }; ; return in Normalized shadow +// Expected output: 3 +// +// Structure: +// x = 0 // pre-loop init +// loop(true) { // condition is Bool literal true +// x = 1 // body assignment +// break // break at end +// } +// x = x + 2 // post-loop assignment +// return x // return updated value (1 + 2 = 3) + +static box Main { + main() { + local x + x = 0 + loop(true) { + x = 1 + break + } + x = x + 2 + return x + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs index 23f69ce3..bf8134a3 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs @@ -36,3 +36,4 @@ pub(in crate::mir::builder) mod loop_true_read_digits_policy; pub(in crate::mir::builder) mod balanced_depth_scan_policy; pub(in crate::mir::builder) mod balanced_depth_scan_policy_box; pub(in crate::mir::builder) mod post_loop_early_return_plan; +pub(in crate::mir::builder) mod normalized_shadow_suffix_router_box; // Phase 132 P0.5 diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs b/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs new file mode 100644 index 00000000..0afab296 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs @@ -0,0 +1,375 @@ +//! Phase 132 P0.5: Suffix router box for loop(true) + post statements +//! +//! ## Responsibility +//! +//! - Detect block suffix starting with loop(true) { ... break } + post statements +//! - Lower the entire suffix to Normalized JoinModule via StepTree +//! - Return consumed count to skip processed statements in build_block() +//! +//! ## Contract +//! +//! - Returns Ok(Some(consumed)): Successfully processed remaining[..consumed] +//! - Returns Ok(None): Pattern not matched, use default behavior +//! - Returns Err(_): Internal error +//! +//! ## Design +//! +//! - Uses existing StepTree infrastructure (no StepNode modification) +//! - Block SSOT: StepTree already supports Block([Loop, Assign, Return]) +//! - Responsibility separation: suffix router = detection + conversion, build_block = wiring + +use crate::ast::ASTNode; +use crate::mir::builder::MirBuilder; +use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox; +use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox; +use crate::mir::control_tree::StepTreeBuilderBox; +use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode}; +use crate::mir::join_ir::lowering::inline_boundary::{JoinInlineBoundary, LoopExitBinding}; + +/// Box-First: Suffix router for normalized shadow lowering +pub struct NormalizedShadowSuffixRouterBox; + +impl NormalizedShadowSuffixRouterBox { + /// Try to lower a block suffix starting with loop(true) + post statements + /// + /// Returns: + /// - Ok(Some(consumed)): Successfully processed remaining[..consumed] + /// - Ok(None): Pattern not matched, use default behavior + /// - Err(_): Internal error + pub fn try_lower_loop_suffix( + builder: &mut MirBuilder, + remaining: &[ASTNode], + func_name: &str, + debug: bool, + ) -> Result, String> { + // Phase 132 P0.5 CRITICAL: Only match suffixes with POST-loop statements! + // + // This function is called on EVERY remaining block suffix, including: + // - Phase 131: [Loop, Return] ← Should go through normal routing (NOT a suffix) + // - Phase 132: [Loop, Assign, Return] ← This is our target (suffix with post-computation) + // + // We MUST NOT match Phase 131 patterns, as they're already handled correctly + // by try_normalized_shadow() in routing.rs. + + // Quick pattern check: need at least loop + assign + return (3 statements minimum) + if remaining.len() < 3 { + return Ok(None); + } + + // Check if first statement is a loop + let is_loop = matches!(&remaining[0], ASTNode::Loop { .. }); + if !is_loop { + return Ok(None); + } + + // Check if there's an assignment after the loop (Phase 132-specific) + let has_post_assignment = matches!(&remaining[1], ASTNode::Assignment { .. }); + if !has_post_assignment { + return Ok(None); // Not Phase 132, let normal routing handle it + } + + // Suffix requires an explicit return statement (the router consumes it) + let return_stmt = match &remaining[2] { + ASTNode::Return { .. } => remaining[2].clone(), + _ => return Ok(None), + }; + + // Phase 132 P0 pattern detection: + // - remaining[0]: Loop(true) { ... break } + // - remaining[1]: Assign (post-loop computation) + // - remaining[2]: Return + // + // Let StepTree handle the pattern validation (condition = true, break at end, etc.) + + let trace = crate::mir::builder::control_flow::joinir::trace::trace(); + trace.routing( + "suffix_router", + func_name, + &format!( + "Detected potential loop suffix: {} statements starting with Loop", + remaining.len() + ), + ); + + // Build StepTree from the reachable suffix (up to and including the return). + // + // Even if the AST block contains extra statements after return, they are unreachable + // and must not affect the structural decision. + let suffix = &remaining[..3]; + let step_tree = StepTreeBuilderBox::build_from_block(suffix); + + // Collect available inputs from MirBuilder state + let available_inputs = AvailableInputsCollectorBox::collect(builder, None); + + trace.routing( + "suffix_router/normalized", + func_name, + &format!( + "Trying Normalized shadow lowering (available_inputs: {})", + available_inputs.len() + ), + ); + + // Try Normalized lowering (loop(true) break-once with post statements) + match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&step_tree, &available_inputs) { + Ok(Some((join_module, join_meta))) => { + trace.routing( + "suffix_router/normalized", + func_name, + &format!( + "Normalized lowering succeeded ({} functions, {} exit bindings)", + join_module.functions.len(), + join_meta.exit_meta.exit_values.len() + ), + ); + + // Phase 132 P0.5: Merge the JoinModule into MIR + Self::merge_normalized_joinir( + builder, + join_module, + join_meta, + func_name, + debug, + )?; + + // Phase 132 P1: Emit host-level return for the consumed suffix. + // + // JoinIR merge converts fragment Returns to Jump(exit_block_id) to keep the + // “single-exit block” merge invariant. For suffix routing, we must still + // terminate the host function at this point. + if let ASTNode::Return { value, .. } = return_stmt { + let _ = builder.build_return_statement(value)?; + } + + // Return consumed count (loop + post-assign + return) + Ok(Some(3)) + } + Ok(None) => { + // Out of scope (not a Normalized pattern) + trace.routing( + "suffix_router/normalized", + func_name, + "Normalized lowering: out of scope", + ); + Ok(None) + } + Err(e) => { + // In scope but failed + let msg = format!( + "Phase 132/suffix_router: Failed to lower loop suffix in '{}': {}", + func_name, e + ); + if crate::config::env::joinir_dev::strict_enabled() { + use crate::mir::join_ir::lowering::error_tags; + return Err(error_tags::freeze_with_hint( + "phase132/suffix_router/internal", + &e, + "Loop suffix should be supported by Normalized but conversion failed. \ + Check that pattern matches loop(true) { ... break } + post statements.", + )); + } + trace.routing("suffix_router/normalized/error", func_name, &msg); + Ok(None) // Fallback to default behavior in non-strict mode + } + } + } + + /// Merge Normalized JoinModule into MIR builder (Phase 132 P0.5) + /// + /// This is the shared merge logic extracted from routing.rs + fn merge_normalized_joinir( + builder: &mut MirBuilder, + join_module: crate::mir::join_ir::JoinModule, + join_meta: crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta, + func_name: &str, + debug: bool, + ) -> Result<(), String> { + use crate::mir::builder::control_flow::joinir::merge; + use crate::mir::join_ir::frontend::JoinFuncMetaMap; + use crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir_with_meta; + use std::collections::BTreeMap; + + let trace = crate::mir::builder::control_flow::joinir::trace::trace(); + + // Build exit_bindings from meta + let exit_bindings: Vec = join_meta + .exit_meta + .exit_values + .iter() + .map(|(carrier_name, join_exit_value)| { + // Get host_slot from variable_map + let host_slot = builder + .variable_ctx + .variable_map + .get(carrier_name) + .copied() + .unwrap_or_else(|| { + panic!( + "[Phase 132 P0.5] Carrier '{}' not in variable_map (available: {:?})", + carrier_name, + builder.variable_ctx.variable_map.keys().collect::>() + ) + }); + + LoopExitBinding { + carrier_name: carrier_name.clone(), + join_exit_value: *join_exit_value, + host_slot, + role: CarrierRole::LoopState, + } + }) + .collect(); + + // Create boundary with DirectValue mode + let mut boundary = JoinInlineBoundary::new_with_exit_bindings( + vec![], // No join_inputs for Normalized + vec![], // No host_inputs for Normalized + exit_bindings, + ); + boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; // Phase 132 P0.5: No PHI + boundary.continuation_func_ids = join_meta.continuation_funcs.clone(); + + // Bridge JoinIR to MIR + let empty_meta: JoinFuncMetaMap = BTreeMap::new(); + let mir_module = bridge_joinir_to_mir_with_meta(&join_module, &empty_meta) + .map_err(|e| format!("[suffix_router/normalized] MIR conversion failed: {:?}", e))?; + + // Merge with boundary - this populates MergeResult.remapped_exit_values + // and calls ExitLineOrchestrator with DirectValue mode + let _exit_phi_result = merge::merge_joinir_mir_blocks( + builder, + &mir_module, + Some(&boundary), + debug, + )?; + + trace.routing( + "suffix_router/normalized", + func_name, + &format!( + "Normalized merge + reconnection completed ({} exit bindings)", + boundary.exit_bindings.len() + ), + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, LiteralValue, Span}; + + #[test] + fn test_try_lower_loop_suffix_phase132_pattern() { + // This is an integration test that would require full MirBuilder setup + // For now, we test the pattern detection logic + + let span = Span::unknown(); + let remaining = vec![ + 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::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::BinaryOp { + left: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + operator: crate::ast::BinaryOperator::Add, + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(2), + span: span.clone(), + }), + span: span.clone(), + }), + span: span.clone(), + }, + ASTNode::Return { + value: Some(Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + })), + span: span.clone(), + }, + ]; + + // Pattern should be detected (at least 2 statements, first is Loop) + assert!(remaining.len() >= 2); + assert!(matches!(&remaining[0], ASTNode::Loop { .. })); + + // Full lowering test would require MirBuilder setup + // This validates the pattern structure + } + + #[test] + fn test_try_lower_loop_suffix_no_match_too_short() { + // Single statement - should not match + let span = Span::unknown(); + let remaining = vec![ + ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: span.clone(), + }), + body: vec![ASTNode::Break { span: span.clone() }], + span: span.clone(), + }, + ]; + + // Pattern should not match (need at least 2 statements) + assert!(remaining.len() < 2); + } + + #[test] + fn test_try_lower_loop_suffix_no_match_not_loop() { + // First statement is not a loop + let span = Span::unknown(); + let remaining = 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::Return { + value: Some(Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + })), + span: span.clone(), + }, + ]; + + // Pattern should not match (first statement is not Loop) + assert!(!matches!(&remaining[0], ASTNode::Loop { .. })); + } +} diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 9be56486..082927d6 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -181,7 +181,44 @@ impl super::MirBuilder { &format!("Processing {} statements", total), trace.is_enabled(), ); - for (idx, statement) in statements.into_iter().enumerate() { + + // Phase 132 P0.5: Use while loop instead of for loop to support suffix skipping + let mut idx = 0; + while idx < statements.len() { + // Phase 132 P0.5: Try suffix router (dev-only) + if crate::config::env::joinir_dev_enabled() { + let remaining = &statements[idx..]; + // Clone func_name to avoid borrow conflict + let func_name = self + .scope_ctx + .current_function + .as_ref() + .map(|f| f.signature.name.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let debug = trace.is_enabled(); + + use crate::mir::builder::control_flow::joinir::patterns::policies::normalized_shadow_suffix_router_box::NormalizedShadowSuffixRouterBox; + match NormalizedShadowSuffixRouterBox::try_lower_loop_suffix( + self, remaining, &func_name, debug + )? { + Some(consumed) => { + trace.emit_if( + "debug", + "build_block/suffix_router", + &format!("Suffix router consumed {} statements (including return), stopping block build", consumed), + debug, + ); + // Suffix router consumes a return statement and emits a host-level Return + // after the JoinIR merge. Break to avoid double-processing. + idx += consumed; + break; + } + None => { + // No match, proceed with normal statement build + } + } + } + trace.emit_if( "debug", "build_block", @@ -198,14 +235,16 @@ impl super::MirBuilder { ), trace.is_enabled(), ); - last_value = Some(self.build_statement(statement)?); + last_value = Some(self.build_statement(statements[idx].clone())?); + idx += 1; + // If the current block was terminated by this statement (e.g., return/throw), // do not emit any further instructions for this block. if is_current_block_terminated(self)? { trace.emit_if( "debug", "build_block", - &format!("Block terminated after statement {}", idx + 1), + &format!("Block terminated after statement {}", idx), trace.is_enabled(), ); break; diff --git a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs index a6c99d4d..b780cc8a 100644 --- a/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs +++ b/src/mir/control_tree/normalized_shadow/loop_true_break_once.rs @@ -1,26 +1,29 @@ -//! Phase 131 P0: loop(true) break-once Normalized lowering +//! Phase 131/132-P4: loop(true) break-once Normalized lowering //! //! ## Responsibility //! //! - Lower `loop(true) { * ; break }` to Normalized JoinModule //! - One-time execution loop (condition is always true, breaks immediately) //! - PHI-free: all state passing via env arguments + continuations +//! - Phase 132-P4: Support minimal post-loop computation (one assign + return) //! //! ## Contract //! -//! - Input: StepTree with loop(true) { body ; break } pattern +//! - Input: StepTree with loop(true) { body ; break } [; ] pattern //! - Output: JoinModule with: //! - main(env) → TailCall(loop_step, env) -//! - loop_step(env) → if true { TailCall(loop_body, env) } else { TailCall(k_exit, env) } +//! - loop_step(env) → TailCall(loop_body, env) // condition is always true //! - loop_body(env) → → TailCall(k_exit, env) -//! - k_exit(env) → Ret(env[x]) or TailCall(post_k, env) +//! - k_exit(env) → Ret(env[x]) or TailCall(post_k, env) // Phase 132-P4 +//! - post_k(env) → → Ret(env[x]) // Phase 132-P4 //! //! ## Scope //! //! - Condition: Bool literal `true` only //! - Body: Assign(int literal/var/add) + LocalDecl only (Phase 130 baseline) //! - Exit: Break at end of body only (no continue, no return in body) -//! - Post-loop: Simple return only (Phase 124-126 baseline) +//! - Post-loop (Phase 131): Simple return only +//! - Post-loop (Phase 132-P4): One assignment + return (reuse Phase 130's lower_assign_stmt) //! //! ## Fail-Fast //! @@ -52,10 +55,20 @@ impl LoopTrueBreakOnceBuilderBox { step_tree: &StepTree, env_layout: &EnvLayout, ) -> Result, String> { + // DEBUG: Log StepTree root structure + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase132/debug] StepTree root: {:?}", step_tree.root); + } + // Extract loop(true) pattern from root let (prefix_nodes, loop_node, post_nodes) = match Self::extract_loop_true_pattern(&step_tree.root) { Some(v) => v, - None => return Ok(None), // Not a loop(true) pattern + None => { + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase132/debug] extract_loop_true_pattern returned None"); + } + return Ok(None); // Not a loop(true) pattern + } }; // Verify condition is Bool(true) @@ -278,23 +291,38 @@ impl LoopTrueBreakOnceBuilderBox { dst: None, }); - // Phase 131 P2: ExitMeta SSOT (DirectValue) + // Phase 131/132-P4: 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. + // produced by the loop (and post-loop if present), not the k_exit parameter placeholders. // // Contract: exit_values keys == env_layout.writes, values == final JoinIR-side ValueIds. + // - Phase 131: Use env_loop_body values + // - Phase 132-P4: Use env_post_k values (computed after post-loop lowering) 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)); + + // Phase 132-P4: 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) + + // DEBUG: Log post_nodes structure + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase132/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), + } + } + } + + 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 { .. }, .. }); + + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase132/debug] has_post_computation = {}", has_post_computation); } // k_exit(env): handle post-loop or return @@ -303,7 +331,97 @@ impl LoopTrueBreakOnceBuilderBox { 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 has_post_computation { + // Phase 132-P4: 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 { + func: post_k_id, + args: k_exit_args, + k_next: None, + dst: None, + }); + + // post_k(env): → 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); + } + + // Lower post-loop return + let StepNode::Stmt { kind: StepStmtKind::Return { value_ast }, .. } = &post_nodes[1] else { + return Ok(None); + }; + if let Some(ast_handle) = value_ast { + if let Some(var_name) = Self::extract_variable_name(&ast_handle.0) { + if let Some(vid) = env_post_k.get(&var_name).copied() { + post_k_func.body.push(JoinInst::Ret { value: Some(vid) }); + } else { + return Ok(None); // Variable not in env + } + } else { + return Ok(None); // Return value is not a variable + } + } else { + post_k_func.body.push(JoinInst::Ret { value: None }); + } + + // Phase 132-P4: 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", + &format!("post_k env missing final value for write '{var_name}'"), + "ensure post-loop assignments update the env map before exit meta is computed", + ) + })?; + exit_values_for_meta.push((var_name.clone(), final_vid)); + } + + // Build module with post_k + let mut module = JoinModule::new(); + module.add_function(main_func); + module.add_function(loop_step_func); + module.add_function(loop_body_func); + module.add_function(k_exit_func); + module.add_function(post_k_func); + module.entry = Some(main_id); + + // Phase 132-P4 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()); + for (id, func) in &module.functions { + eprintln!("[phase132/debug] Function {}: {} ({} instructions)", id.0, func.name, func.body.len()); + } + } + + let exit_meta = ExitMeta { + exit_values: exit_values_for_meta, + }; + let mut meta = JoinFragmentMeta::carrier_only(exit_meta); + meta.continuation_funcs.insert(k_exit_id); + + return Ok(Some((module, meta))); + } + + // Phase 131: Handle simple post-loop or no post-loop if post_nodes.is_empty() { // No post-loop: return void k_exit_func.body.push(JoinInst::Ret { value: None }); @@ -337,10 +455,23 @@ impl LoopTrueBreakOnceBuilderBox { } } } else { - return Ok(None); // Multiple post statements not supported + return Ok(None); // Unsupported post pattern } - // Build module + // Phase 131: ExitMeta uses loop_body'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_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)); + } + + // Build module (Phase 131) let mut module = JoinModule::new(); module.add_function(main_func); module.add_function(loop_step_func); @@ -353,7 +484,8 @@ impl LoopTrueBreakOnceBuilderBox { let exit_meta = ExitMeta { exit_values: exit_values_for_meta, }; - let meta = JoinFragmentMeta::carrier_only(exit_meta); + let mut meta = JoinFragmentMeta::carrier_only(exit_meta); + meta.continuation_funcs.insert(k_exit_id); Ok(Some((module, meta))) } diff --git a/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_llvm_exe.sh new file mode 100644 index 00000000..c2052dca --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_llvm_exe.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Phase 132-P4: loop(true) break-once with post-loop computation (LLVM EXE parity) +# Pattern: loop(true) { x = 1; break }; x = x + 2; return x (should be 3) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +llvm_exe_preflight_or_skip || exit 0 + +# Phase 132-P4 is a dev-only Normalized shadow loop + post case. +# LLVM EXE emission must run with JoinIR dev/strict enabled, otherwise it will freeze. +require_joinir_dev + +# Phase 132-P4: minimal plugin set (StringBox, ConsoleBox, IntegerBox only) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +CONSOLEBOX_SO="$NYASH_ROOT/plugins/nyash-console-plugin/libnyash_console_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" + +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "ConsoleBox|$CONSOLEBOX_SO|nyash-console-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase132_loop_true_break_once_post_add_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase132_loop_true_break_once_post_add_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase132_loop_true_break_once_post_add_llvm_exe" + +EXPECTED_EXIT_CODE=3 +LLVM_BUILD_LOG="/tmp/phase132_loop_true_break_once_post_add_build.log" +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase132_loop_true_break_once_post_add_llvm_exe: exit code matches expected (3)" +else + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_vm.sh new file mode 100644 index 00000000..74890ad2 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase132_loop_true_break_once_post_add_vm.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Phase 132-P4: loop(true) break-once with post-loop computation (Normalized shadow, VM) +# +# Verifies that loop(true) { * ; break }; ; return works in Normalized: +# - x = 0 → loop(true) { x = 1; break } → x = x + 2 → return x → 3 +# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Phase 132-P4 is a dev-only Normalized shadow loop + post case. +require_joinir_dev + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 132-P4: loop(true) break-once with post-loop computation (Normalized shadow, VM)" + +# Test 1: phase132_loop_true_break_once_post_add_min.hako +echo "[INFO] Test 1: phase132_loop_true_break_once_post_add_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase132_loop_true_break_once_post_add_min.hako" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 3 ]; then + # Phase 132-P4: expected output is exit code 3 (1 + 2) + echo "[PASS] exit code verified: 3" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 3)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase132_loop_true_break_once_post_add_vm: All tests passed" + exit 0 +else + test_fail "phase132_loop_true_break_once_post_add_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi