feat(joinir): Phase 286 P3.1 - Pattern2 Plan line 完走(after_bb PHI)
Pattern2(Loop with Break)を Plan/Frag SSOT へ移行。 主な変更: - Pattern2BreakPlan 追加(DomainPlan variant) - extract_pattern2_plan() 実装(PoC サブセット厳守) - normalize_pattern2_break() 実装(6-block CFG, 3 PHI) - after_bb PHI が本質: carrier_out = PHI(header: carrier_current, break_then: carrier_break) - router に Pattern2 追加(Pattern1 より前、より具体的) テスト: - Fixture B (break without update): PASS (出力 11) - 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:
35
apps/tests/phase286_pattern2_break_no_update_min.hako
Normal file
35
apps/tests/phase286_pattern2_break_no_update_min.hako
Normal file
@ -0,0 +1,35 @@
|
||||
// Phase 286 P3.1: Pattern2 "break without update" minimal fixture
|
||||
//
|
||||
// Purpose: Test Pattern2 where break path has NO carrier update
|
||||
// Expected output: 11
|
||||
//
|
||||
// Structure:
|
||||
// i = 0, sum = 10 // non-zero init to clarify intent
|
||||
// loop(i < 3) {
|
||||
// if (i == 1) { break } // NO sum update before break!
|
||||
// sum = sum + 1 // i==0 only: sum = 11
|
||||
// i = i + 1
|
||||
// }
|
||||
// return sum // expects 11 (value at break point)
|
||||
//
|
||||
// Key insight (after_bb PHI):
|
||||
// - break path: carrier_break = carrier_current (no update)
|
||||
// - natural exit: carrier_out = carrier_current (from header)
|
||||
// - after_bb PHI: carrier_out = PHI(header: carrier_current, break_then: carrier_break)
|
||||
//
|
||||
// This fixture tests the case where carrier_break == carrier_current
|
||||
// (i.e., no intermediate update before break)
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local i, sum
|
||||
i = 0
|
||||
sum = 10
|
||||
loop(i < 3) {
|
||||
if (i == 1) { break }
|
||||
sum = sum + 1
|
||||
i = i + 1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
# Phase 286: JoinIR Line Absorption(JoinIR→CorePlan/Frag 収束)
|
||||
|
||||
Status: In Progress (P0, P1, P2, P2.1, P2.2, P2.3, P2.4, P2.4.1, P2.6, P2.7 COMPLETE, P3, 286C-2 COMPLETE)
|
||||
Status: In Progress (P0, P1, P2, P2.1, P2.2, P2.3, P2.4, P2.4.1, P2.6, P2.7, P3, 286C-2 COMPLETE, **P3.1 IN PROGRESS**)
|
||||
|
||||
## Goal
|
||||
|
||||
@ -322,6 +322,64 @@ Phase 286 では JoinIR line を “第2の lowerer” として放置せず、*
|
||||
- Build: cargo build --release PASS
|
||||
- Regression: quick smoke 0 failed
|
||||
|
||||
### P3.1 (Pattern2 Plan化 - Loop with Break) 🚧 IN PROGRESS (2025-12-26)
|
||||
|
||||
**背景**:
|
||||
- Pattern2 (Loop with Break) は P2 で別タスク化された(break経路の値再接続が複雑)
|
||||
- 詳細: [pattern2-deferred.md](./pattern2-deferred.md)
|
||||
|
||||
**本質的課題: after_bb PHI**:
|
||||
- break経路では carrier 更新が実行されない場合がある
|
||||
- after_bb に PHI 必要(header経路 vs break経路の値選択)
|
||||
```
|
||||
carrier_out = PHI(header: carrier_current, break_then: carrier_break)
|
||||
```
|
||||
|
||||
**CFG構造**(6ブロック):
|
||||
```
|
||||
preheader → header(PHI: i_current, carrier_current)
|
||||
↓
|
||||
body(break_cond check)
|
||||
↓
|
||||
┌────┴────┐
|
||||
break_then step
|
||||
(optional ↓
|
||||
update) header (back-edge)
|
||||
↓
|
||||
after_bb(PHI: carrier_out)
|
||||
↑
|
||||
header (natural exit when !cond_loop)
|
||||
```
|
||||
|
||||
**実装ステップ**:
|
||||
1. ✅ Step 0: docs-first - README.md P3.1節追加、pattern2-deferred.md更新
|
||||
2. Step 1: Fixture B作成 + smoke script追加
|
||||
3. Step 2: DomainPlan::Pattern2Break + extract_pattern2_plan() 追加
|
||||
4. Step 3: normalize_pattern2_break() 実装(after_bb PHI)
|
||||
5. Step 4: router に Pattern2 追加
|
||||
6. Step 5: 検証(integration 2本 + quick 154/154)
|
||||
|
||||
**PoC サブセット厳守**:
|
||||
以下は必ず `Ok(None)` で legacy へ fallback(Fail-Fast 回帰防止):
|
||||
- loop_increment が取れない(構造が複雑)
|
||||
- break_cond が単一 if でない(ネスト、複数条件)
|
||||
- break_then が複数文で carrier 更新が特定できない
|
||||
- carrier が複数(PoC は single carrier のみ)
|
||||
- body 側の carrier 更新が特定できない
|
||||
|
||||
**成果物** (予定):
|
||||
- `apps/tests/phase286_pattern2_break_no_update_min.hako` (新規: break without update fixture)
|
||||
- `tools/smokes/v2/profiles/integration/apps/phase286_pattern2_break_no_update_vm.sh` (新規)
|
||||
- `src/mir/builder/control_flow/plan/mod.rs` (変更: Pattern2BreakPlan + DomainPlan variant)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs` (変更: extract_pattern2_plan)
|
||||
- `src/mir/builder/control_flow/plan/normalizer.rs` (変更: normalize_pattern2_break)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/router.rs` (変更: Pattern2 Plan line routing)
|
||||
|
||||
**成功基準**:
|
||||
- Fixture A (break with update): `phase286_pattern2_frag_poc` PASS (出力 42)
|
||||
- Fixture B (break without update): `phase286_pattern2_break_no_update_vm` PASS (出力 11)
|
||||
- Regression: quick smoke 154 PASS, 0 FAILED
|
||||
|
||||
## Acceptance(P0)
|
||||
|
||||
- 2本の lowering が "設計として" どこで 1 本に収束するかが明文化されている
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
# Pattern2 (Loop with Break) - Deferred to Future Phase
|
||||
# Pattern2 (Loop with Break) - Phase 286 P3 で再開
|
||||
|
||||
**Date**: 2025-12-26
|
||||
**Status**: Investigation complete, implementation deferred
|
||||
**Status**: **IN PROGRESS** (Phase 286 P3)
|
||||
|
||||
## Summary
|
||||
|
||||
Pattern2 requires complex value reconnection at the exit point:
|
||||
- break経路ではcarrier更新が実行されない
|
||||
- after_bbにPHI必要(header経路 vs break経路の値選択)
|
||||
- **after_bb に PHI 必要**(header経路 vs break経路の値選択)
|
||||
- compose::loop_との統合、ExitKind::Break配線が必要
|
||||
|
||||
Pattern4 (Loop with Continue) PoC成功後の別タスクとして実装予定。
|
||||
## 実装方針(Phase 286 P3)
|
||||
|
||||
## 再開条件
|
||||
**after_bb PHI が本質**:
|
||||
```
|
||||
carrier_out = PHI(header: carrier_current, break_then: carrier_break)
|
||||
```
|
||||
|
||||
以下が揃った時に Pattern2 Plan化を再開(Phase番号は到達点に応じて後で確定):
|
||||
1. after_bb PHI 生成ロジックの設計完了
|
||||
2. compose::loop_ との統合方針決定
|
||||
3. ExitKind::Break wiring の実装
|
||||
- break 前に update あり → carrier_break = 計算結果
|
||||
- break 前に update なし → carrier_break = carrier_current(そのまま)
|
||||
|
||||
→ 条件が揃ったら Phase 286 の次の空きスロットにエスカレート
|
||||
**PoC サブセット厳守**: 取れない形は `Ok(None)` で legacy へ(Fail-Fast 回帰防止)
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
//! Phase 282 P4: Pattern2 (Loop with Conditional Break) Extraction
|
||||
//! Phase 282 P9a: Integrated with common_helpers
|
||||
//! Phase 286 P3.1: Added extract_pattern2_plan() for Plan/Frag SSOT
|
||||
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
|
||||
|
||||
@ -123,6 +124,163 @@ fn has_return_statement(body: &[ASTNode]) -> bool {
|
||||
common_has_return(body)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Phase 286 P3.1: Plan line extraction
|
||||
// ============================================================================
|
||||
|
||||
/// Extract Pattern2 Plan for Plan/Frag SSOT migration
|
||||
///
|
||||
/// # PoC Subset (Strict - returns Ok(None) for unsupported forms)
|
||||
///
|
||||
/// Supported form:
|
||||
/// ```hako
|
||||
/// loop(i < N) {
|
||||
/// if (break_cond) { [carrier = expr;] break }
|
||||
/// carrier = carrier + expr
|
||||
/// i = i + 1
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Returns Ok(None) for:
|
||||
/// - loop_increment not extractable (complex structure)
|
||||
/// - break_cond not single if (nested, multiple conditions)
|
||||
/// - break_then has multiple statements and carrier update not identifiable
|
||||
/// - Multiple carriers (PoC is single carrier only)
|
||||
/// - Body carrier update not identifiable
|
||||
///
|
||||
/// This prevents Fail-Fast regression by falling back to legacy JoinIR line.
|
||||
pub(crate) fn extract_pattern2_plan(
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String> {
|
||||
use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern2BreakPlan};
|
||||
|
||||
// Step 1: Validate via existing extractor (has break, no continue/return)
|
||||
let parts = extract_loop_with_break_parts(condition, body)?;
|
||||
if parts.is_none() {
|
||||
return Ok(None); // Not Pattern2 → legacy fallback
|
||||
}
|
||||
let parts = parts.unwrap();
|
||||
|
||||
// Step 2: PoC constraint - only support non-loop(true) for now
|
||||
if parts.is_loop_true {
|
||||
return Ok(None); // loop(true) is complex → legacy fallback
|
||||
}
|
||||
|
||||
// Step 3: PoC constraint - only support single break
|
||||
if parts.break_count != 1 {
|
||||
return Ok(None); // Multiple breaks → legacy fallback
|
||||
}
|
||||
|
||||
// Step 4: Body structure must be: if { ... break } + carrier_update + loop_increment
|
||||
// Expected structure:
|
||||
// body[0] = If { then_body contains break }
|
||||
// body[1] = carrier update assignment
|
||||
// body[2] = loop_var update assignment
|
||||
if body.len() != 3 {
|
||||
return Ok(None); // Unexpected body length → legacy fallback
|
||||
}
|
||||
|
||||
// Step 4.1: First element must be an If with break in then_body
|
||||
let (break_condition, carrier_update_in_break) = match &body[0] {
|
||||
ASTNode::If { condition: if_cond, then_body, else_body, .. } => {
|
||||
// PoC: No else branch allowed
|
||||
if else_body.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Extract break condition
|
||||
let break_cond = if_cond.as_ref().clone();
|
||||
|
||||
// Check if then_body ends with break
|
||||
let has_break_at_end = then_body.last()
|
||||
.map(|n| matches!(n, ASTNode::Break { .. }))
|
||||
.unwrap_or(false);
|
||||
if !has_break_at_end {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Extract carrier update in break (if any)
|
||||
// If then_body is [break], no update
|
||||
// If then_body is [assignment, break], extract assignment
|
||||
let carrier_update = if then_body.len() == 1 {
|
||||
None // Just break, no update
|
||||
} else if then_body.len() == 2 {
|
||||
// Should be [assignment, break]
|
||||
match &then_body[0] {
|
||||
ASTNode::Assignment { value, .. } => Some(value.as_ref().clone()),
|
||||
_ => return Ok(None), // Not an assignment → legacy
|
||||
}
|
||||
} else {
|
||||
return Ok(None); // Complex then_body → legacy
|
||||
};
|
||||
|
||||
(break_cond, carrier_update)
|
||||
}
|
||||
_ => return Ok(None), // First element not If → legacy
|
||||
};
|
||||
|
||||
// Step 4.2: Extract carrier update in body (second element)
|
||||
let (carrier_var, carrier_update_in_body) = match &body[1] {
|
||||
ASTNode::Assignment { target, value, .. } => {
|
||||
let carrier_name = match target.as_ref() {
|
||||
ASTNode::Variable { name, .. } => name.clone(),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
(carrier_name, value.as_ref().clone())
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
// Step 4.3: Extract loop increment (third element)
|
||||
let loop_increment = match super::common_helpers::extract_loop_increment_plan(body, &parts.loop_var)? {
|
||||
Some(inc) => inc,
|
||||
None => return Ok(None), // No loop increment found → legacy
|
||||
};
|
||||
|
||||
// Step 5: Validate loop condition is `<var> < <int_lit>` (same as Pattern1)
|
||||
if !validate_loop_condition_for_plan(condition, &parts.loop_var) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(DomainPlan::Pattern2Break(Pattern2BreakPlan {
|
||||
loop_var: parts.loop_var,
|
||||
carrier_var,
|
||||
loop_condition: condition.clone(),
|
||||
break_condition,
|
||||
carrier_update_in_break,
|
||||
carrier_update_in_body,
|
||||
loop_increment,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Validate loop condition: supports `<var> < <int_lit>` only (same as Pattern1)
|
||||
fn validate_loop_condition_for_plan(cond: &ASTNode, loop_var: &str) -> bool {
|
||||
if let ASTNode::BinaryOp { operator, left, right, .. } = cond {
|
||||
if !matches!(operator, BinaryOperator::Less) {
|
||||
return false; // Only < supported for PoC
|
||||
}
|
||||
|
||||
// Left must be the loop variable
|
||||
if let ASTNode::Variable { name, .. } = left.as_ref() {
|
||||
if name != loop_var {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Right must be integer literal
|
||||
if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -219,6 +219,10 @@ static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
|
||||
name: "Pattern9_AccumConstLoop (Phase 286 P2.3)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern9::extract_pattern9_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern2_Break (Phase 286 P3.1)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern2::extract_pattern2_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern1_SimpleWhile (Phase 286 P2.1)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern1::extract_pattern1_plan),
|
||||
|
||||
@ -55,6 +55,8 @@ pub(in crate::mir::builder) enum DomainPlan {
|
||||
Pattern8BoolPredicateScan(Pattern8BoolPredicateScanPlan),
|
||||
/// Pattern3: Loop with If-Phi (Phase 286 P2.6)
|
||||
Pattern3IfPhi(Pattern3IfPhiPlan),
|
||||
/// Pattern2: Loop with Conditional Break (Phase 286 P3.1)
|
||||
Pattern2Break(Pattern2BreakPlan),
|
||||
}
|
||||
|
||||
/// Phase 273 P0: Scan direction for forward/reverse scan
|
||||
@ -225,6 +227,48 @@ pub(in crate::mir::builder) struct Pattern3IfPhiPlan {
|
||||
pub loop_increment: ASTNode,
|
||||
}
|
||||
|
||||
/// Phase 286 P3.1: Extracted structure for Pattern2 (Loop with Conditional Break)
|
||||
///
|
||||
/// This structure contains all the information needed to lower a break-style loop.
|
||||
///
|
||||
/// Key insight: after_bb PHI merges break path and natural exit path carrier values.
|
||||
/// - break path: carrier_break = carrier_update_in_break (if Some) or carrier_current (if None)
|
||||
/// - natural exit: carrier_out = carrier_current (from header PHI)
|
||||
/// - after_bb PHI: carrier_out = PHI(header: carrier_current, break_then: carrier_break)
|
||||
///
|
||||
/// CFG structure (6 blocks):
|
||||
/// ```
|
||||
/// preheader → header(PHI: i_current, carrier_current)
|
||||
/// ↓
|
||||
/// body(break_cond check)
|
||||
/// ↓
|
||||
/// ┌────┴────┐
|
||||
/// break_then step
|
||||
/// (optional ↓
|
||||
/// update) header (back-edge)
|
||||
/// ↓
|
||||
/// after_bb(PHI: carrier_out)
|
||||
/// ↑
|
||||
/// header (natural exit when !cond_loop)
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub(in crate::mir::builder) struct Pattern2BreakPlan {
|
||||
/// Loop variable name (e.g., "i")
|
||||
pub loop_var: String,
|
||||
/// Carrier variable name (e.g., "sum", "result")
|
||||
pub carrier_var: String,
|
||||
/// Loop condition AST (e.g., `i < 3`)
|
||||
pub loop_condition: ASTNode,
|
||||
/// Break condition AST (e.g., `i == 1`)
|
||||
pub break_condition: ASTNode,
|
||||
/// Carrier update in break path (None if no update before break)
|
||||
pub carrier_update_in_break: Option<ASTNode>,
|
||||
/// Carrier update in normal body path (e.g., `sum + 1`)
|
||||
pub carrier_update_in_body: ASTNode,
|
||||
/// Loop increment expression AST (e.g., `i + 1`)
|
||||
pub loop_increment: ASTNode,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CorePlan (固定語彙 - 構造ノードのみ)
|
||||
// ============================================================================
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
use super::{
|
||||
CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan,
|
||||
ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, Pattern9AccumConstLoopPlan,
|
||||
Pattern8BoolPredicateScanPlan, Pattern3IfPhiPlan,
|
||||
Pattern8BoolPredicateScanPlan, Pattern3IfPhiPlan, Pattern2BreakPlan,
|
||||
};
|
||||
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
@ -138,6 +138,7 @@ impl PlanNormalizer {
|
||||
DomainPlan::Pattern9AccumConstLoop(parts) => Self::normalize_pattern9_accum_const_loop(builder, parts, ctx),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -2434,4 +2435,309 @@ impl PlanNormalizer {
|
||||
|
||||
Ok(CorePlan::Loop(loop_plan))
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Phase 286 P3.1: Pattern2 (Loop with Conditional Break) normalization
|
||||
// =========================================================================
|
||||
|
||||
/// Pattern2Break → CorePlan 変換
|
||||
///
|
||||
/// CFG structure (6 blocks):
|
||||
/// ```
|
||||
/// preheader → header(PHI: i_current, carrier_current)
|
||||
/// ↓
|
||||
/// body(break_cond check)
|
||||
/// ↓
|
||||
/// ┌────┴────┐
|
||||
/// break_then step
|
||||
/// (optional ↓
|
||||
/// update) header (back-edge)
|
||||
/// ↓
|
||||
/// after_bb(PHI: carrier_out)
|
||||
/// ↑
|
||||
/// header (natural exit when !cond_loop)
|
||||
/// ```
|
||||
///
|
||||
/// Key: after_bb PHI merges break path and natural exit path carrier values.
|
||||
fn normalize_pattern2_break(
|
||||
builder: &mut MirBuilder,
|
||||
parts: Pattern2BreakPlan,
|
||||
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/pattern2_break",
|
||||
&format!(
|
||||
"Phase 286 P3.1: Normalizing Pattern2Break for {} (loop_var={}, carrier_var={})",
|
||||
ctx.func_name, parts.loop_var, parts.carrier_var
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: Block allocation (6 blocks)
|
||||
let preheader_bb = builder
|
||||
.current_block
|
||||
.ok_or_else(|| "Pattern2Break: 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/pattern2_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 initial values from variable_map
|
||||
let loop_var_init = builder
|
||||
.variable_ctx
|
||||
.variable_map
|
||||
.get(&parts.loop_var)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("Pattern2Break: loop_var '{}' not in variable_map", parts.loop_var))?;
|
||||
|
||||
let carrier_init = builder
|
||||
.variable_ctx
|
||||
.variable_map
|
||||
.get(&parts.carrier_var)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("Pattern2Break: carrier_var '{}' not in variable_map", parts.carrier_var))?;
|
||||
|
||||
// Step 3: 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_loop = builder.alloc_typed(MirType::Bool); // loop condition
|
||||
let cond_break = builder.alloc_typed(MirType::Bool); // break condition
|
||||
let carrier_break = builder.alloc_typed(MirType::Integer); // break path carrier
|
||||
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
|
||||
|
||||
// Step 4: phi_bindings for AST lowering
|
||||
let phi_bindings = create_phi_bindings(&[
|
||||
(&parts.loop_var, loop_var_current),
|
||||
(&parts.carrier_var, carrier_current),
|
||||
]);
|
||||
|
||||
// Step 5: Lower AST expressions
|
||||
// 5.1: Loop condition (e.g., `i < 3`)
|
||||
let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) =
|
||||
Self::lower_compare_ast(&parts.loop_condition, builder, &phi_bindings)?;
|
||||
|
||||
// 5.2: Break condition (e.g., `i == 1`)
|
||||
let (break_cond_lhs, break_cond_op, break_cond_rhs, break_cond_consts) =
|
||||
Self::lower_compare_ast(&parts.break_condition, builder, &phi_bindings)?;
|
||||
|
||||
// 5.3: Carrier update in break path (if any)
|
||||
let break_then_effects = if let Some(ref break_update_ast) = parts.carrier_update_in_break {
|
||||
let (lhs, op, rhs, consts) = Self::lower_binop_ast(break_update_ast, builder, &phi_bindings)?;
|
||||
let mut effects = consts;
|
||||
effects.push(CoreEffectPlan::BinOp {
|
||||
dst: carrier_break,
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
});
|
||||
effects
|
||||
} else {
|
||||
// No update: carrier_break = carrier_current (copy via Add 0)
|
||||
let zero = builder.alloc_typed(MirType::Integer);
|
||||
vec![
|
||||
CoreEffectPlan::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
},
|
||||
CoreEffectPlan::BinOp {
|
||||
dst: carrier_break,
|
||||
lhs: carrier_current,
|
||||
op: BinaryOp::Add,
|
||||
rhs: zero,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
// 5.4: Carrier update in body (step path) (e.g., `sum + 1`)
|
||||
let (carrier_lhs, carrier_op, carrier_rhs, carrier_consts) =
|
||||
Self::lower_binop_ast(&parts.carrier_update_in_body, builder, &phi_bindings)?;
|
||||
|
||||
// 5.5: Loop increment (e.g., `i + 1`)
|
||||
let (loop_inc_lhs, loop_inc_op, loop_inc_rhs, loop_inc_consts) =
|
||||
Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?;
|
||||
|
||||
// Step 6: Build header_effects (loop condition)
|
||||
let mut header_effects = loop_cond_consts;
|
||||
header_effects.push(CoreEffectPlan::Compare {
|
||||
dst: cond_loop,
|
||||
lhs: loop_cond_lhs,
|
||||
op: loop_cond_op,
|
||||
rhs: loop_cond_rhs,
|
||||
});
|
||||
|
||||
// Step 7: Build body plans (break condition check)
|
||||
// Body is in loop_plan.body, not block_effects (V10 contract)
|
||||
let mut body_plans: Vec<CorePlan> = Vec::new();
|
||||
for const_effect in break_cond_consts {
|
||||
body_plans.push(CorePlan::Effect(const_effect));
|
||||
}
|
||||
body_plans.push(CorePlan::Effect(CoreEffectPlan::Compare {
|
||||
dst: cond_break,
|
||||
lhs: break_cond_lhs,
|
||||
op: break_cond_op,
|
||||
rhs: break_cond_rhs,
|
||||
}));
|
||||
|
||||
// Step 8: 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,
|
||||
});
|
||||
step_effects.extend(loop_inc_consts);
|
||||
step_effects.push(CoreEffectPlan::BinOp {
|
||||
dst: loop_var_next,
|
||||
lhs: loop_inc_lhs,
|
||||
op: loop_inc_op,
|
||||
rhs: loop_inc_rhs,
|
||||
});
|
||||
|
||||
// Step 9: Build block_effects (V10 compliant: body_bb empty)
|
||||
let block_effects = vec![
|
||||
(preheader_bb, vec![]),
|
||||
(header_bb, header_effects),
|
||||
(body_bb, vec![]), // V10: body effects in body plans, not block_effects
|
||||
(break_then_bb, break_then_effects),
|
||||
(step_bb, step_effects),
|
||||
];
|
||||
|
||||
// Step 10: Build PHIs (3 total)
|
||||
let phis = vec![
|
||||
// Header PHI: loop_var_current
|
||||
CorePhiInfo {
|
||||
block: header_bb,
|
||||
dst: loop_var_current,
|
||||
inputs: vec![
|
||||
(preheader_bb, loop_var_init),
|
||||
(step_bb, loop_var_next),
|
||||
],
|
||||
tag: format!("loop_var_{}", parts.loop_var),
|
||||
},
|
||||
// Header PHI: carrier_current
|
||||
CorePhiInfo {
|
||||
block: header_bb,
|
||||
dst: carrier_current,
|
||||
inputs: vec![
|
||||
(preheader_bb, carrier_init),
|
||||
(step_bb, carrier_step),
|
||||
],
|
||||
tag: format!("carrier_{}", parts.carrier_var),
|
||||
},
|
||||
// After PHI: carrier_out (KEY PHI for Pattern2!)
|
||||
CorePhiInfo {
|
||||
block: after_bb,
|
||||
dst: carrier_out,
|
||||
inputs: vec![
|
||||
(header_bb, carrier_current), // natural exit
|
||||
(break_then_bb, carrier_break), // break path
|
||||
],
|
||||
tag: format!("after_{}", parts.carrier_var),
|
||||
},
|
||||
];
|
||||
|
||||
// Step 11: Build Frag (CFG structure)
|
||||
let empty_args = EdgeArgs {
|
||||
layout: JumpArgsLayout::CarriersOnly,
|
||||
values: vec![],
|
||||
};
|
||||
|
||||
let branches = vec![
|
||||
// header: cond_loop → body / after
|
||||
BranchStub {
|
||||
from: header_bb,
|
||||
cond: cond_loop,
|
||||
then_target: body_bb,
|
||||
else_target: after_bb,
|
||||
then_args: empty_args.clone(),
|
||||
else_args: empty_args.clone(),
|
||||
},
|
||||
// body: cond_break → break_then / step
|
||||
BranchStub {
|
||||
from: body_bb,
|
||||
cond: cond_break,
|
||||
then_target: break_then_bb,
|
||||
else_target: step_bb,
|
||||
then_args: empty_args.clone(),
|
||||
else_args: empty_args.clone(),
|
||||
},
|
||||
];
|
||||
|
||||
let wires = vec![
|
||||
// break_then → after
|
||||
EdgeStub {
|
||||
from: break_then_bb,
|
||||
kind: ExitKind::Normal,
|
||||
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 12: Build final_values
|
||||
// carrier returns after_bb PHI dst (carrier_out)
|
||||
let final_values = vec![
|
||||
(parts.loop_var.clone(), loop_var_current),
|
||||
(parts.carrier_var.clone(), carrier_out), // KEY: after PHI dst!
|
||||
];
|
||||
|
||||
// Step 13: Build CoreLoopPlan
|
||||
let loop_plan = CoreLoopPlan {
|
||||
preheader_bb,
|
||||
header_bb,
|
||||
body_bb,
|
||||
step_bb,
|
||||
after_bb,
|
||||
found_bb: after_bb, // No early exit (break goes to after)
|
||||
body: body_plans,
|
||||
cond_loop,
|
||||
cond_match: cond_break, // Break condition for body branching
|
||||
block_effects,
|
||||
phis,
|
||||
frag,
|
||||
final_values,
|
||||
};
|
||||
|
||||
if debug {
|
||||
trace_logger.debug(
|
||||
"normalizer/pattern2_break",
|
||||
"CorePlan construction complete (6 blocks, 3 PHIs, after_bb PHI is key!)",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(CorePlan::Loop(loop_plan))
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Phase 286 P3.1: Pattern2 "break without update" test
|
||||
# Tests: Pattern2 where break path has NO carrier update
|
||||
#
|
||||
# Key insight (after_bb PHI):
|
||||
# - break path: carrier_break = carrier_current (no update)
|
||||
# - after_bb PHI: carrier_out = PHI(header: carrier_current, break_then: carrier_break)
|
||||
#
|
||||
# Expected: Output "11" (sum=10 init, sum+1 at i==0, break at i==1)
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase286_pattern2_break_no_update_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_pattern2_break_no_update_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Expected output: "11" (may be "RC: 11" or "11" format)
|
||||
# Exit code may be 11 (return value) or 0
|
||||
if echo "$OUTPUT" | grep -qE "(^11$|RC: 11$)"; then
|
||||
test_pass "phase286_pattern2_break_no_update_vm: Pattern2 break-no-update succeeded (output: 11)"
|
||||
exit 0
|
||||
else
|
||||
echo "[FAIL] Unexpected output (expected: 11)"
|
||||
echo "[INFO] Exit code: $EXIT_CODE"
|
||||
echo "[INFO] Output:"
|
||||
echo "$OUTPUT" | head -n 20 || true
|
||||
test_fail "phase286_pattern2_break_no_update_vm: Unexpected output"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user