phase29ai(p10): move pattern2 extractor into plan layer
This commit is contained in:
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user