phase29ai(p10): move pattern2 extractor into plan layer

This commit is contained in:
2025-12-29 09:44:04 +09:00
parent 9abc726394
commit 4d26133d6a
10 changed files with 1216 additions and 1094 deletions

View File

@ -1,613 +1,10 @@
//! Common Extraction Helpers for Pattern1-5
//! Phase 29ai P10: Wrapper for plan-layer common_helpers (SSOT)
//!
//! Phase 282 P9a: Extracted from Pattern1-5 extractors to eliminate common duplication.
//! Legacy path preserved:
//! `crate::mir::builder::control_flow::joinir::patterns::extractors::common_helpers::*`
//!
//! # Design Principles
//!
//! - **Pure Functions**: No side effects, no builder mutations
//! - **Fail-Fast**: Err for logic bugs, Ok(None) for non-matches
//! - **Configurability**: ControlFlowDetector for pattern-specific behavior
//! - **Scope-Limited**: Common detection only, pattern-specific logic excluded
//!
//! # Groups (P9a Scope-Limited)
//!
//! 1. Control Flow Counting (count_control_flow) - Universal counter
//! 2. Control Flow Detection (has_break_statement, has_continue_statement, etc.) - Common detection
//! 3. Condition Validation (extract_loop_variable, is_true_literal) - Condition helpers
//! 4. Pattern5-Specific Helpers (validate_continue_at_end, validate_break_in_simple_if) - NOT generalized
//!
//! **IMPORTANT**: Pattern-specific interpretation logic (e.g., Pattern3's nested_if) is EXCLUDED.
//! Such logic remains in individual pattern files to maintain clear SSOT boundaries.
//! SSOT implementation lives in:
//! `crate::mir::builder::control_flow::plan::extractors::common_helpers::*`
use crate::ast::ASTNode;
pub(crate) use crate::mir::builder::control_flow::plan::extractors::common_helpers::*;
/// ============================================================
/// Group 1: Control Flow Counting (汎用カウンター)
/// ============================================================
#[derive(Debug, Clone, Default)]
pub(crate) struct ControlFlowCounts {
pub break_count: usize,
pub continue_count: usize,
pub return_count: usize,
pub has_nested_loop: bool,
}
/// Control flow detection options
///
/// # P9a Scope-Limited
/// - `detect_nested_if` removed (Pattern3-specific, deferred to P9b)
/// - Only common detection options included
#[derive(Debug, Clone)]
pub(crate) struct ControlFlowDetector {
/// Skip break/continue inside nested loops?
pub skip_nested_control_flow: bool,
/// Count return statements?
pub count_returns: bool,
}
impl Default for ControlFlowDetector {
fn default() -> Self {
Self {
skip_nested_control_flow: true,
count_returns: false,
}
}
}
/// Universal control flow counter
///
/// # Examples (P9a Scope-Limited)
/// - Pattern1: default (skip_nested=true, count_returns=false)
/// - Pattern2: default (skip_nested=true, count_returns=false)
/// - Pattern4: default (skip_nested=true, count_returns=false)
/// - Pattern5: count_returns=true (returns Err if return found)
pub(crate) fn count_control_flow(
body: &[ASTNode],
detector: ControlFlowDetector,
) -> ControlFlowCounts {
let mut counts = ControlFlowCounts::default();
fn scan_node(
node: &ASTNode,
counts: &mut ControlFlowCounts,
detector: &ControlFlowDetector,
depth: usize,
) {
match node {
ASTNode::Break { .. } => {
counts.break_count += 1;
}
ASTNode::Continue { .. } => {
counts.continue_count += 1;
}
ASTNode::Return { .. } if detector.count_returns => {
counts.return_count += 1;
}
ASTNode::Loop { .. } if depth > 0 => {
counts.has_nested_loop = true;
// Skip nested loop bodies if configured
if detector.skip_nested_control_flow {
return;
}
}
ASTNode::If {
then_body,
else_body,
..
} => {
// Recurse into if/else bodies
for stmt in then_body {
scan_node(stmt, counts, detector, depth + 1);
}
if let Some(else_b) = else_body {
for stmt in else_b {
scan_node(stmt, counts, detector, depth + 1);
}
}
}
_ => {}
}
}
for stmt in body {
scan_node(stmt, &mut counts, &detector, 0);
}
counts
}
/// ============================================================
/// Group 2: Control Flow Detection (真偽値判定)
/// ============================================================
/// Check if body has ANY break statement
pub(crate) fn has_break_statement(body: &[ASTNode]) -> bool {
count_control_flow(body, ControlFlowDetector::default()).break_count > 0
}
/// Check if body has ANY continue statement
pub(crate) fn has_continue_statement(body: &[ASTNode]) -> bool {
count_control_flow(body, ControlFlowDetector::default()).continue_count > 0
}
/// Check if body has ANY return statement
pub(crate) fn has_return_statement(body: &[ASTNode]) -> bool {
let mut detector = ControlFlowDetector::default();
detector.count_returns = true;
count_control_flow(body, detector).return_count > 0
}
/// Check if body has ANY break or continue
pub(crate) fn has_control_flow_statement(body: &[ASTNode]) -> bool {
let counts = count_control_flow(body, ControlFlowDetector::default());
counts.break_count > 0 || counts.continue_count > 0
}
/// Phase 286 P2.6: Check if body has ANY if statement (recursive)
///
/// This is a supplementary helper for Pattern1 extraction to prevent
/// Pattern1 from incorrectly matching Pattern3 fixtures (if-else-phi).
///
/// # Returns
/// - true if any if statement found in body (recursively)
/// - false otherwise
pub(crate) fn has_if_statement(body: &[ASTNode]) -> bool {
for node in body {
match node {
ASTNode::If { .. } => return true,
ASTNode::ScopeBox { body, .. } => {
if has_if_statement(body) {
return true;
}
}
ASTNode::Loop { body, .. } => {
if has_if_statement(body) {
return true;
}
}
_ => {}
}
}
false
}
/// Phase 286 P2.6: Check if body has ANY if-else statement (recursive)
///
/// This is more specific than has_if_statement - it only detects if statements
/// with else branches, which are Pattern3 territory (if-phi merge).
///
/// # Returns
/// - true if any if-else statement found in body (recursively)
/// - false otherwise
pub(crate) fn has_if_else_statement(body: &[ASTNode]) -> bool {
for node in body {
match node {
ASTNode::If { else_body: Some(_), .. } => return true,
ASTNode::ScopeBox { body, .. } => {
if has_if_else_statement(body) {
return true;
}
}
ASTNode::Loop { body, .. } => {
if has_if_else_statement(body) {
return true;
}
}
_ => {}
}
}
false
}
/// Phase 286: Find first if-else statement in loop body (non-recursive)
///
/// Returns first if statement with else branch in the given body.
/// This is a PoC subset helper - only finds the FIRST if-else (Pattern3 constraint).
///
/// # Returns
/// - Some(&ASTNode) if an if-else statement found
/// - None if no if-else statement found
///
/// # Note
/// This is intentionally non-recursive (top-level only) for Pattern3 extraction.
/// Pattern3 allows multiple if statements - this just finds the first if-else.
pub(crate) fn find_if_else_statement(body: &[ASTNode]) -> Option<&ASTNode> {
for stmt in body {
if matches!(stmt, ASTNode::If { else_body: Some(_), .. }) {
return Some(stmt); // Found first if-else
}
}
None // No if-else found
}
/// ============================================================
/// Group 3: Condition Validation (比較演算検証)
/// ============================================================
use crate::ast::BinaryOperator;
/// Validate condition: 比較演算 (左辺が変数)
///
/// # Returns
/// - Some("var_name") if valid comparison with variable on left
/// - None otherwise
pub(crate) fn extract_loop_variable(condition: &ASTNode) -> Option<String> {
match condition {
ASTNode::BinaryOp { operator, left, .. } => {
if !matches!(
operator,
BinaryOperator::Less
| BinaryOperator::LessEqual
| BinaryOperator::Greater
| BinaryOperator::GreaterEqual
| BinaryOperator::Equal
| BinaryOperator::NotEqual
) {
return None;
}
if let ASTNode::Variable { name, .. } = left.as_ref() {
return Some(name.clone());
}
None
}
_ => None,
}
}
/// Check if condition is true literal
pub(crate) fn is_true_literal(condition: &ASTNode) -> bool {
use crate::ast::LiteralValue;
matches!(
condition,
ASTNode::Literal {
value: LiteralValue::Bool(true),
..
}
)
}
/// ============================================================
/// Group 4: Loop Increment Extraction (Common for Plan line)
/// ============================================================
/// Phase 286 P2.2: Extract loop increment for Plan line patterns
///
/// Supports `<var> = <var> + <int_lit>` pattern only (PoC safety).
/// Used by Pattern1 and Pattern4 Plan line extractors.
///
/// # Returns
/// - Ok(Some(ASTNode)) if valid increment found
/// - Ok(None) if no increment or unsupported pattern
/// - Err if malformed AST (rare)
pub(crate) fn extract_loop_increment_plan(body: &[ASTNode], loop_var: &str) -> Result<Option<ASTNode>, String> {
for stmt in body {
if let ASTNode::Assignment { target, value, .. } = stmt {
if let ASTNode::Variable { name, .. } = target.as_ref() {
if name == loop_var {
// Check for `i = i + <int>`
if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() {
if matches!(operator, BinaryOperator::Add) {
if let ASTNode::Variable { name: lname, .. } = left.as_ref() {
if lname == loop_var {
if matches!(right.as_ref(), ASTNode::Literal { .. }) {
return Ok(Some(value.as_ref().clone()));
}
}
}
}
}
}
}
}
}
Ok(None)
}
/// ============================================================
/// Group 5: Pattern5-Specific Helpers (NOT generalized)
/// ============================================================
///
/// **IMPORTANT**: These helpers are Pattern5-specific and intentionally NOT generalized.
/// Other patterns with similar needs should implement their own validation logic
/// to maintain clear SSOT boundaries.
/// Validate continue is at body end (Pattern5 specific)
///
/// # Pattern5 Shape Requirement
/// - Continue statement MUST be the last statement in loop body (top-level)
/// - This is Pattern5's specific shape constraint, not a general pattern
pub(crate) fn validate_continue_at_end(body: &[ASTNode]) -> bool {
matches!(body.last(), Some(ASTNode::Continue { .. }))
}
/// Validate break is in simple if pattern (Pattern5 specific)
///
/// # Pattern5 Shape Requirement
/// - Break MUST be in simple if: `if (...) { break }` (no else branch)
/// - This is Pattern5's specific shape constraint, not a general pattern
pub(crate) fn validate_break_in_simple_if(body: &[ASTNode]) -> bool {
for stmt in body {
if let ASTNode::If {
then_body,
else_body,
..
} = stmt
{
if then_body.len() == 1
&& matches!(then_body[0], ASTNode::Break { .. })
&& else_body.is_none()
{
return true;
}
}
}
false
}
// ============================================================================
// Unit Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
// Test fixtures
fn make_break() -> ASTNode {
ASTNode::Break {
span: Span::unknown(),
}
}
fn make_continue() -> ASTNode {
ASTNode::Continue {
span: Span::unknown(),
}
}
fn make_return() -> ASTNode {
ASTNode::Return {
value: None,
span: Span::unknown(),
}
}
fn make_if_with_break() -> ASTNode {
ASTNode::If {
condition: Box::new(ASTNode::Variable {
name: "done".to_string(),
span: Span::unknown(),
}),
then_body: vec![make_break()],
else_body: None,
span: Span::unknown(),
}
}
#[test]
fn test_count_control_flow_break_only() {
let body = vec![make_if_with_break()];
let counts = count_control_flow(&body, ControlFlowDetector::default());
assert_eq!(counts.break_count, 1);
assert_eq!(counts.continue_count, 0);
assert_eq!(counts.return_count, 0);
assert!(!counts.has_nested_loop);
}
#[test]
fn test_count_control_flow_continue_only() {
let body = vec![make_continue()];
let counts = count_control_flow(&body, ControlFlowDetector::default());
assert_eq!(counts.break_count, 0);
assert_eq!(counts.continue_count, 1);
assert_eq!(counts.return_count, 0);
assert!(!counts.has_nested_loop);
}
#[test]
fn test_count_control_flow_return_with_detector() {
let body = vec![make_return()];
let mut detector = ControlFlowDetector::default();
detector.count_returns = true;
let counts = count_control_flow(&body, detector);
assert_eq!(counts.break_count, 0);
assert_eq!(counts.continue_count, 0);
assert_eq!(counts.return_count, 1);
assert!(!counts.has_nested_loop);
}
#[test]
fn test_has_break_statement() {
let body = vec![make_if_with_break()];
assert!(has_break_statement(&body));
}
#[test]
fn test_has_break_statement_false() {
let body = vec![make_continue()];
assert!(!has_break_statement(&body));
}
#[test]
fn test_has_continue_statement() {
let body = vec![make_continue()];
assert!(has_continue_statement(&body));
}
#[test]
fn test_has_continue_statement_false() {
let body = vec![make_break()];
assert!(!has_continue_statement(&body));
}
#[test]
fn test_has_return_statement() {
let body = vec![make_return()];
assert!(has_return_statement(&body));
}
#[test]
fn test_has_return_statement_false() {
let body = vec![make_break()];
assert!(!has_return_statement(&body));
}
#[test]
fn test_has_control_flow_statement_break() {
let body = vec![make_if_with_break()];
assert!(has_control_flow_statement(&body));
}
#[test]
fn test_has_control_flow_statement_continue() {
let body = vec![make_continue()];
assert!(has_control_flow_statement(&body));
}
#[test]
fn test_has_control_flow_statement_false() {
let body = vec![ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}];
assert!(!has_control_flow_statement(&body));
}
#[test]
fn test_extract_loop_variable_success() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
assert_eq!(extract_loop_variable(&condition), Some("i".to_string()));
}
#[test]
fn test_extract_loop_variable_not_comparison() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
assert_eq!(extract_loop_variable(&condition), None);
}
#[test]
fn test_extract_loop_variable_not_variable() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
assert_eq!(extract_loop_variable(&condition), None);
}
#[test]
fn test_is_true_literal_success() {
let condition = ASTNode::Literal {
value: LiteralValue::Bool(true),
span: Span::unknown(),
};
assert!(is_true_literal(&condition));
}
#[test]
fn test_is_true_literal_false() {
let condition = ASTNode::Literal {
value: LiteralValue::Bool(false),
span: Span::unknown(),
};
assert!(!is_true_literal(&condition));
}
#[test]
fn test_is_true_literal_not_literal() {
let condition = ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
};
assert!(!is_true_literal(&condition));
}
#[test]
fn test_validate_continue_at_end_success() {
let body = vec![make_break(), make_continue()];
assert!(validate_continue_at_end(&body));
}
#[test]
fn test_validate_continue_at_end_false() {
let body = vec![make_continue(), make_break()];
assert!(!validate_continue_at_end(&body));
}
#[test]
fn test_validate_break_in_simple_if_success() {
let body = vec![make_if_with_break()];
assert!(validate_break_in_simple_if(&body));
}
#[test]
fn test_validate_break_in_simple_if_with_else() {
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::Variable {
name: "done".to_string(),
span: Span::unknown(),
}),
then_body: vec![make_break()],
else_body: Some(vec![make_continue()]),
span: Span::unknown(),
}];
assert!(!validate_break_in_simple_if(&body));
}
#[test]
fn test_validate_break_in_simple_if_multiple_statements() {
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::Variable {
name: "done".to_string(),
span: Span::unknown(),
}),
then_body: vec![make_break(), make_continue()],
else_body: None,
span: Span::unknown(),
}];
assert!(!validate_break_in_simple_if(&body));
}
}

