feat(joinir): Phase 286 P3.2 - Pattern5 Plan line (loop(true) + early exit)

- Pattern5InfiniteEarlyExitPlan (Return/Break variants)
- extract_pattern5_plan() for loop(true) literal only
- normalize_pattern5_return(): 5 blocks CFG (header→body→found/step)
- normalize_pattern5_break(): 6 blocks CFG with carrier PHI
- NormalizationPlanBox exclusion for Pattern5-style loops
- Fixtures: phase286_pattern5_{return,break}_min.hako
- quick smoke 154/154 PASS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 09:56:34 +09:00
parent ec55cce380
commit 22945c190c
12 changed files with 963 additions and 16 deletions

View File

@ -0,0 +1,16 @@
// Phase 286 P3.2: Pattern5 infinite loop with break
// Tests: loop(true) with if (i == 3) { break } and carrier update
// Expected: 3 (sum = 0 + 1 + 1 + 1 = 3 for i = 0, 1, 2)
static box Main {
main() {
local i = 0
local sum = 0
loop(true) {
if (i == 3) { break }
sum = sum + 1
i = i + 1
}
return sum
}
}

View File

@ -0,0 +1,14 @@
// Phase 286 P3.2: Pattern5 infinite loop with early return
// Tests: loop(true) with if (i == 3) { return 7 }
// Expected: 7
static box Main {
main() {
local i = 0
loop(true) {
if (i == 3) { return 7 }
i = i + 1
}
return 0
}
}

View File

@ -1,8 +1,8 @@
# Self Current Task — Now (main)
## Current Focus: Phase 286 P2.7 (Plan line guardrails)
## Current Focus: Phase 284 P0 (design-first) / Post Phase 286
Pattern3 まで Plan line で完走し、legacy fallback を撤去できた。次は「同じ落とし穴を二度踏まない」ため、Plan/LoopPlan の契約(例: body ブロックの effect 配置をドキュメント・verify で固定していく
Phase 286 で JoinIR line の主要 pattern19が Plan/Frag SSOT に吸収できた。次は “出口語彙” をさらに SSOT に寄せるため、Phase 284Return as ExitKind SSOTへ進む
設計相談(将来の正規化):
- `docs/development/current/main/investigations/phase-286-plan-normalization-consult.md`
@ -23,11 +23,11 @@ Pattern3 まで Plan line で完走し、legacy fallback を撤去できた。
- quick smoke 154/154 PASS 維持、Pattern1/4 PoC 両方 PASS
**次のステップ**:
1. **Phase 286 P2.7** (current): Plan line guardrailsverify / doc
- 例: CoreLoopPlan の “body effects は loop_plan.body に積む” 契約を Fail-Fast で固定
2. **Phase 286future design, separate phase**: Plan 生成の正規化をどう進めるか(相談パケット)
1. **Phase 284design-first**: Return as ExitKind SSOTpattern に散らさない
2. **future designseparate phase**: Plan 生成の正規化をどう進めるか(相談パケット)
- `docs/development/current/main/investigations/phase-286-plan-normalization-consult.md`
3. **Phase 284** (design-first): Return as ExitKind SSOT
3. optional, post self-hostREPL = script engine 構想docs-only
- `docs/reference/language/repl.md`
## Recently Completed

View File

@ -59,13 +59,12 @@ Related:
- 既存の `apps/tests/*.hako` fixture を再利用し、VM/LLVM parity のスモークが維持される。
- weak の語彙(`weak <expr>` / `weak_to_strong()`が同じ意味で動作するcycleは当面リーク仕様でも可
- **Phase 286planned, design-first: JoinIR Line AbsorptionJoinIR→CorePlan/Frag 収束)**
- **Phase 286✅ complete: JoinIR Line AbsorptionJoinIR→CorePlan/Frag 収束)**
- 目的: 移行期間に残っている「2本の loweringPlan line / JoinIR line」を、構造で 1 本に収束させる
- ねらい: `return/break/continue` のような “大きな出口語彙” の実装場所が揺れない状態にする
- 入口: `docs/development/current/main/phases/phase-286/README.md`
- P0docs-only: `docs/development/current/main/phases/phase-286/P0-INSTRUCTIONS.md`
- 状況: P2Pattern4/P2.1Pattern1/P2.2hygiene/P2.3Pattern9/P2.4.1Pattern8 Plan完走まで完了。現在は P2.6Pattern3 Plan化進行中
- 次の実装指示Pattern3完走: `docs/development/current/main/phases/phase-286/P2.6.1-INSTRUCTIONS.md`
- 状況: Pattern19 の主要ループ形が Plan/Frag SSOT に吸収され、quick gate は green を維持
- 将来の設計相談Phase 286 の範囲外で、別フェーズでSSOT化してから: `docs/development/current/main/investigations/phase-286-plan-normalization-consult.md`
- SSOT:
- Plan/Frag: `compose::*` + `emit_frag()`Phase 280/281

View File

