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

@ -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),