phase29ai(p6): move pattern6/7 extractors into plan layer
This commit is contained in:
@ -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<Option<DomainPlan>, 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<bool, String> {
|
||||
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 + <expr>
|
||||
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 + <expr>".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 + <expr>".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<ScanWithInitPlan> {
|
||||
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<bool> = 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::*;
|
||||
|
||||
@ -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<ASTNode>, // 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<Option<crate::mir::builder::control_flow::plan::DomainPlan>, 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<SplitScanParts> {
|
||||
// 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<String, String> {
|
||||
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::*;
|
||||
|
||||
4
src/mir/builder/control_flow/plan/extractors/mod.rs
Normal file
4
src/mir/builder/control_flow/plan/extractors/mod.rs
Normal file
@ -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;
|
||||
@ -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<Option<DomainPlan>, 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<bool, String> {
|
||||
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 + <expr>
|
||||
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 + <expr>".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 + <expr>".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<ScanWithInitPlan> {
|
||||
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<bool> = 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()
|
||||
@ -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<ASTNode>, // 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<Option<crate::mir::builder::control_flow::plan::DomainPlan>, 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<SplitScanParts> {
|
||||
// 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<String, String> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user