@ -1,6 +1,6 @@
# Phase 286: JoinIR Line AbsorptionJoinIR→CorePlan/Frag 収束)
Status: In Progress (P0, P1, P2, P2.1, P2.2, P2.3, P2.4, P2.4.1, P2.6, P2.7, P3, 286C-2, **P3.1 COMPLETE**)
Status: ✅ COMPLETE (P0, P1, P2, P2.1, P2.2, P2.3, P2.4.1, P2.6, P2.7, P2.8, P3.1, P3.2, P3 COMPLETE)
## Goal
@ -38,8 +38,22 @@ Phase 286 では JoinIR line を “第2の lowerer” として放置せず、*
## Next短期の道筋
- **P2.6.1Pattern3 Plan完走**: `docs/development/current/main/phases/phase-286/P2.6.1-INSTRUCTIONS.md`
- **将来設計の相談別フェーズでSSOT化してから**: `docs/development/current/main/investigations/phase-286-plan-normalization-consult.md`
- **次のおすすめdesign-first**: Phase 284Return as ExitKind SSOT
## Coverage MapPlan line 移行状況)
| Pattern | Shape | Status | Notes |
|---:|---|---|---|
| 1 | SimpleWhile | ✅ Plan line | PoC + integration fixture固定済み |
| 2 | Break | ✅ Plan line | PoCサブセットは Plan 完走、PoC外は `Ok(None)` で legacy fallback |
| 3 | If-Phi | ✅ Plan line | Fail-Fast 統一extract `Some` 後の legacy fallback なし) |
| 4 | Continue | ✅ Plan line | PoC + integration fixture固定済み |
| 5 | InfiniteEarlyExit | ✅ Plan line | `loop(true)` literal に限定した PoC サブセットReturn/Break |
| 6 | ScanWithInit | ✅ Plan line | Phase 273 で SSOT |
| 7 | SplitScan | ✅ Plan line | Phase 273 で SSOT |
| 8 | BoolPredicateScan | ✅ Plan line | static box は設計上スキップReceiverNormalizeBox が担当) |
| 9 | AccumConstLoop | ✅ Plan line | Pattern1 より優先(より具体的) |
### P0docs-only✅ COMPLETE (2025-12-25)
@ -380,6 +394,67 @@ preheader → header(PHI: i_current, carrier_current)
- ✅ Fixture B (break without update): Plan line PASS (出力 11)
- ✅ Regression: quick smoke 154 PASS, 0 FAILED
### P3.2 (Pattern5 Plan化 - Infinite Loop with Early Exit) ✅ COMPLETE (2025-12-26)
**完了内容**:
- Pattern5InfiniteEarlyExitPlan 追加Return版・Break版両対応
- extract_pattern5_plan() 実装loop(true) リテラル限定)
- normalize_pattern5_infinite_early_exit() 実装Return版5blocks、Break版6blocks CFG
- NormalizationPlanBox で Pattern5 スタイルループを除外Plan line へルーティング)
- quick smoke 154/154 PASS
**背景**:
- Pattern5 は `loop(true)` の無限ループパターン
- 既存 legacy Pattern5 は `break + continue` 両方必須の複雑な形式
- PoC は **simpler subset**: 早期 return または break 単独
**CFG構造Return版**:
```
preheader → header(PHI: i_current) → body(exit_cond)
↑ ↓
└───── step ←──────── else path
then path: CoreExitPlan::Return
```
**CFG構造Break版**:
```
preheader → header(PHI: i, carrier) → body(exit_cond)
↑ ↓
└───── step ←────────── else path
then path → after_bb(PHI: carrier_out)
```
**実装ステップ**:
1. Step 0: docs-first - README P3.2節追加
2. Step 1: integration fixture 2本 (return版、break版)
3. Step 2: DomainPlan::Pattern5InfiniteEarlyExit 追加
4. Step 3: extract_pattern5_plan() 実装
5. Step 4: normalize_pattern5_infinite_early_exit() 実装
6. Step 5: router に Pattern5 追加
7. Step 6: 検証
**PoC サブセット厳守**:
- `loop(true)` リテラルのみ(`loop(1)` や truthy は `Ok(None)`
- Return版: `if (cond) { return <expr> }` + `i = i + 1`
- Break版: `if (cond) { break }` + `sum = sum + 1` + `i = i + 1`carrier_update 必須)
**成果物** (予定):
- `apps/tests/phase286_pattern5_return_min.hako` (Fixture A)
- `apps/tests/phase286_pattern5_break_min.hako` (Fixture B)
- `tools/smokes/v2/profiles/integration/apps/phase286_pattern5_return_vm.sh`
- `tools/smokes/v2/profiles/integration/apps/phase286_pattern5_break_vm.sh`
- `src/mir/builder/control_flow/plan/mod.rs` (Pattern5InfiniteEarlyExitPlan + Pattern5ExitKind)
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern5.rs` (extract_pattern5_plan)
- `src/mir/builder/control_flow/plan/normalizer.rs` (normalize_pattern5_infinite_early_exit)
- `src/mir/builder/control_flow/joinir/patterns/router.rs` (Pattern5 Plan line routing)
**成功基準**:
- Fixture A (return): PASS (出力 7)
- Fixture B (break): PASS (出力 3)
- Regression: quick smoke 154 PASS, 0 FAILED
## AcceptanceP0
- 2本の lowering が "設計として" どこで 1 本に収束するかが明文化されている

View File

@ -163,6 +163,141 @@ fn count_control_flow_recursive(body: &[ASTNode]) -> (usize, usize, usize, bool)
// Phase 282 P9a: validate_continue_at_end moved to common_helpers
// Phase 282 P9a: validate_break_in_simple_if moved to common_helpers
// ============================================================================
// Phase 286 P3.2: Plan line extractor (PoC subset)
// ============================================================================
use crate::mir::builder::control_flow::plan::{
DomainPlan, Pattern5InfiniteEarlyExitPlan, Pattern5ExitKind,
};
/// Extract variable name and increment expression from assignment
///
/// Returns (variable_name, increment_expr) for `var = expr` form
fn extract_assignment_parts(stmt: &ASTNode) -> Option<(String, ASTNode)> {
if let ASTNode::Assignment { target, value, .. } = stmt {
if let ASTNode::Variable { name, .. } = target.as_ref() {
return Some((name.clone(), value.as_ref().clone()));
}
}
None
}
/// Extract Pattern5 Plan (Phase 286 P3.2)
///
/// # PoC Subset (strict)
///
/// - `loop(true)` literal ONLY (not `loop(1)` or truthy)
/// - Return version: `if (cond) { return <expr> }` + `i = i + 1`
/// - Break version: `if (cond) { break }` + `sum = sum + 1` + `i = i + 1` (carrier_update required)
///
/// # Returns
///
/// - `Ok(Some(DomainPlan))`: Pattern5 Plan match
/// - `Ok(None)`: Not PoC subset (fall back to legacy or other patterns)
/// - `Err(msg)`: Close-but-unsupported (Fail-Fast)
pub(crate) fn extract_pattern5_plan(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<DomainPlan>, String> {
// ========================================================================
// Block 1: Check condition is `true` literal (loop(true) ONLY)
// ========================================================================
if !is_true_literal(condition) {
return Ok(None); // Not loop(true) → Not Pattern5 Plan
}
// ========================================================================
// Block 2: Find exit statement (if at first position)
// ========================================================================
// PoC subset: first statement must be `if (cond) { return/break }`
if body.is_empty() {
return Ok(None);
}
let first_stmt = &body[0];
let (exit_kind, exit_condition, exit_value) = match first_stmt {
ASTNode::If { condition: if_cond, then_body, else_body: None, .. } => {
// Must be single-statement then body
if then_body.len() != 1 {
return Ok(None);
}
match &then_body[0] {
ASTNode::Return { value, .. } => {
(Pattern5ExitKind::Return, if_cond.as_ref().clone(), value.clone())
}
ASTNode::Break { .. } => {
(Pattern5ExitKind::Break, if_cond.as_ref().clone(), None)
}
_ => return Ok(None),
}
}
_ => return Ok(None),
};
// ========================================================================
// Block 3: Parse remaining body for carrier update and loop increment
// ========================================================================
let remaining = &body[1..];
match exit_kind {
Pattern5ExitKind::Return => {
// Return version: just need loop increment
// Expected: [increment]
if remaining.len() != 1 {
return Ok(None);
}
let (loop_var, loop_increment) = match extract_assignment_parts(&remaining[0]) {
Some(result) => result,
None => return Ok(None),
};
// Unbox exit_value if present
let exit_value_unboxed = exit_value.map(|boxed| boxed.as_ref().clone());
Ok(Some(DomainPlan::Pattern5InfiniteEarlyExit(Pattern5InfiniteEarlyExitPlan {
loop_var,
exit_kind,
exit_condition,
exit_value: exit_value_unboxed,
carrier_var: None,
carrier_update: None,
loop_increment,
})))
}
Pattern5ExitKind::Break => {
// Break version: need carrier update + loop increment
// Expected: [carrier_update, increment]
if remaining.len() != 2 {
return Ok(None);
}
// Parse carrier update (sum = sum + 1)
let (carrier_var, carrier_update) = match extract_assignment_parts(&remaining[0]) {
Some(result) => result,
None => return Ok(None),
};
// Parse loop increment (i = i + 1)
let (loop_var, loop_increment) = match extract_assignment_parts(&remaining[1]) {
Some(result) => result,
None => return Ok(None),
};
Ok(Some(DomainPlan::Pattern5InfiniteEarlyExit(Pattern5InfiniteEarlyExitPlan {
loop_var,
exit_kind,
exit_condition,
exit_value: None,
carrier_var: Some(carrier_var),
carrier_update: Some(carrier_update),
loop_increment,
})))
}
}
}
// ============================================================================
// Unit Tests
// ============================================================================

View File

@ -203,6 +203,10 @@ static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
name: "Pattern7_SplitScan (Phase 273)",
extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan),
},
PlanExtractorEntry {
name: "Pattern5_InfiniteEarlyExit (Phase 286 P3.2)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern5::extract_pattern5_plan),
},
PlanExtractorEntry {
name: "Pattern8_BoolPredicateScan (Phase 286 P2.4)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern8::extract_pattern8_plan),

View File

@ -52,15 +52,16 @@ impl NormalizationPlanBox {
// First statement must be a loop with condition `true`
// Phase 131-136 ONLY support loop(true), not loop(i < n) etc.
let is_loop_true = match &remaining[0] {
ASTNode::Loop { condition, .. } => {
let (is_loop_true, loop_body) = match &remaining[0] {
ASTNode::Loop { condition, body, .. } => {
// Only accept loop(true) - literal Bool true
matches!(
let is_true = matches!(
condition.as_ref(),
ASTNode::Literal { value: LiteralValue::Bool(true), .. }
)
);
(is_true, Some(body.as_slice()))
}
_ => false,
_ => (false, None),
};
if !is_loop_true {
if debug {
@ -73,6 +74,31 @@ impl NormalizationPlanBox {
return Ok(None);
}
// Phase 286 P3.2: Reject Pattern5-style loops (if with early return/break)
// These are handled by the Plan line (Pattern5InfiniteEarlyExit), not StepTree
if let Some(body) = loop_body {
if !body.is_empty() {
if let ASTNode::If { then_body, else_body, .. } = &body[0] {
// Check if it's a Pattern5-style if (no else, then contains return or break)
if else_body.is_none() {
let has_early_exit = then_body.iter().any(|stmt| {
matches!(stmt, ASTNode::Return { .. } | ASTNode::Break { .. })
});
if has_early_exit {
if debug {
trace.routing(
"normalization/plan",
func_name,
"Loop body has if with early return/break - Pattern5 (Plan line), returning None",
);
}
return Ok(None);
}
}
}
}
}
// 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

View File

@ -57,6 +57,8 @@ pub(in crate::mir::builder) enum DomainPlan {
Pattern3IfPhi(Pattern3IfPhiPlan),
/// Pattern2: Loop with Conditional Break (Phase 286 P3.1)
Pattern2Break(Pattern2BreakPlan),
/// Pattern5: Infinite Loop with Early Exit (Phase 286 P3.2)
Pattern5InfiniteEarlyExit(Pattern5InfiniteEarlyExitPlan),
}
/// Phase 273 P0: Scan direction for forward/reverse scan
@ -269,6 +271,61 @@ pub(in crate::mir::builder) struct Pattern2BreakPlan {
pub loop_increment: ASTNode,
}
/// Phase 286 P3.2: Exit kind for Pattern5 infinite loop
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::mir::builder) enum Pattern5ExitKind {
/// Early return from function
Return,
/// Break from loop
Break,
}
/// Phase 286 P3.2: Extracted structure for Pattern5 (Infinite Loop with Early Exit)
///
/// This structure contains all the information needed to lower a loop(true) pattern
/// with early exit (return or break).
///
/// # PoC Subset
///
/// - `loop(true)` literal only (not `loop(1)` or truthy)
/// - Return version: `if (cond) { return <expr> }` + `i = i + 1`
/// - Break version: `if (cond) { break }` + `sum = sum + 1` + `i = i + 1` (carrier_update required)
///
/// # CFG Structure (Return version)
/// ```text
/// preheader → header(PHI: i_current) → body(exit_cond)
/// ↑ ↓
/// └───── step ←──────── else path
/// ↓
/// then path: CoreExitPlan::Return
/// ```
///
/// # CFG Structure (Break version)
/// ```text
/// preheader → header(PHI: i, carrier) → body(exit_cond)
/// ↑ ↓
/// └───── step ←────────── else path
/// ↓
/// then path → after_bb(PHI: carrier_out)
/// ```
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct Pattern5InfiniteEarlyExitPlan {
/// Loop variable name (e.g., "i")
pub loop_var: String,
/// Exit kind (Return or Break)
pub exit_kind: Pattern5ExitKind,
/// Exit condition AST (e.g., `i == 3`)
pub exit_condition: ASTNode,
/// Return value expression (Some for Return, None for Break)
pub exit_value: Option<ASTNode>,
/// Carrier variable name (Some for Break with carrier, None for Return)
pub carrier_var: Option<String>,
/// Carrier update expression (Some for Break, None for Return)
pub carrier_update: Option<ASTNode>,
/// Loop increment expression AST (e.g., `i + 1`)
pub loop_increment: ASTNode,
}
// ============================================================================
// CorePlan (固定語彙 - 構造ノードのみ)
// ============================================================================

