phase29ai(p6): move pattern6/7 extractors into plan layer

This commit is contained in:
2025-12-29 08:18:09 +09:00
parent e031375c99
commit 2abcd8e32b
8 changed files with 924 additions and 906 deletions

View File

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

View File

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

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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