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:
2025-12-26 05:56:30 +09:00
parent f76fb6cd3e
commit b235a5b6db
8 changed files with 658 additions and 12 deletions

View File

@ -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::*;

View File

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