View File

@ -15,6 +15,7 @@ use super::{
CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan,
ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, Pattern9AccumConstLoopPlan,
Pattern8BoolPredicateScanPlan, Pattern3IfPhiPlan, Pattern2BreakPlan,
Pattern5InfiniteEarlyExitPlan, Pattern5ExitKind,
};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::builder::MirBuilder;
@ -139,6 +140,7 @@ impl PlanNormalizer {
DomainPlan::Pattern8BoolPredicateScan(parts) => Self::normalize_pattern8_bool_predicate_scan(builder, parts, ctx),
DomainPlan::Pattern3IfPhi(parts) => Self::normalize_pattern3_if_phi(builder, parts, ctx),
DomainPlan::Pattern2Break(parts) => Self::normalize_pattern2_break(builder, parts, ctx),
DomainPlan::Pattern5InfiniteEarlyExit(parts) => Self::normalize_pattern5_infinite_early_exit(builder, parts, ctx),
}
}
@ -2740,4 +2742,553 @@ impl PlanNormalizer {
Ok(CorePlan::Loop(loop_plan))
}
/// Pattern5InfiniteEarlyExit → CorePlan 変換 (Phase 286 P3.2)
///
/// Expands infinite loop with early exit into CorePlan.
///
/// # Return version CFG (5 blocks)
/// ```text
/// preheader → header(PHI: i_current) → body(exit_cond)
/// ↑ ↓
/// └───── step ←──────── else path
/// ↓
/// then path: Return (found_bb)
/// ```
///
/// # Break version CFG (6 blocks)
/// ```text
/// preheader → header(PHI: i, carrier) → body(exit_cond)
/// ↑ ↓
/// └───── step ←────────── else path
/// ↓
/// then path → after_bb(PHI: carrier_out)
/// ```
fn normalize_pattern5_infinite_early_exit(
builder: &mut MirBuilder,
parts: Pattern5InfiniteEarlyExitPlan,
ctx: &LoopPatternContext,
) -> Result<CorePlan, String> {
use crate::mir::builder::control_flow::joinir::trace;
let trace_logger = trace::trace();
let debug = ctx.debug;
if debug {
trace_logger.debug(
"normalizer/pattern5_infinite_early_exit",
&format!(
"Phase 286 P3.2: Normalizing Pattern5 for {} (loop_var={}, exit_kind={:?})",
ctx.func_name, parts.loop_var, parts.exit_kind
),
);
}
match parts.exit_kind {
Pattern5ExitKind::Return => {
Self::normalize_pattern5_return(builder, parts, ctx, debug, &trace_logger)
}
Pattern5ExitKind::Break => {
Self::normalize_pattern5_break(builder, parts, ctx, debug, &trace_logger)
}
}
}
/// Pattern5 Return version normalizer
///
/// CFG: preheader → header → body → found_bb (return) / step → header
fn normalize_pattern5_return(
builder: &mut MirBuilder,
parts: Pattern5InfiniteEarlyExitPlan,
ctx: &LoopPatternContext,
debug: bool,
trace_logger: &crate::mir::builder::control_flow::joinir::trace::JoinLoopTrace,
) -> Result<CorePlan, String> {
// Step 1: Block allocation (5 blocks)
let preheader_bb = builder
.current_block
.ok_or_else(|| "Pattern5Return: no current block".to_string())?;
let header_bb = builder.next_block_id();
let body_bb = builder.next_block_id();
let found_bb = builder.next_block_id(); // Return block
let step_bb = builder.next_block_id();
let after_bb = builder.next_block_id(); // Unreachable for infinite loop
if debug {
trace_logger.debug(
"normalizer/pattern5_return",
&format!(
"Block allocation: preheader={:?}, header={:?}, body={:?}, found={:?}, step={:?}, after={:?}",
preheader_bb, header_bb, body_bb, found_bb, step_bb, after_bb
),
);
}
// Step 2: Get initial values from variable_map
let loop_var_init = builder
.variable_ctx
.variable_map
.get(&parts.loop_var)
.copied()
.ok_or_else(|| format!("Pattern5Return: loop_var '{}' not in variable_map", parts.loop_var))?;
// Step 3: ValueId allocation
let loop_var_current = builder.alloc_typed(MirType::Integer); // header PHI dst
let cond_exit = builder.alloc_typed(MirType::Bool); // exit condition
let loop_var_next = builder.alloc_typed(MirType::Integer); // loop var next
let true_val = builder.alloc_typed(MirType::Bool); // for infinite loop
// Step 4: phi_bindings for AST lowering
let phi_bindings = create_phi_bindings(&[(&parts.loop_var, loop_var_current)]);
// Step 5: Lower AST expressions
// 5.1: Exit condition (e.g., `i == 3`)
let (exit_cond_lhs, exit_cond_op, exit_cond_rhs, exit_cond_consts) =
Self::lower_compare_ast(&parts.exit_condition, builder, &phi_bindings)?;
// 5.2: Return value (if any)
let return_value = if let Some(ref exit_val_ast) = parts.exit_value {
let return_val_id = builder.alloc_typed(MirType::Integer);
let return_val_ast = exit_val_ast.clone();
Some((return_val_id, return_val_ast))
} else {
None
};
// 5.3: Loop increment (e.g., `i + 1`)
let (inc_lhs, inc_op, inc_rhs, inc_consts) =
Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?;
// Step 6: Build header_effects
// For infinite loop, we just need a dummy true condition for loop control
let header_effects = vec![
CoreEffectPlan::Const {
dst: true_val,
value: ConstValue::Bool(true),
},
];
// Step 7: Build body plans
let mut body_plans: Vec<CorePlan> = Vec::new();
// Add exit condition consts
for eff in exit_cond_consts {
body_plans.push(CorePlan::Effect(eff));
}
// Add exit condition compare
body_plans.push(CorePlan::Effect(CoreEffectPlan::Compare {
dst: cond_exit,
lhs: exit_cond_lhs,
op: exit_cond_op,
rhs: exit_cond_rhs,
}));
// Step 8: Build found_bb effects (return value computation + Return)
let mut found_effects: Vec<CoreEffectPlan> = Vec::new();
let return_val_id = if let Some((ret_val_id, ref ret_val_ast)) = return_value {
// Lower return value as const if it's a literal
if let crate::ast::ASTNode::Literal { value: crate::ast::LiteralValue::Integer(n), .. } = ret_val_ast {
found_effects.push(CoreEffectPlan::Const {
dst: ret_val_id,
value: ConstValue::Integer(*n),
});
} else {
// For non-literal, use binop lowering (simplified)
let (lhs, op, rhs, consts) = Self::lower_binop_ast(ret_val_ast, builder, &phi_bindings)?;
for c in consts {
found_effects.push(c);
}
found_effects.push(CoreEffectPlan::BinOp {
dst: ret_val_id,
lhs,
op,
rhs,
});
}
Some(ret_val_id)
} else {
None
};
// Step 9: Build step_effects (increment loop var)
let mut step_effects = inc_consts;
step_effects.push(CoreEffectPlan::BinOp {
dst: loop_var_next,
lhs: inc_lhs,
op: inc_op,
rhs: inc_rhs,
});
// Step 10: Build PHIs
let phis = vec![
CorePhiInfo {
block: header_bb,
dst: loop_var_current,
inputs: vec![
(preheader_bb, loop_var_init),
(step_bb, loop_var_next),
],
tag: format!("loop_carrier_{}", parts.loop_var),
},
];
// Step 11: Build block_effects (V10 contract: body effects in loop_plan.body)
let block_effects = vec![
(preheader_bb, vec![]),
(header_bb, header_effects),
(body_bb, vec![]), // V10: body effects in loop_plan.body
(found_bb, found_effects), // Return value computation
(step_bb, step_effects),
(after_bb, vec![]), // Unreachable for infinite loop (needed for CFG)
];
// Step 12: Build Frag with Return exit
let empty_args = EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
};
let branches = vec![
// header → body (always, infinite loop)
BranchStub {
from: header_bb,
cond: true_val,
then_target: body_bb,
else_target: after_bb, // Never taken for infinite loop
then_args: empty_args.clone(),
else_args: empty_args.clone(),
},
// body → found_bb (exit) / step (continue)
BranchStub {
from: body_bb,
cond: cond_exit,
then_target: found_bb,
else_target: step_bb,
then_args: empty_args.clone(),
else_args: empty_args.clone(),
},
];
// Build Return value args
let return_args = EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: if let Some(val_id) = return_val_id {
vec![val_id]
} else {
vec![]
},
};
// Wires: internal jumps + Return terminators (emit_wires handles Return with target=None)
let wires = vec![
// step → header (back-edge)
EdgeStub {
from: step_bb,
kind: ExitKind::Normal,
target: Some(header_bb),
args: empty_args.clone(),
},
// found_bb → Return (early exit)
EdgeStub {
from: found_bb,
kind: ExitKind::Return,
target: None,
args: return_args,
},
// after_bb → Return (unreachable for infinite loop, but CFG needs terminator)
EdgeStub {
from: after_bb,
kind: ExitKind::Return,
target: None,
args: empty_args.clone(),
},
];
let frag = Frag {
entry: header_bb,
exits: BTreeMap::new(), // No exits - all terminators are in wires
wires,
branches,
};
// Step 13: Build final_values
let final_values = vec![
(parts.loop_var.clone(), loop_var_current),
];
// Step 14: Build CoreLoopPlan
let loop_plan = CoreLoopPlan {
preheader_bb,
header_bb,
body_bb,
step_bb,
after_bb,
found_bb,
body: body_plans,
cond_loop: true_val, // Infinite loop
cond_match: cond_exit, // Exit condition for body branching
block_effects,
phis,
frag,
final_values,
};
if debug {
trace_logger.debug(
"normalizer/pattern5_return",
"CorePlan construction complete (5 blocks, 1 PHI, Return exit)",
);
}
Ok(CorePlan::Loop(loop_plan))
}
/// Pattern5 Break version normalizer
///
/// CFG: preheader → header → body → break_then → after / step → header
fn normalize_pattern5_break(
builder: &mut MirBuilder,
parts: Pattern5InfiniteEarlyExitPlan,
ctx: &LoopPatternContext,
debug: bool,
trace_logger: &crate::mir::builder::control_flow::joinir::trace::JoinLoopTrace,
) -> Result<CorePlan, String> {
// Step 1: Block allocation (6 blocks)
let preheader_bb = builder
.current_block
.ok_or_else(|| "Pattern5Break: no current block".to_string())?;
let header_bb = builder.next_block_id();
let body_bb = builder.next_block_id();
let break_then_bb = builder.next_block_id();
let step_bb = builder.next_block_id();
let after_bb = builder.next_block_id();
if debug {
trace_logger.debug(
"normalizer/pattern5_break",
&format!(
"Block allocation: preheader={:?}, header={:?}, body={:?}, break_then={:?}, step={:?}, after={:?}",
preheader_bb, header_bb, body_bb, break_then_bb, step_bb, after_bb
),
);
}
// Step 2: Get carrier_var (required for Break version)
let carrier_var = parts.carrier_var.as_ref()
.ok_or_else(|| "Pattern5Break: carrier_var required for Break version".to_string())?;
let carrier_update_ast = parts.carrier_update.as_ref()
.ok_or_else(|| "Pattern5Break: carrier_update required for Break version".to_string())?;
// Step 3: Get initial values from variable_map
let loop_var_init = builder
.variable_ctx
.variable_map
.get(&parts.loop_var)
.copied()
.ok_or_else(|| format!("Pattern5Break: loop_var '{}' not in variable_map", parts.loop_var))?;
let carrier_init = builder
.variable_ctx
.variable_map
.get(carrier_var)
.copied()
.ok_or_else(|| format!("Pattern5Break: carrier_var '{}' not in variable_map", carrier_var))?;
// Step 4: ValueId allocation
let loop_var_current = builder.alloc_typed(MirType::Integer); // header PHI dst
let carrier_current = builder.alloc_typed(MirType::Integer); // header PHI dst
let cond_exit = builder.alloc_typed(MirType::Bool); // exit condition
let carrier_step = builder.alloc_typed(MirType::Integer); // step path carrier
let loop_var_next = builder.alloc_typed(MirType::Integer); // loop var next
let carrier_out = builder.alloc_typed(MirType::Integer); // after_bb PHI dst
let true_val = builder.alloc_typed(MirType::Bool); // for infinite loop
// Step 5: phi_bindings for AST lowering
let phi_bindings = create_phi_bindings(&[
(&parts.loop_var, loop_var_current),
(carrier_var, carrier_current),
]);
// Step 6: Lower AST expressions
// 6.1: Exit condition (e.g., `i == 3`)
let (exit_cond_lhs, exit_cond_op, exit_cond_rhs, exit_cond_consts) =
Self::lower_compare_ast(&parts.exit_condition, builder, &phi_bindings)?;
// 6.2: Carrier update (e.g., `sum + 1`)
let (carrier_lhs, carrier_op, carrier_rhs, carrier_consts) =
Self::lower_binop_ast(carrier_update_ast, builder, &phi_bindings)?;
// 6.3: Loop increment (e.g., `i + 1`)
let (inc_lhs, inc_op, inc_rhs, inc_consts) =
Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?;
// Step 7: Build header_effects
let header_effects = vec![
CoreEffectPlan::Const {
dst: true_val,
value: ConstValue::Bool(true),
},
];
// Step 8: Build body plans (exit condition check)
let mut body_plans: Vec<CorePlan> = Vec::new();
for eff in exit_cond_consts {
body_plans.push(CorePlan::Effect(eff));
}
body_plans.push(CorePlan::Effect(CoreEffectPlan::Compare {
dst: cond_exit,
lhs: exit_cond_lhs,
op: exit_cond_op,
rhs: exit_cond_rhs,
}));
// Step 9: Build break_then_effects (empty - carrier_current goes directly to after PHI)
let break_then_effects = vec![];
// Step 10: Build step_effects (carrier update + loop increment)
let mut step_effects = carrier_consts;
step_effects.push(CoreEffectPlan::BinOp {
dst: carrier_step,
lhs: carrier_lhs,
op: carrier_op,
rhs: carrier_rhs,
});
for c in inc_consts {
step_effects.push(c);
}
step_effects.push(CoreEffectPlan::BinOp {
dst: loop_var_next,
lhs: inc_lhs,
op: inc_op,
rhs: inc_rhs,
});
// Step 11: Build PHIs (3 total)
let phis = vec![
CorePhiInfo {
block: header_bb,
dst: loop_var_current,
inputs: vec![
(preheader_bb, loop_var_init),
(step_bb, loop_var_next),
],
tag: format!("loop_carrier_{}", parts.loop_var),
},
CorePhiInfo {
block: header_bb,
dst: carrier_current,
inputs: vec![
(preheader_bb, carrier_init),
(step_bb, carrier_step),
],
tag: format!("loop_carrier_{}", carrier_var),
},
// after_bb PHI: carrier_out from break_then (carrier_current) or step (carrier_step)
// But for infinite loop with break, only break_then reaches after_bb
CorePhiInfo {
block: after_bb,
dst: carrier_out,
inputs: vec![
(break_then_bb, carrier_current), // break path: current carrier value
],
tag: format!("exit_carrier_{}", carrier_var),
},
];
// Step 12: Build block_effects (V10 contract)
let block_effects = vec![
(preheader_bb, vec![]),
(header_bb, header_effects),
(body_bb, vec![]), // V10: body effects in loop_plan.body
(break_then_bb, break_then_effects),
(step_bb, step_effects),
];
// Step 13: Build Frag
let empty_args = EdgeArgs {
layout: JumpArgsLayout::CarriersOnly,
values: vec![],
};
let branches = vec![
// header → body (always, infinite loop)
BranchStub {
from: header_bb,
cond: true_val,
then_target: body_bb,
else_target: after_bb, // Never taken for infinite loop
then_args: empty_args.clone(),
else_args: empty_args.clone(),
},
// body → break_then (exit) / step (continue)
BranchStub {
from: body_bb,
cond: cond_exit,
then_target: break_then_bb,
else_target: step_bb,
then_args: empty_args.clone(),
else_args: empty_args.clone(),
},
];
// Use fixed LoopId(0) for internal break wiring
let loop_id = crate::mir::control_form::LoopId(0);
let wires = vec![
// break_then → after
EdgeStub {
from: break_then_bb,
kind: ExitKind::Break(loop_id),
target: Some(after_bb),
args: empty_args.clone(),
},
// step → header (back-edge)
EdgeStub {
from: step_bb,
kind: ExitKind::Normal,
target: Some(header_bb),
args: empty_args.clone(),
},
];
let frag = Frag {
entry: header_bb,
exits: BTreeMap::new(),
wires,
branches,
};
// Step 14: Build final_values
let final_values = vec![
(parts.loop_var.clone(), loop_var_current),
(carrier_var.clone(), carrier_out), // KEY: after PHI dst!
];
// Step 15: Build CoreLoopPlan
let loop_plan = CoreLoopPlan {
preheader_bb,
header_bb,
body_bb,
step_bb,
after_bb,
found_bb: after_bb, // No early exit via found (break goes to after)
body: body_plans,
cond_loop: true_val, // Infinite loop
cond_match: cond_exit, // Exit condition for body branching
block_effects,
phis,
frag,
final_values,
};
if debug {
trace_logger.debug(
"normalizer/pattern5_break",
"CorePlan construction complete (6 blocks, 3 PHIs, after_bb PHI for carrier)",
);
}
Ok(CorePlan::Loop(loop_plan))
}
}

