diff --git a/apps/tests/phase135_loop_true_break_once_post_empty_return_min.hako b/apps/tests/phase135_loop_true_break_once_post_empty_return_min.hako new file mode 100644 index 00000000..fdf76340 --- /dev/null +++ b/apps/tests/phase135_loop_true_break_once_post_empty_return_min.hako @@ -0,0 +1,28 @@ +// Phase 135 P0: loop(true) break-once with post 0 assigns + return +// +// Purpose: Test loop(true) { * ; break } + return (0 assignments) +// Expected output: 1 +// +// Structure: +// x = 0 // pre-loop init +// loop(true) { // condition is Bool literal true +// x = 1 // body assignment +// break // break at end +// } +// return x // direct return (NO post-loop assignments) +// +// Difference from Phase 131: +// Phase 131: loop only (no return) +// Phase 135: loop + return (0 post assignments) + +static box Main { + main() { + local x + x = 0 + loop(true) { + x = 1 + break + } + return x + } +} diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index f0c0ba12..e10896b3 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -1,5 +1,35 @@ # Self Current Task — Now (main) +## 2025-12-18:Phase 135 P0 完了 ✅ + +**Phase 135 P0: Normalization Plan Suffix Detection Generalization** +- 目的: NormalizationPlanBox の suffix 検出を一般化し、post-loop assign が 0 回でも OK に +- 背景: + - Phase 133 までは `loop + assign+ + return`(assign 1回以上必須) + - Phase 131 は `loop` のみ(return なし) + - ギャップ: `loop + return`(0 assign)が別経路 +- 改善: + - `loop + assign* + return`(N >= 0 assignments)を統一パターンとして検出 + - Phase 131 と Phase 132-133 を `LoopWithPost { post_assign_count }` enum で統一 + - Phase 131 (loop-only, no return) は `PlanKind::LoopOnly` として独立維持 +- 実装: + - `src/mir/builder/control_flow/normalization/plan_box.rs`(検出ロジック修正のみ) + - `post_assign_count >= 1` チェックを削除 → `>= 0` を許容 + - `execute_box.rs` は変更なし(既存ロジックが 0 assigns を自然にサポート) +- Fixture: `apps/tests/phase135_loop_true_break_once_post_empty_return_min.hako`(期待: exit code 1) +- Smoke tests: + - `phase135_loop_true_break_once_post_empty_return_vm.sh` PASS + - `phase135_loop_true_break_once_post_empty_return_llvm_exe.sh` PASS +- Regression: + - Phase 131: 2/2 PASS(VM + LLVM EXE) + - Phase 133: 2/2 PASS(VM + LLVM EXE) +- Unit tests: 9/9 PASS(plan_box module) +- 設計原則: + - **最小変更**: plan_box.rs の検出条件のみ変更 + - **SSOT 維持**: 検出ロジックは plan_box.rs に集約 + - **Box-First**: PlanBox(what)と ExecuteBox(how)の責任分離維持 +- 入口: `src/mir/builder/control_flow/normalization/README.md`(Phase 135 セクション追加) + ## 2025-12-18:Phase 133 完了 ✅ **Phase 133: loop(true) break-once + multiple post-loop assigns(dev-only)** diff --git a/src/mir/builder/control_flow/normalization/README.md b/src/mir/builder/control_flow/normalization/README.md index 567c557a..5803bbc7 100644 --- a/src/mir/builder/control_flow/normalization/README.md +++ b/src/mir/builder/control_flow/normalization/README.md @@ -87,13 +87,13 @@ pub enum PlanKind { --- -## Pattern Detection (Phase 131-133) +## Pattern Detection (Phase 131-135) ### Phase 131: Loop-Only -- Pattern: `loop(true) { ... break }` +- Pattern: `loop(true) { ... break }` (single statement, no return) - Consumed: 1 statement - Kind: `PlanKind::LoopOnly` -- Example: `loop(true) { x = 1; break }; return x` +- Example: `loop(true) { x = 1; break }` ### Phase 132: Loop + Single Post - Pattern: `loop(true) { ... break }; ; return ` @@ -107,6 +107,14 @@ pub enum PlanKind { - Kind: `PlanKind::LoopWithPost { post_assign_count: N }` - Example: `loop(true) { x = 1; break }; x = x + 2; x = x + 3; return x` +### Phase 135: Loop + Return (Zero Post Assigns) **NEW** +- Pattern: `loop(true) { ... break }; return ` (0 post-loop assignments) +- Consumed: 2 statements (loop, return) +- Kind: `PlanKind::LoopWithPost { post_assign_count: 0 }` +- Example: `loop(true) { x = 1; break }; return x` +- **Improvement**: Unifies Phase 131 and Phase 132-133 patterns under `LoopWithPost` enum +- **Compatibility**: Phase 131 (loop-only, no return) remains as `PlanKind::LoopOnly` + --- ## Contract @@ -127,7 +135,7 @@ pub enum PlanKind { **Invariants**: - `consumed <= remaining.len()` (never consume more than available) - If `requires_return` is true, `remaining[consumed-1]` must be Return -- Post-assign patterns require `consumed >= 3` (loop + assign + return minimum) +- Post-assign patterns require `consumed >= 2` (loop + return minimum, Phase 135+) ### NormalizationExecuteBox::execute() @@ -172,10 +180,13 @@ pub enum PlanKind { - Phase 131 pattern detection (loop-only) - Phase 132 pattern detection (loop + single post) - Phase 133 pattern detection (loop + multiple post) +- **Phase 135 pattern detection (loop + zero post)** **NEW** - Return boundary detection (consumed stops at return) +- **Return boundary with trailing statements** **NEW** - Non-matching patterns (returns None) ### Regression Smokes +- **Phase 135**: `phase135_loop_true_break_once_post_empty_return_{vm,llvm_exe}.sh` **NEW** - Phase 133: `phase133_loop_true_break_once_post_multi_add_{vm,llvm_exe}.sh` - Phase 132: `phase132_loop_true_break_once_post_add_llvm_exe.sh` - Phase 131: `phase131_loop_true_break_once_{vm,llvm_exe}.sh` diff --git a/src/mir/builder/control_flow/normalization/plan_box.rs b/src/mir/builder/control_flow/normalization/plan_box.rs index 1803202d..1a24c91e 100644 --- a/src/mir/builder/control_flow/normalization/plan_box.rs +++ b/src/mir/builder/control_flow/normalization/plan_box.rs @@ -65,39 +65,7 @@ impl NormalizationPlanBox { return Ok(Some(NormalizationPlan::loop_only())); } - // Check if second statement is an assignment (Phase 132-133 specific) - let has_post_assignment = matches!(&remaining[1], ASTNode::Assignment { .. }); - if !has_post_assignment { - // Not a post-assignment pattern, treat as loop-only if it's just a loop - // (if there are other statements after the loop that aren't assignments, - // this is not our pattern - return None) - if debug { - trace.routing( - "normalization/plan", - func_name, - "No post-assignment after loop, treating as loop-only", - ); - } - return Ok(Some(NormalizationPlan::loop_only())); - } - - // Phase 132-133: Loop + post assignments + return - // Minimum: loop + 1 assign + return (3 statements) - if remaining.len() < 3 { - // Has assignment after loop but no return - not a valid pattern - if debug { - trace.routing( - "normalization/plan", - func_name, - &format!( - "Loop + assignment but no return ({} statements), not a valid pattern", - remaining.len() - ), - ); - } - return Ok(None); - } - + // Phase 132-135: Loop + (assign*) + return // Count consecutive assignments after the loop let mut post_assign_count = 0; for i in 1..remaining.len() { @@ -108,32 +76,33 @@ impl NormalizationPlanBox { } } - // After assignments, we need a return statement + // After assignments (0 or more), we need a return statement let return_index = 1 + post_assign_count; if return_index >= remaining.len() { - // No return statement - cannot use post pattern + // No statement after assignments - not a post pattern if debug { trace.routing( "normalization/plan", func_name, &format!( - "No return after {} assignments, not a valid post pattern", + "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 assignments is not return - not a post pattern + // Statement after loop (and optional assignments) is not return - not a post pattern if debug { trace.routing( "normalization/plan", func_name, &format!( - "Statement after {} assignments is not return, not a valid post pattern", + "Statement after loop and {} assignments is not return, not a valid post pattern", post_assign_count ), ); @@ -141,13 +110,13 @@ impl NormalizationPlanBox { return Ok(None); } - // Valid Phase 132-133 pattern: loop + N assignments + return + // Valid Phase 132-135 pattern: loop + N assignments (N >= 0) + return if debug { trace.routing( "normalization/plan", func_name, &format!( - "Detected Phase 132-133 pattern: loop + {} post assigns + return", + "Detected Phase 132-135 pattern: loop + {} post assigns + return", post_assign_count ), ); @@ -351,4 +320,47 @@ mod tests { // 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"), + ]; + + 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(); + 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 }); + } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_llvm_exe.sh new file mode 100644 index 00000000..0173556d --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_llvm_exe.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Phase 135 P0: loop(true) break-once + return (0 post assigns, LLVM EXE parity) +# Pattern: loop(true) { x = 1; break } → return x (should be 1) + +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 135 is a dev-only Normalized shadow loop case. +# LLVM EXE emission must run with JoinIR dev/strict enabled, otherwise it will freeze. +require_joinir_dev + +# Phase 135: 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/phase135_loop_true_break_once_post_empty_return_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase135_loop_true_break_once_post_empty_return_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase135_loop_true_break_once_post_empty_return_llvm_exe" + +EXPECTED_EXIT_CODE=1 +LLVM_BUILD_LOG="/tmp/phase135_loop_true_break_once_post_empty_return_build.log" +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase135_loop_true_break_once_post_empty_return_llvm_exe: exit code matches expected (1)" +else + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_vm.sh new file mode 100644 index 00000000..641f7602 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase135_loop_true_break_once_post_empty_return_vm.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Phase 135 P0: loop(true) break-once + return (0 post assigns, Normalized shadow, VM) +# +# Verifies that loop(true) { * ; break } + return (0 assigns) works: +# - x = 0 → loop(true) { x = 1; break } → return x → 1 +# - 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 135 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 135 P0: loop(true) break-once + return (0 post assigns, Normalized shadow, VM)" + +# Test 1: phase135_loop_true_break_once_post_empty_return_min.hako +echo "[INFO] Test 1: phase135_loop_true_break_once_post_empty_return_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase135_loop_true_break_once_post_empty_return_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 1 ]; then + # Phase 135: expected output is exit code 1 (return value) + echo "[PASS] exit code verified: 1" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 1)" + 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 "phase135_loop_true_break_once_post_empty_return_vm: All tests passed" + exit 0 +else + test_fail "phase135_loop_true_break_once_post_empty_return_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi