From 2abcd8e32b18e7897d0ba25dc210621048fcd14f Mon Sep 17 00:00:00 2001 From: tomoaki Date: Mon, 29 Dec 2025 08:18:09 +0900 Subject: [PATCH] phase29ai(p6): move pattern6/7 extractors into plan layer --- .../patterns/pattern6_scan_with_init.rs | 478 +----------------- .../joinir/patterns/pattern7_split_scan.rs | 432 +--------------- .../control_flow/plan/extractors/mod.rs | 4 + .../extractors/pattern6_scan_with_init.rs | 477 +++++++++++++++++ .../plan/extractors/pattern7_split_scan.rs | 433 ++++++++++++++++ src/mir/builder/control_flow/plan/mod.rs | 2 + .../single_planner/legacy_rules/pattern6.rs | 2 +- .../single_planner/legacy_rules/pattern7.rs | 2 +- 8 files changed, 924 insertions(+), 906 deletions(-) create mode 100644 src/mir/builder/control_flow/plan/extractors/mod.rs create mode 100644 src/mir/builder/control_flow/plan/extractors/pattern6_scan_with_init.rs create mode 100644 src/mir/builder/control_flow/plan/extractors/pattern7_split_scan.rs diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs index 3cfec0e7..887148fe 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs @@ -1,477 +1,5 @@ -//! Pattern 6: Scan with Init (index_of/find/contains form) -//! -//! Phase 254 P0: Dedicated pattern for scan loops with init-time method calls -//! -//! ## Pattern Structure -//! -//! ```nyash -//! index_of(s, ch) { -//! local i = 0 -//! loop(i < s.length()) { -//! if s.substring(i, i + 1) == ch { -//! return i -//! } -//! i = i + 1 -//! } -//! return -1 -//! } -//! ``` -//! -//! ## Detection Criteria (Structure Only - No Function Names) -//! -//! 1. Loop condition: `i < x.length()` or `i < len` -//! 2. Loop body has if statement with: -//! - Condition containing MethodCall (e.g., `substring(i, i+1) == ch`) -//! - Then branch: early return (break) -//! 3. Loop body has step: `i = i + 1` -//! 4. Post-loop: return statement (not-found value) -//! -//! ## Why Not Pattern 2? -//! -//! - Pattern 2 expects break condition without init-time MethodCall -//! - This pattern needs MethodCall in condition (substring) -//! - MethodCall allowed_in_condition() = false, but allowed_in_init() = true -//! - Need to hoist MethodCall to init phase +//! Phase 29ai P6: Re-export wrapper for Pattern6 extractor (compat) - // Phase 255 P2: Use shared var() helper -use crate::ast::ASTNode; -use crate::mir::builder::control_flow::joinir::patterns::common::{finalize_extract, ExtractDecision}; +#![allow(unused_imports)] -// Phase 273 P1: Import DomainPlan types (Plan renamed to DomainPlan) -use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection as PlanScanDirection, ScanWithInitPlan}; - -// Phase 273 P0.1: ScanDirection/ScanWithInitPlan is SSOT in plan/mod.rs - -// Phase 273 P0.1: extract_scan_with_init_parts() returns ScanWithInitPlan directly - -/// Phase 273 P1: Pure extractor that returns DomainPlan (SSOT) -/// -/// This is the new entry point for Pattern6 extraction. -/// Router calls this directly, then passes to Normalizer + Verifier + Lowerer. -/// -/// # Returns -/// -/// * `Ok(Some(DomainPlan::ScanWithInit(...)))` - Successfully extracted the pattern -/// * `Ok(None)` - Not a scan-with-init pattern (try next pattern) -/// * `Err(String)` - Contract violation (fail-fast) -pub(crate) fn extract_scan_with_init_plan( - condition: &ASTNode, - body: &[ASTNode], - fn_body: Option<&[ASTNode]>, -) -> Result, String> { - // Call internal extraction helper - let parts = finalize_extract( - extract_scan_with_init_parts(condition, body, fn_body), - "phase29ab/pattern6/contract", - )?; - - // Wrap in DomainPlan if extracted successfully - Ok(parts.map(DomainPlan::ScanWithInit)) -} - -// Phase 273 P0.1: can_lower() removed (router now calls extract_scan_with_init_plan() directly) - -/// Phase 258 P0: Extract and validate substring window arguments -/// -/// Checks if the substring call uses a dynamic window or fixed window: -/// - Fixed: `substring(i, i + 1)` → returns `false` -/// - Dynamic: `substring(i, i + substr.length())` → returns `true` -/// -/// # Arguments -/// -/// * `substring_call` - The MethodCall AST node for substring() -/// * `loop_var` - The loop index variable name (e.g., "i") -/// -/// # Returns -/// -/// * `Ok(true)` - Dynamic needle (variable.length()) -/// * `Ok(false)` - Fixed needle (literal 1) -/// * `Err(String)` - Invalid substring pattern (not this pattern) -fn extract_substring_window( - substring_call: &ASTNode, - loop_var: &str, -) -> Result { - use crate::ast::{BinaryOperator, LiteralValue}; - - // Extract arguments from substring(start, end) - let args = match substring_call { - ASTNode::MethodCall { method, arguments, .. } if method == "substring" => arguments, - _ => return Err("Not a substring call".to_string()), - }; - - if args.len() != 2 { - return Err(format!("substring expects 2 args, got {}", args.len())); - } - - // Check arg[0] is loop_var - match &args[0] { - ASTNode::Variable { name, .. } if name == loop_var => {} - _ => return Err("substring start must be loop_var".to_string()), - } - - // Check arg[1] is loop_var + - match &args[1] { - ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left, - right, - .. - } => { - // Left must be loop_var - match left.as_ref() { - ASTNode::Variable { name, .. } if name == loop_var => {} - _ => return Err("substring end must be loop_var + ".to_string()), - } - - // Right determines mode - match right.as_ref() { - // Fixed: substring(i, i + 1) - ASTNode::Literal { - value: LiteralValue::Integer(1), - .. - } => Ok(false), // Fixed window (ch) - - // Dynamic: substring(i, i + substr.length()) - ASTNode::MethodCall { method, .. } if method == "length" => Ok(true), // Dynamic window (substr) - - // Other patterns not supported - _ => Err("substring window must be 1 or variable.length()".to_string()), - } - } - _ => Err("substring end must be loop_var + ".to_string()), - } -} - -/// Phase 254 P1: Extract scan-with-init pattern parts from loop AST -/// -/// This function analyzes the loop structure and extracts all necessary information -/// for lowering an index_of-style loop to JoinIR. -/// -/// # Arguments -/// -/// * `condition` - Loop condition AST node -/// * `body` - Loop body statements -/// * `fn_body` - Full function body (needed to check post-loop return) -/// -/// # Returns -/// -/// * `ExtractDecision::Match(ScanWithInitPlan)` - Successfully extracted the pattern -/// * `ExtractDecision::NotApplicable` - Not a scan-with-init pattern (different pattern) -/// * `ExtractDecision::Contract` - Contract violation -/// -/// # P0 Restrictions -/// -/// - Loop condition must be `i < s.length()` (forward) or `i >= 0` (reverse) -/// - Step must be `i = i + 1` (forward, step_lit == 1) or `i = i - 1` (reverse, step_lit == -1) -/// - Not-found return must be `-1` -/// - Early return must be `return loop_var` -fn extract_scan_with_init_parts( - condition: &ASTNode, - body: &[ASTNode], - _fn_body: Option<&[ASTNode]>, -) -> ExtractDecision { - use crate::ast::{BinaryOperator, LiteralValue}; - - // 1. Check loop condition: i < s.length() (forward) or i >= 0 (reverse) - // Phase 258 P0: Also accept i <= s.length() - substr.length() (dynamic needle) - let (loop_var, haystack_opt, scan_direction) = match condition { - // Forward (Fixed): i < s.length() - ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left, - right, - .. - } => { - let loop_var = match left.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return ExtractDecision::NotApplicable, - }; - - let haystack = match right.as_ref() { - ASTNode::MethodCall { - object, method, .. - } if method == "length" => match object.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return ExtractDecision::NotApplicable, - }, - _ => return ExtractDecision::NotApplicable, - }; - - (loop_var, Some(haystack), PlanScanDirection::Forward) - } - // Forward (Dynamic): i <= s.length() - substr.length() - // Phase 258 P0: Accept dynamic needle form for index_of_string - ASTNode::BinaryOp { - operator: BinaryOperator::LessEqual, - left, - right, - .. - } => { - let loop_var = match left.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return ExtractDecision::NotApplicable, - }; - - // Right side must be: s.length() - substr.length() - let haystack = match right.as_ref() { - ASTNode::BinaryOp { - operator: BinaryOperator::Subtract, - left: sub_left, - right: sub_right, - .. - } => { - // Left of subtraction: s.length() - let haystack = match sub_left.as_ref() { - ASTNode::MethodCall { - object, method, .. - } if method == "length" => match object.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return ExtractDecision::NotApplicable, - }, - _ => return ExtractDecision::NotApplicable, - }; - - // Right of subtraction: substr.length() - match sub_right.as_ref() { - ASTNode::MethodCall { method, .. } if method == "length" => { - // Valid: s.length() - substr.length() - haystack - } - _ => return ExtractDecision::NotApplicable, - } - } - _ => return ExtractDecision::NotApplicable, - }; - - (loop_var, Some(haystack), PlanScanDirection::Forward) - } - // Reverse: i >= 0 - ASTNode::BinaryOp { - operator: BinaryOperator::GreaterEqual, - left, - right, - .. - } => { - let loop_var = match left.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return ExtractDecision::NotApplicable, - }; - - // Check right is Literal(0) - match right.as_ref() { - ASTNode::Literal { - value: LiteralValue::Integer(0), - .. - } => {} - _ => return ExtractDecision::NotApplicable, - } - - // For reverse, haystack will be extracted from substring call in body - (loop_var, None, PlanScanDirection::Reverse) - } - _ => return ExtractDecision::NotApplicable, - }; - - // 2. Find if statement with substring == needle and return loop_var - // Also extract haystack for reverse scans - let mut needle_opt = None; - let mut early_return_expr_opt = None; - let mut haystack_from_substring_opt = None; - let mut dynamic_needle_opt: Option = None; // Phase 258 P0: Track window mode - - for stmt in body { - if let ASTNode::If { - condition: if_cond, - then_body, - .. - } = stmt - { - // Check if condition is MethodCall(substring) == Variable(needle) - if let ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left, - right, - .. - } = if_cond.as_ref() - { - let substring_side = if matches!(left.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") - { - left.as_ref() - } else if matches!(right.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") - { - right.as_ref() - } else { - continue; - }; - - let needle_side = if std::ptr::eq(substring_side, left.as_ref()) { - right.as_ref() - } else { - left.as_ref() - }; - - // Phase 257 P0: Extract haystack from substring call for reverse scan - if let ASTNode::MethodCall { - object, method, .. - } = substring_side - { - if method == "substring" { - if let ASTNode::Variable { name: haystack_name, .. } = object.as_ref() { - haystack_from_substring_opt = Some(haystack_name.clone()); - } - } - } - - // Phase 258 P0: Validate substring arguments and extract window mode - match extract_substring_window(substring_side, &loop_var) { - Ok(dynamic_needle) => { - dynamic_needle_opt = Some(dynamic_needle); - } - Err(_) => { - // Not a valid substring pattern, fall through - continue; - } - } - - if let ASTNode::Variable { name: needle_name, .. } = needle_side { - // Check then_body contains return loop_var - if then_body.len() == 1 { - if let ASTNode::Return { value, .. } = &then_body[0] { - if let Some(ret_val) = value { - if let ASTNode::Variable { name: ret_name, .. } = ret_val.as_ref() { - if ret_name == &loop_var { - needle_opt = Some(needle_name.clone()); - early_return_expr_opt = Some(ret_val.as_ref().clone()); - } - } - } - } - } - } - } - } - } - - // Phase 273 P2: Return NotApplicable if pattern doesn't match (allow Pattern7 to try) - let needle = match needle_opt { - Some(n) => n, - None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern - }; - let early_return_expr = match early_return_expr_opt { - Some(e) => e, - None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern - }; - - // Phase 257 P0: Determine haystack based on scan direction - let haystack = match scan_direction { - PlanScanDirection::Forward => match haystack_opt { - Some(value) => value, - None => { - return ExtractDecision::contract( - "scan-with-init contract: forward scan missing haystack", - "use `i < s.length()` or `i <= s.length() - needle.length()` for forward scans", - ); - } - }, - PlanScanDirection::Reverse => match haystack_from_substring_opt { - Some(value) => value, - None => { - return ExtractDecision::contract( - "scan-with-init contract: reverse scan missing haystack", - "use `s.substring(i, i + 1)` (or dynamic) in the match condition", - ); - } - }, - }; - - // 3. Check for step: i = i + 1 (forward) or i = i - 1 (reverse) - let mut step_lit_opt = None; - - for stmt in body { - if let ASTNode::Assignment { target, value, .. } = stmt { - if let ASTNode::Variable { name: target_name, .. } = target.as_ref() { - if target_name == &loop_var { - if let ASTNode::BinaryOp { - operator, - left, - right, - .. - } = value.as_ref() - { - if let ASTNode::Variable { name: left_name, .. } = left.as_ref() { - if left_name == &loop_var { - if let ASTNode::Literal { - value: LiteralValue::Integer(lit), - .. - } = right.as_ref() - { - match operator { - BinaryOperator::Add => { - step_lit_opt = Some(*lit); - } - BinaryOperator::Subtract => { - step_lit_opt = Some(-lit); - } - _ => {} - } - } - } - } - } - } - } - } - } - - let step_lit = match step_lit_opt { - Some(value) => value, - None => { - return ExtractDecision::contract( - "scan-with-init contract: missing step update", - "add `i = i + 1` (forward) or `i = i - 1` (reverse) inside the loop", - ); - } - }; - - // Phase 257 P0: Verify step matches scan direction - match scan_direction { - PlanScanDirection::Forward => { - if step_lit != 1 { - return ExtractDecision::contract( - "scan-with-init contract: forward step must be `i = i + 1`", - "change the step update to `i = i + 1`", - ); - } - } - PlanScanDirection::Reverse => { - if step_lit != -1 { - return ExtractDecision::contract( - "scan-with-init contract: reverse step must be `i = i - 1`", - "change the step update to `i = i - 1`", - ); - } - } - } - - // 4. P0: not-found return must be -1 (hardcoded for now) - let not_found_return_lit = -1; - - // Phase 258 P0: Extract dynamic_needle (default to false for backward compat) - let dynamic_needle = dynamic_needle_opt.unwrap_or(false); - - ExtractDecision::Match(ScanWithInitPlan { - loop_var, - haystack, - needle, - step_lit, - early_return_expr, - not_found_return_lit, - scan_direction, - dynamic_needle, - }) -} - -// Phase 273 P0.1: lower() removed (router now uses PlanLowerer::lower_scan_with_init()) - -// Phase 273 P0.1: cf_loop_pattern6_scan_with_init_impl() removed (migrated to plan/lowerer.rs) -// The implementation is now in PlanLowerer::lower_scan_with_init() +pub(crate) use crate::mir::builder::control_flow::plan::extractors::pattern6_scan_with_init::*; diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs index 72b08c09..721dcc49 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs @@ -1,431 +1,5 @@ -//! Phase 256 P0: Pattern 7 - Split/Tokenization with Variable Step -//! -//! **Status**: P0 Implementation - 1-char separator only -//! -//! ## Pattern Description -//! -//! Detects string splitting pattern with conditional step: -//! ```nyash -//! loop(i <= s.length() - separator.length()) { -//! if s.substring(i, i + separator.length()) == separator { -//! result.push(s.substring(start, i)) // Match: variable step -//! start = i + separator.length() -//! i = start -//! } else { -//! i = i + 1 // No match: constant step -//! } -//! } -//! if start <= s.length() { -//! result.push(s.substring(start, s.length())) // Final segment -//! } -//! ``` -//! -//! Key features: -//! - Two carriers: i (loop index), start (segment start) -//! - Three invariants: s (haystack), separator, result (ArrayBox) -//! - Conditional step via Select instruction (Pattern 4 style) -//! - Side effects: result.push() in both loop and post-loop -//! - P0 restriction: 1-char separator only +//! Phase 29ai P6: Re-export wrapper for Pattern7 extractor (compat) -use crate::ast::ASTNode; -use crate::mir::builder::control_flow::joinir::patterns::common::{ - finalize_extract, ContractViolation, ExtractDecision, -}; +#![allow(unused_imports)] -/// Phase 256 P0: Split/Scan pattern parts extractor -/// -/// Holds all extracted variables and AST nodes needed for JoinIR lowering. -/// P0: Fixed-form parser only (Fail-Fast on mismatch). -#[derive(Debug, Clone)] -struct SplitScanParts { - // Variables (5 total) - s_var: String, // haystack variable name - sep_var: String, // separator variable name - result_var: String, // accumulator (ArrayBox) variable name - i_var: String, // loop index variable name - start_var: String, // segment start position variable name - - // Extracted ASTs for JoinIR lowering - loop_cond_ast: ASTNode, // i <= s.length() - sep.length() - match_if_cond_ast: ASTNode, // s.substring(i, i + sep.length()) == sep - then_push_ast: ASTNode, // result.push(s.substring(start, i)) - then_start_next_ast: ASTNode, // start = i + sep.length() - then_i_next_ast: ASTNode, // i = start - else_i_next_ast: ASTNode, // i = i + 1 - post_push_ast: Option, // result.push(s.substring(start, s.length())) -} - -/// Phase 273 P2: Extract SplitScanPlan from AST (pure) -/// -/// Returns DomainPlan if pattern matches, Ok(None) if not. -pub(crate) fn extract_split_scan_plan( - condition: &ASTNode, - body: &[ASTNode], - _post_loop_code: &[ASTNode], -) -> Result, String> { - use crate::mir::builder::control_flow::plan::{DomainPlan, SplitScanPlan}; - - // Try to extract using existing implementation - let parts = finalize_extract( - extract_split_scan_parts(condition, body, &[]), - "phase29ab/pattern7/contract", - )?; - let plan = parts.map(|parts| SplitScanPlan { - s_var: parts.s_var, - sep_var: parts.sep_var, - result_var: parts.result_var, - i_var: parts.i_var, - start_var: parts.start_var, - }); - Ok(plan.map(DomainPlan::SplitScan)) -} - -/// Phase 256 P0: Extract SplitScanParts from AST -/// -/// **P0 Strategy**: Fixed-form parser (Fail-Fast on mismatch) -/// - Accepts only the exact pattern shape -/// - Returns Err immediately on any deviation -/// - No fallback, no coercion (correctness first) -fn extract_split_scan_parts( - condition: &ASTNode, - body: &[ASTNode], - post_loop_code: &[ASTNode], -) -> ExtractDecision { - // Step 1: Extract variables from loop condition - // Expected: i <= s.length() - separator.length() - let (i_var, s_var, sep_var) = match extract_loop_condition_vars(condition) { - Ok(values) => values, - Err(_) => return ExtractDecision::NotApplicable, - }; - - // Step 2: Find the if statement in loop body - let if_stmt = match body.iter().find(|stmt| matches!(stmt, ASTNode::If { .. })) { - Some(stmt) => stmt, - None => return ExtractDecision::NotApplicable, - }; - - let (match_if_cond_ast, then_body, else_body) = match if_stmt { - ASTNode::If { - condition, - then_body, - else_body, - .. - } => (condition.as_ref().clone(), then_body, else_body), - _ => return ExtractDecision::NotApplicable, - }; - - if let Err(err) = validate_separator_literal_len(&match_if_cond_ast) { - return ExtractDecision::Contract(err); - } - - // Step 3: Extract push operation from then branch - let then_push_ast = match then_body - .iter() - .find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push")) - { - Some(stmt) => stmt.clone(), - None => return ExtractDecision::NotApplicable, - }; - - // Step 4: Extract start assignment (start = i + separator.length()) - let then_start_next_ast = match then_body - .iter() - .find(|stmt| { - matches!(stmt, ASTNode::Assignment { target, .. } if { - matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") - }) - }) { - Some(stmt) => stmt.clone(), - None => return ExtractDecision::NotApplicable, - }; - - // Step 5: Extract i assignment (i = start or i = variable) - let then_i_next_ast = match then_body.iter().find(|stmt| { - matches!(stmt, ASTNode::Assignment { target, value, .. } if { - matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") - && matches!(value.as_ref(), ASTNode::Variable { .. }) - }) - }) { - Some(stmt) => stmt.clone(), - None => return ExtractDecision::NotApplicable, - }; - - // Step 6: Extract else branch assignment (i = i + 1) - let else_i_next_ast = if let Some(else_statements) = else_body { - match else_statements.iter().find(|stmt| { - matches!(stmt, ASTNode::Assignment { target, .. } if { - matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") - }) - }) { - Some(stmt) => stmt.clone(), - None => return ExtractDecision::NotApplicable, - } - } else { - return ExtractDecision::NotApplicable; - }; - - if let Err(err) = validate_start_update(&then_start_next_ast, &i_var, &sep_var) { - return ExtractDecision::Contract(err); - } - if let Err(err) = validate_i_set_to_start(&then_i_next_ast, &i_var) { - return ExtractDecision::Contract(err); - } - if let Err(err) = validate_i_increment_by_one(&else_i_next_ast, &i_var) { - return ExtractDecision::Contract(err); - } - - // Step 7: Extract post-loop push (result.push(...)) - let post_push_ast = post_loop_code - .iter() - .find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push")) - .cloned(); - - // Step 8: Extract result variable from push statements - let result_var = match extract_result_var(&then_push_ast) { - Ok(value) => value, - Err(_) => return ExtractDecision::NotApplicable, - }; - - ExtractDecision::Match(SplitScanParts { - s_var, - sep_var, - result_var, - i_var, - start_var: "start".to_string(), // Fixed for P0 - loop_cond_ast: condition.clone(), - match_if_cond_ast, - then_push_ast, - then_start_next_ast, - then_i_next_ast, - else_i_next_ast, - post_push_ast, - }) -} - -/// Extract i_var, s_var, sep_var from loop condition -/// Expected: i <= s.length() - separator.length() -fn extract_loop_condition_vars(condition: &ASTNode) -> Result<(String, String, String), String> { - use crate::ast::BinaryOperator; - - // Loop condition: i <= s.length() - separator.length() - // This is a BinaryOp node with operator LessEqual - - match condition { - ASTNode::BinaryOp { - left, - operator, - right, - .. - } if *operator == BinaryOperator::LessEqual => { - // Left should be: Variable { name: "i" } - let i_var = match left.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => { - return Err("extract_loop_condition_vars: Left side not a variable".to_string()); - } - }; - - // Right should be: s.length() - separator.length() - // This is a BinaryOp: minus - let (s_var, sep_var) = extract_subtraction_vars(right)?; - - Ok((i_var, s_var, sep_var)) - } - _ => Err("extract_loop_condition_vars: Not a <= comparison".to_string()), - } -} - -/// Extract s and sep from: s.length() - separator.length() -fn extract_subtraction_vars(expr: &ASTNode) -> Result<(String, String), String> { - use crate::ast::BinaryOperator; - - match expr { - ASTNode::BinaryOp { - left, - operator, - right, - .. - } if *operator == BinaryOperator::Subtract => { - // Left: s.length() - let s_var = match left.as_ref() { - ASTNode::MethodCall { object, method, .. } if method == "length" => { - match object.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return Err("extract_subtraction_vars: length() object not variable".to_string()), - } - } - _ => return Err("extract_subtraction_vars: Left not s.length()".to_string()), - }; - - // Right: separator.length() - let sep_var = match right.as_ref() { - ASTNode::MethodCall { object, method, .. } if method == "length" => { - match object.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return Err("extract_subtraction_vars: length() object not variable".to_string()), - } - } - _ => return Err("extract_subtraction_vars: Right not separator.length()".to_string()), - }; - - Ok((s_var, sep_var)) - } - _ => Err("extract_subtraction_vars: Not a subtraction".to_string()), - } -} - -fn validate_start_update( - assign: &ASTNode, - i_var: &str, - sep_var: &str, -) -> Result<(), ContractViolation> { - use crate::ast::BinaryOperator; - - let hint = "use `start = i + separator.length()` in the then-branch"; - match assign { - ASTNode::Assignment { target, value, .. } => { - if !matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") { - return Err(ContractViolation::new( - "split scan contract: start target must be `start`", - hint, - )); - } - - match value.as_ref() { - ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left, - right, - .. - } => { - let left_ok = matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var); - let right_ok = matches!(right.as_ref(), ASTNode::MethodCall { object, method, arguments, .. } - if method == "length" - && arguments.is_empty() - && matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var) - ); - - if left_ok && right_ok { - Ok(()) - } else { - Err(ContractViolation::new( - "split scan contract: start update must be `i + separator.length()`", - hint, - )) - } - } - _ => Err(ContractViolation::new( - "split scan contract: start update must be `i + separator.length()`", - hint, - )), - } - } - _ => Err(ContractViolation::new( - "split scan contract: expected start assignment", - hint, - )), - } -} - -fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), ContractViolation> { - let hint = "use `i = start` in the then-branch"; - match assign { - ASTNode::Assignment { target, value, .. } => { - let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var); - let value_ok = matches!(value.as_ref(), ASTNode::Variable { name, .. } if name == "start"); - if target_ok && value_ok { - Ok(()) - } else { - Err(ContractViolation::new( - "split scan contract: then i update must be `i = start`", - hint, - )) - } - } - _ => Err(ContractViolation::new( - "split scan contract: expected then i assignment", - hint, - )), - } -} - -fn validate_i_increment_by_one( - assign: &ASTNode, - i_var: &str, -) -> Result<(), ContractViolation> { - use crate::ast::{BinaryOperator, LiteralValue}; - - let hint = "use `i = i + 1` in the else-branch"; - match assign { - ASTNode::Assignment { target, value, .. } => { - let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var); - let value_ok = matches!(value.as_ref(), ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left, - right, - .. - } if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var) - && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(1), .. }) - ); - - if target_ok && value_ok { - Ok(()) - } else { - Err(ContractViolation::new( - "split scan contract: else i update must be `i = i + 1`", - hint, - )) - } - } - _ => Err(ContractViolation::new( - "split scan contract: expected else i assignment", - hint, - )), - } -} - -fn validate_separator_literal_len(cond: &ASTNode) -> Result<(), ContractViolation> { - use crate::ast::{BinaryOperator, LiteralValue}; - - let hint = "use a 1-char separator (e.g. \",\") or avoid split-scan patterns"; - let (left, right) = match cond { - ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left, - right, - .. - } => (left.as_ref(), right.as_ref()), - _ => return Ok(()), - }; - - let literal = match (left, right) { - (ASTNode::Literal { value: LiteralValue::String(s), .. }, _) => Some(s), - (_, ASTNode::Literal { value: LiteralValue::String(s), .. }) => Some(s), - _ => None, - }; - - if let Some(value) = literal { - if value.chars().count() != 1 { - return Err(ContractViolation::new( - "split scan contract: separator must be 1 char (P0)", - hint, - )); - } - } - - Ok(()) -} - -/// Extract result variable name from push call -/// Expected: result.push(...) -fn extract_result_var(push_stmt: &ASTNode) -> Result { - match push_stmt { - ASTNode::MethodCall { object, method, .. } if method == "push" => { - match object.as_ref() { - ASTNode::Variable { name, .. } => Ok(name.clone()), - _ => Err("extract_result_var: push() object not a variable".to_string()), - } - } - _ => Err("extract_result_var: Not a push() call".to_string()), - } -} +pub(crate) use crate::mir::builder::control_flow::plan::extractors::pattern7_split_scan::*; diff --git a/src/mir/builder/control_flow/plan/extractors/mod.rs b/src/mir/builder/control_flow/plan/extractors/mod.rs new file mode 100644 index 00000000..ecd41848 --- /dev/null +++ b/src/mir/builder/control_flow/plan/extractors/mod.rs @@ -0,0 +1,4 @@ +//! Phase 29ai P6: Plan-layer extractors (Pattern6/7) + +pub(in crate::mir::builder) mod pattern6_scan_with_init; +pub(in crate::mir::builder) mod pattern7_split_scan; diff --git a/src/mir/builder/control_flow/plan/extractors/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/plan/extractors/pattern6_scan_with_init.rs new file mode 100644 index 00000000..3cfec0e7 --- /dev/null +++ b/src/mir/builder/control_flow/plan/extractors/pattern6_scan_with_init.rs @@ -0,0 +1,477 @@ +//! Pattern 6: Scan with Init (index_of/find/contains form) +//! +//! Phase 254 P0: Dedicated pattern for scan loops with init-time method calls +//! +//! ## Pattern Structure +//! +//! ```nyash +//! index_of(s, ch) { +//! local i = 0 +//! loop(i < s.length()) { +//! if s.substring(i, i + 1) == ch { +//! return i +//! } +//! i = i + 1 +//! } +//! return -1 +//! } +//! ``` +//! +//! ## Detection Criteria (Structure Only - No Function Names) +//! +//! 1. Loop condition: `i < x.length()` or `i < len` +//! 2. Loop body has if statement with: +//! - Condition containing MethodCall (e.g., `substring(i, i+1) == ch`) +//! - Then branch: early return (break) +//! 3. Loop body has step: `i = i + 1` +//! 4. Post-loop: return statement (not-found value) +//! +//! ## Why Not Pattern 2? +//! +//! - Pattern 2 expects break condition without init-time MethodCall +//! - This pattern needs MethodCall in condition (substring) +//! - MethodCall allowed_in_condition() = false, but allowed_in_init() = true +//! - Need to hoist MethodCall to init phase + + // Phase 255 P2: Use shared var() helper +use crate::ast::ASTNode; +use crate::mir::builder::control_flow::joinir::patterns::common::{finalize_extract, ExtractDecision}; + +// Phase 273 P1: Import DomainPlan types (Plan renamed to DomainPlan) +use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection as PlanScanDirection, ScanWithInitPlan}; + +// Phase 273 P0.1: ScanDirection/ScanWithInitPlan is SSOT in plan/mod.rs + +// Phase 273 P0.1: extract_scan_with_init_parts() returns ScanWithInitPlan directly + +/// Phase 273 P1: Pure extractor that returns DomainPlan (SSOT) +/// +/// This is the new entry point for Pattern6 extraction. +/// Router calls this directly, then passes to Normalizer + Verifier + Lowerer. +/// +/// # Returns +/// +/// * `Ok(Some(DomainPlan::ScanWithInit(...)))` - Successfully extracted the pattern +/// * `Ok(None)` - Not a scan-with-init pattern (try next pattern) +/// * `Err(String)` - Contract violation (fail-fast) +pub(crate) fn extract_scan_with_init_plan( + condition: &ASTNode, + body: &[ASTNode], + fn_body: Option<&[ASTNode]>, +) -> Result, String> { + // Call internal extraction helper + let parts = finalize_extract( + extract_scan_with_init_parts(condition, body, fn_body), + "phase29ab/pattern6/contract", + )?; + + // Wrap in DomainPlan if extracted successfully + Ok(parts.map(DomainPlan::ScanWithInit)) +} + +// Phase 273 P0.1: can_lower() removed (router now calls extract_scan_with_init_plan() directly) + +/// Phase 258 P0: Extract and validate substring window arguments +/// +/// Checks if the substring call uses a dynamic window or fixed window: +/// - Fixed: `substring(i, i + 1)` → returns `false` +/// - Dynamic: `substring(i, i + substr.length())` → returns `true` +/// +/// # Arguments +/// +/// * `substring_call` - The MethodCall AST node for substring() +/// * `loop_var` - The loop index variable name (e.g., "i") +/// +/// # Returns +/// +/// * `Ok(true)` - Dynamic needle (variable.length()) +/// * `Ok(false)` - Fixed needle (literal 1) +/// * `Err(String)` - Invalid substring pattern (not this pattern) +fn extract_substring_window( + substring_call: &ASTNode, + loop_var: &str, +) -> Result { + use crate::ast::{BinaryOperator, LiteralValue}; + + // Extract arguments from substring(start, end) + let args = match substring_call { + ASTNode::MethodCall { method, arguments, .. } if method == "substring" => arguments, + _ => return Err("Not a substring call".to_string()), + }; + + if args.len() != 2 { + return Err(format!("substring expects 2 args, got {}", args.len())); + } + + // Check arg[0] is loop_var + match &args[0] { + ASTNode::Variable { name, .. } if name == loop_var => {} + _ => return Err("substring start must be loop_var".to_string()), + } + + // Check arg[1] is loop_var + + match &args[1] { + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } => { + // Left must be loop_var + match left.as_ref() { + ASTNode::Variable { name, .. } if name == loop_var => {} + _ => return Err("substring end must be loop_var + ".to_string()), + } + + // Right determines mode + match right.as_ref() { + // Fixed: substring(i, i + 1) + ASTNode::Literal { + value: LiteralValue::Integer(1), + .. + } => Ok(false), // Fixed window (ch) + + // Dynamic: substring(i, i + substr.length()) + ASTNode::MethodCall { method, .. } if method == "length" => Ok(true), // Dynamic window (substr) + + // Other patterns not supported + _ => Err("substring window must be 1 or variable.length()".to_string()), + } + } + _ => Err("substring end must be loop_var + ".to_string()), + } +} + +/// Phase 254 P1: Extract scan-with-init pattern parts from loop AST +/// +/// This function analyzes the loop structure and extracts all necessary information +/// for lowering an index_of-style loop to JoinIR. +/// +/// # Arguments +/// +/// * `condition` - Loop condition AST node +/// * `body` - Loop body statements +/// * `fn_body` - Full function body (needed to check post-loop return) +/// +/// # Returns +/// +/// * `ExtractDecision::Match(ScanWithInitPlan)` - Successfully extracted the pattern +/// * `ExtractDecision::NotApplicable` - Not a scan-with-init pattern (different pattern) +/// * `ExtractDecision::Contract` - Contract violation +/// +/// # P0 Restrictions +/// +/// - Loop condition must be `i < s.length()` (forward) or `i >= 0` (reverse) +/// - Step must be `i = i + 1` (forward, step_lit == 1) or `i = i - 1` (reverse, step_lit == -1) +/// - Not-found return must be `-1` +/// - Early return must be `return loop_var` +fn extract_scan_with_init_parts( + condition: &ASTNode, + body: &[ASTNode], + _fn_body: Option<&[ASTNode]>, +) -> ExtractDecision { + use crate::ast::{BinaryOperator, LiteralValue}; + + // 1. Check loop condition: i < s.length() (forward) or i >= 0 (reverse) + // Phase 258 P0: Also accept i <= s.length() - substr.length() (dynamic needle) + let (loop_var, haystack_opt, scan_direction) = match condition { + // Forward (Fixed): i < s.length() + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return ExtractDecision::NotApplicable, + }; + + let haystack = match right.as_ref() { + ASTNode::MethodCall { + object, method, .. + } if method == "length" => match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return ExtractDecision::NotApplicable, + }, + _ => return ExtractDecision::NotApplicable, + }; + + (loop_var, Some(haystack), PlanScanDirection::Forward) + } + // Forward (Dynamic): i <= s.length() - substr.length() + // Phase 258 P0: Accept dynamic needle form for index_of_string + ASTNode::BinaryOp { + operator: BinaryOperator::LessEqual, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return ExtractDecision::NotApplicable, + }; + + // Right side must be: s.length() - substr.length() + let haystack = match right.as_ref() { + ASTNode::BinaryOp { + operator: BinaryOperator::Subtract, + left: sub_left, + right: sub_right, + .. + } => { + // Left of subtraction: s.length() + let haystack = match sub_left.as_ref() { + ASTNode::MethodCall { + object, method, .. + } if method == "length" => match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return ExtractDecision::NotApplicable, + }, + _ => return ExtractDecision::NotApplicable, + }; + + // Right of subtraction: substr.length() + match sub_right.as_ref() { + ASTNode::MethodCall { method, .. } if method == "length" => { + // Valid: s.length() - substr.length() + haystack + } + _ => return ExtractDecision::NotApplicable, + } + } + _ => return ExtractDecision::NotApplicable, + }; + + (loop_var, Some(haystack), PlanScanDirection::Forward) + } + // Reverse: i >= 0 + ASTNode::BinaryOp { + operator: BinaryOperator::GreaterEqual, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return ExtractDecision::NotApplicable, + }; + + // Check right is Literal(0) + match right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(0), + .. + } => {} + _ => return ExtractDecision::NotApplicable, + } + + // For reverse, haystack will be extracted from substring call in body + (loop_var, None, PlanScanDirection::Reverse) + } + _ => return ExtractDecision::NotApplicable, + }; + + // 2. Find if statement with substring == needle and return loop_var + // Also extract haystack for reverse scans + let mut needle_opt = None; + let mut early_return_expr_opt = None; + let mut haystack_from_substring_opt = None; + let mut dynamic_needle_opt: Option = None; // Phase 258 P0: Track window mode + + for stmt in body { + if let ASTNode::If { + condition: if_cond, + then_body, + .. + } = stmt + { + // Check if condition is MethodCall(substring) == Variable(needle) + if let ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } = if_cond.as_ref() + { + let substring_side = if matches!(left.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") + { + left.as_ref() + } else if matches!(right.as_ref(), ASTNode::MethodCall { method, .. } if method == "substring") + { + right.as_ref() + } else { + continue; + }; + + let needle_side = if std::ptr::eq(substring_side, left.as_ref()) { + right.as_ref() + } else { + left.as_ref() + }; + + // Phase 257 P0: Extract haystack from substring call for reverse scan + if let ASTNode::MethodCall { + object, method, .. + } = substring_side + { + if method == "substring" { + if let ASTNode::Variable { name: haystack_name, .. } = object.as_ref() { + haystack_from_substring_opt = Some(haystack_name.clone()); + } + } + } + + // Phase 258 P0: Validate substring arguments and extract window mode + match extract_substring_window(substring_side, &loop_var) { + Ok(dynamic_needle) => { + dynamic_needle_opt = Some(dynamic_needle); + } + Err(_) => { + // Not a valid substring pattern, fall through + continue; + } + } + + if let ASTNode::Variable { name: needle_name, .. } = needle_side { + // Check then_body contains return loop_var + if then_body.len() == 1 { + if let ASTNode::Return { value, .. } = &then_body[0] { + if let Some(ret_val) = value { + if let ASTNode::Variable { name: ret_name, .. } = ret_val.as_ref() { + if ret_name == &loop_var { + needle_opt = Some(needle_name.clone()); + early_return_expr_opt = Some(ret_val.as_ref().clone()); + } + } + } + } + } + } + } + } + } + + // Phase 273 P2: Return NotApplicable if pattern doesn't match (allow Pattern7 to try) + let needle = match needle_opt { + Some(n) => n, + None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern + }; + let early_return_expr = match early_return_expr_opt { + Some(e) => e, + None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern + }; + + // Phase 257 P0: Determine haystack based on scan direction + let haystack = match scan_direction { + PlanScanDirection::Forward => match haystack_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: forward scan missing haystack", + "use `i < s.length()` or `i <= s.length() - needle.length()` for forward scans", + ); + } + }, + PlanScanDirection::Reverse => match haystack_from_substring_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: reverse scan missing haystack", + "use `s.substring(i, i + 1)` (or dynamic) in the match condition", + ); + } + }, + }; + + // 3. Check for step: i = i + 1 (forward) or i = i - 1 (reverse) + let mut step_lit_opt = None; + + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if let ASTNode::Variable { name: target_name, .. } = target.as_ref() { + if target_name == &loop_var { + if let ASTNode::BinaryOp { + operator, + left, + right, + .. + } = value.as_ref() + { + if let ASTNode::Variable { name: left_name, .. } = left.as_ref() { + if left_name == &loop_var { + if let ASTNode::Literal { + value: LiteralValue::Integer(lit), + .. + } = right.as_ref() + { + match operator { + BinaryOperator::Add => { + step_lit_opt = Some(*lit); + } + BinaryOperator::Subtract => { + step_lit_opt = Some(-lit); + } + _ => {} + } + } + } + } + } + } + } + } + } + + let step_lit = match step_lit_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: missing step update", + "add `i = i + 1` (forward) or `i = i - 1` (reverse) inside the loop", + ); + } + }; + + // Phase 257 P0: Verify step matches scan direction + match scan_direction { + PlanScanDirection::Forward => { + if step_lit != 1 { + return ExtractDecision::contract( + "scan-with-init contract: forward step must be `i = i + 1`", + "change the step update to `i = i + 1`", + ); + } + } + PlanScanDirection::Reverse => { + if step_lit != -1 { + return ExtractDecision::contract( + "scan-with-init contract: reverse step must be `i = i - 1`", + "change the step update to `i = i - 1`", + ); + } + } + } + + // 4. P0: not-found return must be -1 (hardcoded for now) + let not_found_return_lit = -1; + + // Phase 258 P0: Extract dynamic_needle (default to false for backward compat) + let dynamic_needle = dynamic_needle_opt.unwrap_or(false); + + ExtractDecision::Match(ScanWithInitPlan { + loop_var, + haystack, + needle, + step_lit, + early_return_expr, + not_found_return_lit, + scan_direction, + dynamic_needle, + }) +} + +// Phase 273 P0.1: lower() removed (router now uses PlanLowerer::lower_scan_with_init()) + +// Phase 273 P0.1: cf_loop_pattern6_scan_with_init_impl() removed (migrated to plan/lowerer.rs) +// The implementation is now in PlanLowerer::lower_scan_with_init() diff --git a/src/mir/builder/control_flow/plan/extractors/pattern7_split_scan.rs b/src/mir/builder/control_flow/plan/extractors/pattern7_split_scan.rs new file mode 100644 index 00000000..bd8727ca --- /dev/null +++ b/src/mir/builder/control_flow/plan/extractors/pattern7_split_scan.rs @@ -0,0 +1,433 @@ +//! Phase 256 P0: Pattern 7 - Split/Tokenization with Variable Step +//! +//! **Status**: P0 Implementation - 1-char separator only +//! +//! ## Pattern Description +//! +//! Detects string splitting pattern with conditional step: +//! ```nyash +//! loop(i <= s.length() - separator.length()) { +//! if s.substring(i, i + separator.length()) == separator { +//! result.push(s.substring(start, i)) // Match: variable step +//! start = i + separator.length() +//! i = start +//! } else { +//! i = i + 1 // No match: constant step +//! } +//! } +//! if start <= s.length() { +//! result.push(s.substring(start, s.length())) // Final segment +//! } +//! ``` +//! +//! Key features: +//! - Two carriers: i (loop index), start (segment start) +//! - Three invariants: s (haystack), separator, result (ArrayBox) +//! - Conditional step via Select instruction (Pattern 4 style) +//! - Side effects: result.push() in both loop and post-loop +//! - P0 restriction: 1-char separator only + +#![allow(dead_code)] + +use crate::ast::ASTNode; +use crate::mir::builder::control_flow::joinir::patterns::common::{ + finalize_extract, ContractViolation, ExtractDecision, +}; + +/// Phase 256 P0: Split/Scan pattern parts extractor +/// +/// Holds all extracted variables and AST nodes needed for JoinIR lowering. +/// P0: Fixed-form parser only (Fail-Fast on mismatch). +#[derive(Debug, Clone)] +struct SplitScanParts { + // Variables (5 total) + s_var: String, // haystack variable name + sep_var: String, // separator variable name + result_var: String, // accumulator (ArrayBox) variable name + i_var: String, // loop index variable name + start_var: String, // segment start position variable name + + // Extracted ASTs for JoinIR lowering + loop_cond_ast: ASTNode, // i <= s.length() - sep.length() + match_if_cond_ast: ASTNode, // s.substring(i, i + sep.length()) == sep + then_push_ast: ASTNode, // result.push(s.substring(start, i)) + then_start_next_ast: ASTNode, // start = i + sep.length() + then_i_next_ast: ASTNode, // i = start + else_i_next_ast: ASTNode, // i = i + 1 + post_push_ast: Option, // result.push(s.substring(start, s.length())) +} + +/// Phase 273 P2: Extract SplitScanPlan from AST (pure) +/// +/// Returns DomainPlan if pattern matches, Ok(None) if not. +pub(crate) fn extract_split_scan_plan( + condition: &ASTNode, + body: &[ASTNode], + _post_loop_code: &[ASTNode], +) -> Result, String> { + use crate::mir::builder::control_flow::plan::{DomainPlan, SplitScanPlan}; + + // Try to extract using existing implementation + let parts = finalize_extract( + extract_split_scan_parts(condition, body, &[]), + "phase29ab/pattern7/contract", + )?; + let plan = parts.map(|parts| SplitScanPlan { + s_var: parts.s_var, + sep_var: parts.sep_var, + result_var: parts.result_var, + i_var: parts.i_var, + start_var: parts.start_var, + }); + Ok(plan.map(DomainPlan::SplitScan)) +} + +/// Phase 256 P0: Extract SplitScanParts from AST +/// +/// **P0 Strategy**: Fixed-form parser (Fail-Fast on mismatch) +/// - Accepts only the exact pattern shape +/// - Returns Err immediately on any deviation +/// - No fallback, no coercion (correctness first) +fn extract_split_scan_parts( + condition: &ASTNode, + body: &[ASTNode], + post_loop_code: &[ASTNode], +) -> ExtractDecision { + // Step 1: Extract variables from loop condition + // Expected: i <= s.length() - separator.length() + let (i_var, s_var, sep_var) = match extract_loop_condition_vars(condition) { + Ok(values) => values, + Err(_) => return ExtractDecision::NotApplicable, + }; + + // Step 2: Find the if statement in loop body + let if_stmt = match body.iter().find(|stmt| matches!(stmt, ASTNode::If { .. })) { + Some(stmt) => stmt, + None => return ExtractDecision::NotApplicable, + }; + + let (match_if_cond_ast, then_body, else_body) = match if_stmt { + ASTNode::If { + condition, + then_body, + else_body, + .. + } => (condition.as_ref().clone(), then_body, else_body), + _ => return ExtractDecision::NotApplicable, + }; + + if let Err(err) = validate_separator_literal_len(&match_if_cond_ast) { + return ExtractDecision::Contract(err); + } + + // Step 3: Extract push operation from then branch + let then_push_ast = match then_body + .iter() + .find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push")) + { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; + + // Step 4: Extract start assignment (start = i + separator.length()) + let then_start_next_ast = match then_body + .iter() + .find(|stmt| { + matches!(stmt, ASTNode::Assignment { target, .. } if { + matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") + }) + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; + + // Step 5: Extract i assignment (i = start or i = variable) + let then_i_next_ast = match then_body.iter().find(|stmt| { + matches!(stmt, ASTNode::Assignment { target, value, .. } if { + matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") + && matches!(value.as_ref(), ASTNode::Variable { .. }) + }) + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; + + // Step 6: Extract else branch assignment (i = i + 1) + let else_i_next_ast = if let Some(else_statements) = else_body { + match else_statements.iter().find(|stmt| { + matches!(stmt, ASTNode::Assignment { target, .. } if { + matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") + }) + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + } + } else { + return ExtractDecision::NotApplicable; + }; + + if let Err(err) = validate_start_update(&then_start_next_ast, &i_var, &sep_var) { + return ExtractDecision::Contract(err); + } + if let Err(err) = validate_i_set_to_start(&then_i_next_ast, &i_var) { + return ExtractDecision::Contract(err); + } + if let Err(err) = validate_i_increment_by_one(&else_i_next_ast, &i_var) { + return ExtractDecision::Contract(err); + } + + // Step 7: Extract post-loop push (result.push(...)) + let post_push_ast = post_loop_code + .iter() + .find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push")) + .cloned(); + + // Step 8: Extract result variable from push statements + let result_var = match extract_result_var(&then_push_ast) { + Ok(value) => value, + Err(_) => return ExtractDecision::NotApplicable, + }; + + ExtractDecision::Match(SplitScanParts { + s_var, + sep_var, + result_var, + i_var, + start_var: "start".to_string(), // Fixed for P0 + loop_cond_ast: condition.clone(), + match_if_cond_ast, + then_push_ast, + then_start_next_ast, + then_i_next_ast, + else_i_next_ast, + post_push_ast, + }) +} + +/// Extract i_var, s_var, sep_var from loop condition +/// Expected: i <= s.length() - separator.length() +fn extract_loop_condition_vars(condition: &ASTNode) -> Result<(String, String, String), String> { + use crate::ast::BinaryOperator; + + // Loop condition: i <= s.length() - separator.length() + // This is a BinaryOp node with operator LessEqual + + match condition { + ASTNode::BinaryOp { + left, + operator, + right, + .. + } if *operator == BinaryOperator::LessEqual => { + // Left should be: Variable { name: "i" } + let i_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => { + return Err("extract_loop_condition_vars: Left side not a variable".to_string()); + } + }; + + // Right should be: s.length() - separator.length() + // This is a BinaryOp: minus + let (s_var, sep_var) = extract_subtraction_vars(right)?; + + Ok((i_var, s_var, sep_var)) + } + _ => Err("extract_loop_condition_vars: Not a <= comparison".to_string()), + } +} + +/// Extract s and sep from: s.length() - separator.length() +fn extract_subtraction_vars(expr: &ASTNode) -> Result<(String, String), String> { + use crate::ast::BinaryOperator; + + match expr { + ASTNode::BinaryOp { + left, + operator, + right, + .. + } if *operator == BinaryOperator::Subtract => { + // Left: s.length() + let s_var = match left.as_ref() { + ASTNode::MethodCall { object, method, .. } if method == "length" => { + match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Err("extract_subtraction_vars: length() object not variable".to_string()), + } + } + _ => return Err("extract_subtraction_vars: Left not s.length()".to_string()), + }; + + // Right: separator.length() + let sep_var = match right.as_ref() { + ASTNode::MethodCall { object, method, .. } if method == "length" => { + match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Err("extract_subtraction_vars: length() object not variable".to_string()), + } + } + _ => return Err("extract_subtraction_vars: Right not separator.length()".to_string()), + }; + + Ok((s_var, sep_var)) + } + _ => Err("extract_subtraction_vars: Not a subtraction".to_string()), + } +} + +fn validate_start_update( + assign: &ASTNode, + i_var: &str, + sep_var: &str, +) -> Result<(), ContractViolation> { + use crate::ast::BinaryOperator; + + let hint = "use `start = i + separator.length()` in the then-branch"; + match assign { + ASTNode::Assignment { target, value, .. } => { + if !matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") { + return Err(ContractViolation::new( + "split scan contract: start target must be `start`", + hint, + )); + } + + match value.as_ref() { + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } => { + let left_ok = matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var); + let right_ok = matches!(right.as_ref(), ASTNode::MethodCall { object, method, arguments, .. } + if method == "length" + && arguments.is_empty() + && matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var) + ); + + if left_ok && right_ok { + Ok(()) + } else { + Err(ContractViolation::new( + "split scan contract: start update must be `i + separator.length()`", + hint, + )) + } + } + _ => Err(ContractViolation::new( + "split scan contract: start update must be `i + separator.length()`", + hint, + )), + } + } + _ => Err(ContractViolation::new( + "split scan contract: expected start assignment", + hint, + )), + } +} + +fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), ContractViolation> { + let hint = "use `i = start` in the then-branch"; + match assign { + ASTNode::Assignment { target, value, .. } => { + let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var); + let value_ok = matches!(value.as_ref(), ASTNode::Variable { name, .. } if name == "start"); + if target_ok && value_ok { + Ok(()) + } else { + Err(ContractViolation::new( + "split scan contract: then i update must be `i = start`", + hint, + )) + } + } + _ => Err(ContractViolation::new( + "split scan contract: expected then i assignment", + hint, + )), + } +} + +fn validate_i_increment_by_one( + assign: &ASTNode, + i_var: &str, +) -> Result<(), ContractViolation> { + use crate::ast::{BinaryOperator, LiteralValue}; + + let hint = "use `i = i + 1` in the else-branch"; + match assign { + ASTNode::Assignment { target, value, .. } => { + let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var); + let value_ok = matches!(value.as_ref(), ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var) + && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(1), .. }) + ); + + if target_ok && value_ok { + Ok(()) + } else { + Err(ContractViolation::new( + "split scan contract: else i update must be `i = i + 1`", + hint, + )) + } + } + _ => Err(ContractViolation::new( + "split scan contract: expected else i assignment", + hint, + )), + } +} + +fn validate_separator_literal_len(cond: &ASTNode) -> Result<(), ContractViolation> { + use crate::ast::{BinaryOperator, LiteralValue}; + + let hint = "use a 1-char separator (e.g. \",\") or avoid split-scan patterns"; + let (left, right) = match cond { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } => (left.as_ref(), right.as_ref()), + _ => return Ok(()), + }; + + let literal = match (left, right) { + (ASTNode::Literal { value: LiteralValue::String(s), .. }, _) => Some(s), + (_, ASTNode::Literal { value: LiteralValue::String(s), .. }) => Some(s), + _ => None, + }; + + if let Some(value) = literal { + if value.chars().count() != 1 { + return Err(ContractViolation::new( + "split scan contract: separator must be 1 char (P0)", + hint, + )); + } + } + + Ok(()) +} + +/// Extract result variable name from push call +/// Expected: result.push(...) +fn extract_result_var(push_stmt: &ASTNode) -> Result { + match push_stmt { + ASTNode::MethodCall { object, method, .. } if method == "push" => { + match object.as_ref() { + ASTNode::Variable { name, .. } => Ok(name.clone()), + _ => Err("extract_result_var: push() object not a variable".to_string()), + } + } + _ => Err("extract_result_var: Not a push() call".to_string()), + } +} diff --git a/src/mir/builder/control_flow/plan/mod.rs b/src/mir/builder/control_flow/plan/mod.rs index 3c6763fd..0e6e9c0a 100644 --- a/src/mir/builder/control_flow/plan/mod.rs +++ b/src/mir/builder/control_flow/plan/mod.rs @@ -35,6 +35,8 @@ pub(in crate::mir::builder) mod facts; pub(in crate::mir::builder) mod normalize; pub(in crate::mir::builder) mod planner; pub(in crate::mir::builder) mod emit; +// Phase 29ai P6: Extractors moved into plan layer +pub(in crate::mir::builder) mod extractors; // Phase 29ai P5: JoinIR router → single plan extraction entrypoint pub(in crate::mir::builder) mod single_planner; diff --git a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern6.rs b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern6.rs index c71c7e2a..68994fdc 100644 --- a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern6.rs +++ b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern6.rs @@ -1,6 +1,6 @@ use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::control_flow::plan::DomainPlan; -use crate::mir::builder::control_flow::joinir::patterns::pattern6_scan_with_init::extract_scan_with_init_plan; +use crate::mir::builder::control_flow::plan::extractors::pattern6_scan_with_init::extract_scan_with_init_plan; pub(in crate::mir::builder) fn extract( ctx: &LoopPatternContext, diff --git a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern7.rs b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern7.rs index 1b8801a4..fdecec6b 100644 --- a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern7.rs +++ b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern7.rs @@ -1,6 +1,6 @@ use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::control_flow::plan::DomainPlan; -use crate::mir::builder::control_flow::joinir::patterns::pattern7_split_scan::extract_split_scan_plan; +use crate::mir::builder::control_flow::plan::extractors::pattern7_split_scan::extract_split_scan_plan; pub(in crate::mir::builder) fn extract( ctx: &LoopPatternContext,