View File

@ -0,0 +1,35 @@
#!/bin/bash
# Phase 286 P3.2: Pattern5 "infinite loop with break" test
# Tests: loop(true) with if (i == 3) { break } and carrier update
#
# Expected: Output "3" (sum = 0 + 1 + 1 + 1 = 3 for i = 0, 1, 2)
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase286_pattern5_break_min.hako"
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
set +e
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$INPUT" 2>&1)
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" -eq 124 ]; then
test_fail "phase286_pattern5_break_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
# Expected output: "3" (may be "RC: 3" or "3" format)
if echo "$OUTPUT" | grep -qE "(^3$|RC: 3$)"; then
test_pass "phase286_pattern5_break_vm: Pattern5 break succeeded (output: 3)"
exit 0
else
echo "[FAIL] Unexpected output (expected: 3)"
echo "[INFO] Exit code: $EXIT_CODE"
echo "[INFO] Output:"
echo "$OUTPUT" | head -n 20 || true
test_fail "phase286_pattern5_break_vm: Unexpected output"
exit 1
fi

View File

@ -0,0 +1,35 @@
#!/bin/bash
# Phase 286 P3.2: Pattern5 "infinite loop with early return" test
# Tests: loop(true) with if (i == 3) { return 7 }
#
# Expected: Output "7"
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase286_pattern5_return_min.hako"
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
set +e
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$INPUT" 2>&1)
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" -eq 124 ]; then
test_fail "phase286_pattern5_return_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
# Expected output: "7" (may be "RC: 7" or "7" format)
if echo "$OUTPUT" | grep -qE "(^7$|RC: 7$)"; then
test_pass "phase286_pattern5_return_vm: Pattern5 early-return succeeded (output: 7)"
exit 0
else
echo "[FAIL] Unexpected output (expected: 7)"
echo "[INFO] Exit code: $EXIT_CODE"
echo "[INFO] Output:"
echo "$OUTPUT" | head -n 20 || true
test_fail "phase286_pattern5_return_vm: Unexpected output"
exit 1
fi