# Phase 142-loopstmt: Statement-Level Loop Normalization Status: ✅ P0 Complete Date: 2025-12-19 --- ## 目的(Why) Phase 131-141 で loop(true) 系の Normalized shadow を段階的に拡張したが、「block suffix (loop + post assigns + return)」の直積パターン増殖を止める必要があった。 **Phase 142-loopstmt** では、正規化単位を **"block suffix" から "statement (loop 1個)" へ寄せる** ことで、パターン爆発を防ぐ。 ### Non-Goal - ⚠️ **Phase 142 (Canonicalizer Pattern Extension) とは別物** - Phase 142 = trim leading/trailing, continue pattern (Canonicalizer) - Phase 142-loopstmt = Statement-level normalization (Normalized shadow) - SSOT 衝突回避のため phase-142-loopstmt として独立管理 --- ## P0: Statement-Level Normalization (COMPLETE ✅) ### Summary Normalization unit changed from "block suffix (loop + post + return)" to "statement (loop only)". ### Implementation #### 1. PlanBox の挙動変更 **File**: `src/mir/builder/control_flow/normalization/plan_box.rs` **Before**: - `loop(true)` の後ろに return が無いと plan が None になりやすい - LoopWithPost pattern を返す(loop + post assigns + return を一括消費) **After**: - `remaining[0]` が `loop(true)` なら 常に `NormalizationPlan::loop_only()` を返す - **consumed = 1** (loop のみ) - 後続文(return/assign 等)は通常 MIR lowering へ戻す **Code changes**: ```rust // Phase 142-loopstmt P0: Always return loop_only for loop(true) // Normalization unit is now "statement (loop 1個)" not "block suffix" // Subsequent statements handled by normal MIR lowering if debug { trace.routing( "normalization/plan", func_name, "Detected loop(true) - Phase 142-loopstmt P0: returning loop_only (consumed=1)", ); } Ok(Some(NormalizationPlan::loop_only())) ``` **Impact**: ~70 lines reduced #### 2. SuffixRouter の LoopOnly 対応 **File**: `src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs` **Before**: Lines 64-75 で PlanKind::LoopOnly を reject ("not a suffix") **After**: LoopOnly も受け入れて execute_box に渡す **Code changes**: ```rust // Phase 142-loopstmt P0: Accept both LoopOnly and LoopWithPost // Normalization unit is now "statement (loop 1個)", not "block suffix" if debug { let description = match &plan.kind { PlanKind::LoopOnly => "Loop-only pattern".to_string(), PlanKind::LoopWithPost { post_assign_count } => { format!("Loop+post pattern: {} post assigns", post_assign_count) } }; trace.routing("suffix_router", func_name, &description); } ``` **Impact**: ~12 lines reduced #### 3. build_block で consumed 後の break 削除 **File**: `src/mir/builder/stmts.rs` **Before**: - suffix_router が Some(consumed) を返したら break - return statement が消費されたと仮定 **After**: - consumed 後も `idx += consumed` でスキップし、後続文を通常処理 - Phase 142-loopstmt では loop 正規化後も `return s.length()` 等が残る **Code changes**: ```rust Some(consumed) => { trace.emit_if( "debug", "build_block/suffix_router", &format!("Phase 142-loopstmt P0: Suffix router consumed {} statement(s), continuing to process subsequent statements", consumed), debug, ); // Phase 142-loopstmt 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; // No break - continue processing subsequent statements } ``` ### Unit Tests **File**: `src/mir/builder/control_flow/normalization/plan_box.rs` **Updated tests** (7 tests total): - `test_plan_block_suffix_phase131_loop_only()` - unchanged - `test_plan_block_suffix_phase142_loop_with_subsequent_stmts()` - was phase132, now expects LoopOnly - `test_plan_block_suffix_phase142_loop_only_always()` - was phase133, now expects LoopOnly - `test_plan_block_suffix_phase142_loop_with_trailing_stmt()` - new, no return but still LoopOnly - `test_plan_block_suffix_no_match_empty()` - unchanged - `test_plan_block_suffix_no_match_not_loop()` - unchanged - `test_plan_block_suffix_no_match_loop_not_true()` - unchanged **Results**: ✅ 7/7 passed ### E2E Tests #### Fixture **File**: `apps/tests/phase142_loop_stmt_only_then_return_length_min.hako` ```hako static box Main { main() { local s s = "abc" loop(true) { break } return s.length() } } ``` **Expected**: exit code 3 (s="abc" → s.length() → 3) #### VM Smoke Test **File**: `tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh` **Command**: ```bash bash tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh ``` **Result**: ✅ PASS (exit code 3) #### LLVM EXE Smoke Test **File**: `tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_llvm_exe.sh` **Status**: ⚠️ TODO in P1 (requires Phase 130 LLVM EXE gate complete) --- ## Refactoring (COMPLETE ✅) ### Task 1: suffix_router コメント更新 **Commit**: `21a3c6b5d` - "docs(normalization): Update suffix_router comments for Phase 142-loopstmt P0" **Changes**: - Updated header comments to reflect post statements no longer required - Documented LoopOnly pattern acceptance ### Task 2: LoopWithPost Deprecation **Commit**: `aaba27d31` - "refactor(normalization): Deprecate LoopWithPost variant" **Changes**: - Added `#[deprecated]` to `PlanKind::LoopWithPost` enum variant - Added deprecation to `loop_with_post()` constructor function - Documented migration path to LoopOnly - Kept for backward compatibility **Deprecation warnings** (4 locations): - `normalized_shadow_suffix_router_box.rs:69` - `routing.rs:457` - `plan.rs:74` - `execute_box.rs:66` ### Task 3: README 更新 **Commit**: `3ef929df5` - "docs(normalization): Update README for Phase 142-loopstmt P0" **Changes**: - Added Phase 142-loopstmt P0 section - Marked Phase 132-135 as LEGACY - Updated suffix_router description --- ## Acceptance Criteria (All Met ✅) - [x] PlanBox が loop(true) に対して常に loop_only を返す(consumed=1) - [x] SuffixRouter が LoopOnly を受け入れて実行する - [x] build_block が consumed 後も後続文を処理する - [x] Fixture が exit code 3 を返す(VM) - [x] 既存 smokes が緑(Phase 97/131/136/137/139/141) - Phase 131 VM: ✅ PASS - Normalization unit tests: ✅ 10 passed - [x] Out-of-scope は常に Ok(None) でフォールバック(既定挙動不変) - [x] Documentation 作成 - [x] Refactoring tasks 完了(3 commits) - [x] Main implementation commit --- ## Design Principles ### Pattern Explosion Prevention - **制御フローの骨格(loop/if/post_k/continuation)**: 正規化(段階投入)で固める - **式(return value を含む)**: 一般化(AST walker)で受ける(Phase 140+) ### Normalization Unit Evolution **Phase 141 以前**: ``` NormalizationPlan::loop_with_post(n) → consumed = 1 + n + 1 (loop + assigns + return) ``` **Phase 142-loopstmt P0**: ``` NormalizationPlan::loop_only() → consumed = 1 (loop のみ) 後続文は通常 MIR lowering で処理 ``` ### Fail-Fast Policy - **Out-of-scope**: 常に `Ok(None)` でフォールバック(既定挙動不変) - **Fail-Fast**: "in-scope のはずなのに壊れた" ケースのみ(internal error) --- ## Files Modified ### Core Implementation 1. `src/mir/builder/control_flow/normalization/plan_box.rs` - Always return loop_only for loop(true) - 7 unit tests updated 2. `src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs` - Accept LoopOnly patterns - Remove rejection logic 3. `src/mir/builder/stmts.rs` - Remove break after consumed - Continue processing subsequent statements ### Tests 4. `apps/tests/phase142_loop_stmt_only_then_return_length_min.hako` (NEW) 5. `tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh` (NEW) ### Documentation 6. `src/mir/builder/control_flow/normalization/plan.rs` (deprecation) 7. `src/mir/builder/control_flow/normalization/README.md` (updated) ### Statistics - **Total commits**: 4 - **Files changed**: 7 - **Net change**: -38 lines (削減成功!) - **Code reduction**: ~82 lines deleted (pattern detection logic) --- ## Verification Commands ### Unit Tests ```bash cargo test -p nyash-rust --lib mir::builder::control_flow::normalization ``` ### Regression Tests ```bash # Phase 131 regression bash tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh # Phase 141 regression bash tools/smokes/v2/profiles/integration/apps/phase141_p1_if_only_post_k_return_length_vm.sh # Phase 142-loopstmt P0 bash tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_vm.sh ``` --- ## Next Steps ### P1: LLVM EXE Smoke Test (TODO) **File**: `tools/smokes/v2/profiles/integration/apps/phase142_loop_stmt_only_then_return_length_min_llvm_exe.sh` **Prerequisites**: Phase 130 LLVM EXE gate complete **Expected**: exit code 3 parity with VM ### P2: Code Contract Enforcement (Planned) **Goal**: Prevent future regression to pattern explosion **Options**: - **Option A (Recommended)**: Remove LoopWithPost creation path entirely - **Option B**: Introduce Outcome { consumed, stop_block } SSOT ### Phase 143-loopvocab (Planned) **Goal**: 「語彙追加」で `loop(true){ if(cond) break/continue }` を吸収 **Approach**: - Extend StepTree/ControlTree vocabulary (not new patterns) - Use NormalizedExprLowererBox for pure conditions - JoinIR Jump { cond: Some(vid) } for conditional exits --- ## Related Documentation - [10-Now.md](../../10-Now.md) - Current progress - [30-Backlog.md](../../30-Backlog.md) - Future phases - [phase-143-loopvocab/README.md](../phase-143-loopvocab/README.md) - Next planned vocabulary expansion - [normalized-expr-lowering.md](../../design/normalized-expr-lowering.md) - Normalized design SSOT - [control-tree.md](../../design/control-tree.md) - ControlTree design - [normalization/README.md](../../../src/mir/builder/control_flow/normalization/README.md) - Architecture --- ## Commits 1. `21a3c6b5d` - docs(normalization): Update suffix_router comments for Phase 142-loopstmt P0 2. `aaba27d31` - refactor(normalization): Deprecate LoopWithPost variant 3. `3ef929df5` - docs(normalization): Update README for Phase 142-loopstmt P0 4. `275fe45ba` - feat(normalization): Phase 142-loopstmt P0 - Statement-level normalization