View File

@ -1,496 +1,23 @@
//! 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
//! Phase 29ai P10: Wrapper for plan-layer Pattern2 extractor (SSOT)
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::ast::ASTNode;
// Phase 282 P9a: Use common_helpers
use super::common_helpers::{
count_control_flow, has_continue_statement as common_has_continue,
has_return_statement as common_has_return, ControlFlowDetector,
};
pub(crate) use crate::mir::builder::control_flow::plan::extractors::pattern2_break::Pattern2Parts;
#[derive(Debug, Clone)]
pub(crate) struct Pattern2Parts {
pub loop_var: String, // Loop variable name (empty for loop(true))
pub is_loop_true: bool, // true if loop(true), false otherwise
pub break_count: usize, // Number of break statements (validation)
// Note: AST reused from ctx - no duplication
}
/// Extract Pattern2 (Loop with Conditional Break) parts
///
/// # Detection Criteria (Lightweight - SSOT minimal)
///
/// 1. **Condition**: 比較演算 (left=variable) OR loop(true)
/// 2. **Body**: MUST have at least 1 break statement
/// 3. **Body**: NO continue statements (Pattern4 territory)
/// 4. **Body**: NO return statements (Pattern2 = break-only)
///
/// # Four-Phase Validation
///
/// **Phase 1**: Validate condition (比較演算 or loop(true))
/// **Phase 2**: Validate HAS break (required for Pattern2)
/// **Phase 3**: Validate NO continue/return (break-only)
/// **Phase 4**: Extract core info
///
/// # Fail-Fast Rules
///
/// - `Ok(Some(parts))`: Pattern2 match confirmed
/// - `Ok(None)`: Not Pattern2 (no break, has continue/return, or Pattern1)
/// - `Err(msg)`: Logic bug (malformed AST)
///
/// # Note
///
/// Carrier update validation is deferred to can_lower() via CommonPatternInitializer (SSOT).
pub(crate) fn extract_loop_with_break_parts(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<Pattern2Parts>, String> {
// Phase 1: Validate condition
let (loop_var, is_loop_true) = validate_condition_for_pattern2(condition);
// Phase 2: Validate HAS break (required)
let break_count = count_break_statements(body);
if break_count == 0 {
return Ok(None); // No break → Pattern1 territory
}
// Phase 3: Validate NO continue/return (break-only)
if has_continue_statement(body) {
return Ok(None); // Has continue → Pattern4 territory
}
if has_return_statement(body) {
return Ok(None); // Has return → Pattern2 is break-only
}
// Phase 4: Return extracted info
Ok(Some(Pattern2Parts {
loop_var,
is_loop_true,
break_count,
}))
crate::mir::builder::control_flow::plan::extractors::pattern2_break::extract_loop_with_break_parts(
condition, body,
)
}
/// Validate condition: 比較演算 or loop(true)
fn validate_condition_for_pattern2(condition: &ASTNode) -> (String, bool) {
// Case 1: loop(true)
if matches!(condition, ASTNode::Literal { value: LiteralValue::Bool(true), .. }) {
return ("".to_string(), true); // loop_var determined in can_lower()
}
// Case 2: 比較演算 (same as Pattern1)
match condition {
ASTNode::BinaryOp { operator, left, .. } => {
if matches!(
operator,
BinaryOperator::Less
| BinaryOperator::LessEqual
| BinaryOperator::Greater
| BinaryOperator::GreaterEqual
| BinaryOperator::Equal
| BinaryOperator::NotEqual
) {
if let ASTNode::Variable { name, .. } = left.as_ref() {
return (name.clone(), false);
}
}
}
_ => {}
}
// Unknown condition - let can_lower() decide
("".to_string(), false)
}
/// Count break statements recursively
///
/// # Phase 282 P9a: Delegates to common_helpers::count_control_flow
fn count_break_statements(body: &[ASTNode]) -> usize {
count_control_flow(body, ControlFlowDetector::default()).break_count
}
/// Check for continue statements
///
/// # Phase 282 P9a: Delegates to common_helpers::has_continue_statement
fn has_continue_statement(body: &[ASTNode]) -> bool {
common_has_continue(body)
}
/// Check for return statements (Pattern2 = break-only)
///
/// # Phase 282 P9a: Delegates to common_helpers::has_return_statement
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::*;
use crate::ast::Span;
#[test]
fn test_extract_with_break_success() {
// loop(i < 10) { if (i == 5) { break } i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![
ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Break {
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
let result = extract_loop_with_break_parts(&condition, &body);
assert!(result.is_ok());
let parts = result.unwrap();
assert!(parts.is_some());
let parts = parts.unwrap();
assert_eq!(parts.loop_var, "i");
assert_eq!(parts.is_loop_true, false);
assert_eq!(parts.break_count, 1);
}
#[test]
fn test_extract_no_break_returns_none() {
// loop(i < 10) { i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let result = extract_loop_with_break_parts(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // No break → Pattern1 territory
}
#[test]
fn test_extract_with_continue_returns_none() {
// loop(i < 10) { if (i == 5) { continue } i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Continue {
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
}];
let result = extract_loop_with_break_parts(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Has continue → Pattern4 territory
}
#[test]
fn test_extract_loop_true_with_break_success() {
// loop(true) { if (i == 5) { break } i = i + 1 }
let condition = ASTNode::Literal {
value: LiteralValue::Bool(true),
span: Span::unknown(),
};
let body = vec![
ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Break {
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
let result = extract_loop_with_break_parts(&condition, &body);
assert!(result.is_ok());
let parts = result.unwrap();
assert!(parts.is_some());
let parts = parts.unwrap();
assert_eq!(parts.loop_var, ""); // Empty for loop(true)
assert_eq!(parts.is_loop_true, true);
assert_eq!(parts.break_count, 1);
}
crate::mir::builder::control_flow::plan::extractors::pattern2_break::extract_pattern2_plan(
condition, body,
)
}