diff --git a/apps/tests/phase142_loop_stmt_only_then_return_length_min.hako b/apps/tests/phase142_loop_stmt_only_then_return_length_min.hako new file mode 100644 index 00000000..1af9795f --- /dev/null +++ b/apps/tests/phase142_loop_stmt_only_then_return_length_min.hako @@ -0,0 +1,24 @@ +// Phase 142 P0: Loop normalization as single statement +// +// Purpose: Verify loop(true) normalization returns consumed=1, allowing +// subsequent statements (return s.length()) to be processed normally. +// +// Expected output: 3 (s="abc" → s.length() → 3) +// +// Structure: +// s = "abc" // pre-loop init with string +// loop(true) { // condition is Bool literal true +// break // break at end +// } +// return s.length() // subsequent statement processed normally + +static box Main { + main() { + local s + s = "abc" + loop(true) { + break + } + return s.length() + } +} diff --git a/src/mir/builder/control_flow/normalization/plan_box.rs b/src/mir/builder/control_flow/normalization/plan_box.rs index 01ed19ac..77e24e27 100644 --- a/src/mir/builder/control_flow/normalization/plan_box.rs +++ b/src/mir/builder/control_flow/normalization/plan_box.rs @@ -70,76 +70,17 @@ impl NormalizationPlanBox { return Ok(None); } - // Phase 131: Loop-only pattern (single statement) - if remaining.len() == 1 { - if debug { - trace.routing( - "normalization/plan", - func_name, - "Detected Phase 131 pattern: loop-only", - ); - } - return Ok(Some(NormalizationPlan::loop_only())); - } - - // Phase 132-135: Loop + (assign*) + return - // Count consecutive assignments after the loop - let mut post_assign_count = 0; - for i in 1..remaining.len() { - if matches!(&remaining[i], ASTNode::Assignment { .. }) { - post_assign_count += 1; - } else { - break; - } - } - - // After assignments (0 or more), we need a return statement - let return_index = 1 + post_assign_count; - if return_index >= remaining.len() { - // No statement after assignments - not a post pattern - if debug { - trace.routing( - "normalization/plan", - func_name, - &format!( - "No statement after {} assignments, not a valid post pattern", - post_assign_count - ), - ); - } - // If there's a non-assignment statement after loop but no return, treat as not a pattern - return Ok(None); - } - - let has_return = matches!(&remaining[return_index], ASTNode::Return { .. }); - if !has_return { - // Statement after loop (and optional assignments) is not return - not a post pattern - if debug { - trace.routing( - "normalization/plan", - func_name, - &format!( - "Statement after loop and {} assignments is not return, not a valid post pattern", - post_assign_count - ), - ); - } - return Ok(None); - } - - // Valid Phase 132-135 pattern: loop + N assignments (N >= 0) + return + // Phase 142 P0: Always return loop_only for loop(true), regardless of what follows + // Normalization unit is now "statement (loop 1個)" not "block suffix" + // Subsequent statements (return, assignments, etc.) handled by normal MIR lowering if debug { trace.routing( "normalization/plan", func_name, - &format!( - "Detected Phase 132-135 pattern: loop + {} post assigns + return", - post_assign_count - ), + "Detected loop(true) - Phase 142 P0: returning loop_only (consumed=1)", ); } - - Ok(Some(NormalizationPlan::loop_with_post(post_assign_count))) + Ok(Some(NormalizationPlan::loop_only())) } } @@ -226,9 +167,10 @@ mod tests { } #[test] - fn test_plan_block_suffix_phase132_loop_post_single() { + fn test_plan_block_suffix_phase142_loop_with_subsequent_stmts() { use crate::mir::builder::MirBuilder; + // Phase 142 P0: loop(true) returns loop_only regardless of subsequent statements let remaining = vec![ make_loop(), make_assignment("x", 2), @@ -241,15 +183,16 @@ mod tests { assert!(plan.is_some()); let plan = plan.unwrap(); - assert_eq!(plan.consumed, 3); // loop + 1 assign + return - assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 1 }); - assert!(plan.requires_return); + assert_eq!(plan.consumed, 1); // Phase 142: only consume loop + assert_eq!(plan.kind, PlanKind::LoopOnly); + assert!(!plan.requires_return); // Loop itself doesn't require return } #[test] - fn test_plan_block_suffix_phase133_loop_post_multi() { + fn test_plan_block_suffix_phase142_loop_only_always() { use crate::mir::builder::MirBuilder; + // Phase 142 P0: loop(true) always returns loop_only, even with multiple statements after let remaining = vec![ make_loop(), make_assignment("x", 2), @@ -263,32 +206,9 @@ mod tests { assert!(plan.is_some()); let plan = plan.unwrap(); - assert_eq!(plan.consumed, 4); // loop + 2 assigns + return - assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 2 }); - assert!(plan.requires_return); - } - - #[test] - fn test_plan_block_suffix_return_boundary() { - use crate::mir::builder::MirBuilder; - - // Pattern with unreachable statement after return - let remaining = vec![ - make_loop(), - make_assignment("x", 2), - make_return("x"), - make_assignment("y", 999), // Unreachable - ]; - - let builder = MirBuilder::new(); - let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false) - .expect("Should not error"); - - assert!(plan.is_some()); - let plan = plan.unwrap(); - // Should consume only up to return (not the unreachable statement) - assert_eq!(plan.consumed, 3); // loop + 1 assign + return - assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 1 }); + assert_eq!(plan.consumed, 1); // Phase 142: only consume loop + assert_eq!(plan.kind, PlanKind::LoopOnly); + assert!(!plan.requires_return); } #[test] @@ -321,64 +241,25 @@ mod tests { } #[test] - fn test_plan_block_suffix_no_match_no_return() { + fn test_plan_block_suffix_phase142_loop_with_trailing_stmt() { use crate::mir::builder::MirBuilder; + // Phase 142 P0: loop(true) returns loop_only even if no return follows let remaining = vec![ make_loop(), make_assignment("x", 2), - // Missing return - ]; - - let builder = MirBuilder::new(); - let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false) - .expect("Should not error"); - - // No return means not a valid post pattern - assert!(plan.is_none()); - } - - #[test] - fn test_plan_block_suffix_phase135_loop_return_only() { - use crate::mir::builder::MirBuilder; - - // Phase 135: loop + return (0 post assignments) - let remaining = vec![ - make_loop(), - make_return("x"), + // No return - but still returns loop_only ]; let builder = MirBuilder::new(); let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false) .expect("Should not error"); + // Phase 142: loop(true) always matches, regardless of subsequent statements assert!(plan.is_some()); let plan = plan.unwrap(); - assert_eq!(plan.consumed, 2); // loop + return - assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 0 }); - assert!(plan.requires_return); - } - - #[test] - fn test_plan_block_suffix_return_boundary_with_trailing() { - use crate::mir::builder::MirBuilder; - - // Pattern with unreachable statement after return (Phase 135 variation) - let remaining = vec![ - make_loop(), - make_return("x"), - make_assignment("y", 999), // Unreachable - ]; - - let builder = MirBuilder::new(); - let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false) - .expect("Should not error"); - - assert!(plan.is_some()); - let plan = plan.unwrap(); - // Should consume only up to return (not the unreachable statement) - assert_eq!(plan.consumed, 2); // loop + return - assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 0 }); + assert_eq!(plan.consumed, 1); // Only consume loop + assert_eq!(plan.kind, PlanKind::LoopOnly); } #[test] diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 410d5c79..dcafde70 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -208,13 +208,14 @@ impl super::MirBuilder { trace.emit_if( "debug", "build_block/suffix_router", - &format!("Suffix router consumed {} statements (including return), stopping block build", consumed), + &format!("Phase 142 P0: Suffix router consumed {} statement(s), continuing to process subsequent statements", consumed), debug, ); - // Suffix router consumes a return statement and emits a host-level Return - // after the JoinIR merge. Break to avoid double-processing. + // Phase 142 P0: Normalization unit is now "statement (loop 1個)" + // Loop normalization returns consumed=1, and subsequent statements + // (return, assignments, etc.) are handled by normal MIR lowering idx += consumed; - break; + // No break - continue processing subsequent statements } None => { // No match, proceed with normal statement build diff --git a/tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh new file mode 100644 index 00000000..b7678a67 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Phase 142 P0: Loop normalization as single statement (VM) +# +# Verifies that loop(true) normalization returns consumed=1, allowing +# subsequent statements (return s.length()) to be processed normally. +# Expected: exit code 3 (s="abc" → s.length() → 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 142 is a dev-only Normalized shadow loop case +require_joinir_dev + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 142 P0: Loop normalization as single statement (VM)" + +# Test 1: phase142_loop_stmt_only_then_return_length_min.hako +echo "[INFO] Test 1: phase142_loop_stmt_only_then_return_length_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase142_loop_stmt_only_then_return_length_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 142: expected output is exit code 3 (s.length() where s="abc") + 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 "phase142_loop_stmt_only_then_return_length_min_vm: All tests passed" + exit 0 +else + test_fail "phase142_loop_stmt_only_then_return_length_min_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi