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:
@ -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
|
||||
// ============================================================================
|
||||
|
||||
@ -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),
|
||||
|
||||
Reference in New Issue
Block a user