feat(joinir): Phase 286 P2.4 - Pattern8 BoolPredicateScan Plan化 PoC

## 概要
Pattern8 (BoolPredicateScan) を Plan extraction routing に追加。
static box 除外(Phase 269 決定)を尊重し、非 static box fixture で PoC。

## 実装内容
- Pattern8BoolPredicateScanPlan struct + DomainPlan variant
- extract_pattern8_plan(): 条件・predicate check・increment 抽出
- normalize_pattern8_bool_predicate_scan(): PoC stub(CoreExitPlan::Return 未統合)
- PLAN_EXTRACTORS テーブルに Pattern8 追加(3rd priority)
- エラーフォールバック: Plan normalization 失敗時 → legacy Pattern8 へ

## 動作フロー
Plan extraction MATCHED → normalization failed (PoC stub) → legacy Pattern8 MATCHED

## 検証結果
- Integration: phase286_pattern8_plan_poc_vm PASS (exit 7)
- Regression: quick 154 PASS, 0 FAILED

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 03:01:11 +09:00
parent 1d24e9a106
commit 064cae169e
8 changed files with 605 additions and 8 deletions

View File

@ -25,6 +25,7 @@ pub(crate) mod pattern2; // Phase 282 P4: Pattern2 extraction
pub(crate) mod pattern3; // Phase 282 P5: Pattern3 extraction
pub(crate) mod pattern4; // Phase 282 P6: Pattern4 extraction
pub(crate) mod pattern5; // Phase 282 P7: Pattern5 extraction
pub(crate) mod pattern8; // Phase 286 P2.4: Pattern8 Plan extraction
pub(crate) mod pattern9; // Phase 286 P2.3: Pattern9 Plan extraction
// Phase 282 P9a: Common extraction helpers

View File

@ -0,0 +1,450 @@
//! Phase 286 P2.4: Pattern8 (BoolPredicateScan) Extraction
//!
//! Minimal subset extractor for Pattern8 Plan line.
//!
//! # Supported subset (PoC safety)
//!
//! - Loop condition: `i < s.length()`
//! - Body: if statement with predicate check + loop increment
//! - If condition: `not me.is_digit(s.substring(i, i + 1))`
//! - Then branch: `return false`
//! - Loop increment: `i = i + 1`
//! - Post-loop: `return true` (enforced by caller)
//! - Step literal: 1 (forward scan only)
//!
//! Returns Ok(None) for unsupported patterns → legacy fallback
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern8BoolPredicateScanPlan};
/// Phase 286 P2.4: Minimal subset extractor for Pattern8 Plan line
///
/// # Detection Criteria
///
/// 1. **Condition**: `i < s.length()` (forward scan)
/// 2. **Body**: if statement with predicate check
/// - Condition: `not me.is_digit(s.substring(i, i + 1))`
/// - Then branch: `return false`
/// 3. **Body**: loop increment `i = i + 1`
/// 4. **Step literal**: 1 (P0: forward scan only)
///
/// # Returns
///
/// - `Ok(Some(plan))`: Pattern8 match confirmed
/// - `Ok(None)`: Not Pattern8 (構造不一致 or unsupported)
/// - `Err(msg)`: Logic bug (malformed AST)
pub(crate) fn extract_pattern8_plan(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<DomainPlan>, String> {
// Step 1: Validate loop condition: i < s.length()
let (loop_var, haystack) = match validate_loop_condition_plan(condition) {
Some((var, hay)) => (var, hay),
None => return Ok(None), // Unsupported condition format
};
// Step 2: Extract predicate check (if not predicate() { return false })
let (predicate_receiver, predicate_method) = match extract_predicate_check(body, &loop_var, &haystack) {
Some((receiver, method)) => (receiver, method),
None => return Ok(None), // No predicate pattern found
};
// Step 3: Extract loop increment (i = i + 1)
let step_lit = match extract_loop_increment(body, &loop_var) {
Some(lit) => lit,
None => return Ok(None), // No increment found
};
// P0: Step must be 1 (forward scan only)
if step_lit != 1 {
return Ok(None);
}
Ok(Some(DomainPlan::Pattern8BoolPredicateScan(
Pattern8BoolPredicateScanPlan {
loop_var,
haystack,
predicate_receiver,
predicate_method,
condition: condition.clone(),
step_lit,
},
)))
}
/// Validate loop condition: supports `i < s.length()` only
fn validate_loop_condition_plan(cond: &ASTNode) -> Option<(String, String)> {
if let ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left,
right,
..
} = cond
{
// Left must be a variable (loop_var)
let loop_var = if let ASTNode::Variable { name, .. } = left.as_ref() {
name.clone()
} else {
return None;
};
// Right must be s.length()
let haystack = if let ASTNode::MethodCall {
object,
method,
..
} = right.as_ref()
{
if method == "length" {
if let ASTNode::Variable { name, .. } = object.as_ref() {
name.clone()
} else {
return None;
}
} else {
return None;
}
} else {
return None;
};
Some((loop_var, haystack))
} else {
None
}
}
/// Extract predicate check from body
///
/// Looks for: `if not receiver.method(s.substring(i, i + 1)) { return false }`
///
/// Returns: Some((receiver, method)) or None
fn extract_predicate_check(
body: &[ASTNode],
loop_var: &str,
haystack: &str,
) -> Option<(String, String)> {
for stmt in body.iter() {
if let ASTNode::If {
condition: if_cond,
then_body,
..
} = stmt
{
// Check if condition is: not receiver.method(...)
if let ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand,
..
} = if_cond.as_ref()
{
// Operand must be MethodCall
if let ASTNode::MethodCall {
object,
method,
arguments,
..
} = operand.as_ref()
{
// Extract receiver (e.g., "me")
let receiver = match object.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
ASTNode::Me { .. } => "me".to_string(),
_ => continue,
};
// P0: Expect 1 argument: s.substring(i, i + 1)
if arguments.len() != 1 {
continue;
}
// Validate argument is substring call
if !validate_substring_call(&arguments[0], haystack, loop_var) {
continue;
}
// Check then_body contains: return false
if then_body.len() == 1 {
if let ASTNode::Return { value, .. } = &then_body[0] {
if let Some(ret_val) = value {
if matches!(
ret_val.as_ref(),
ASTNode::Literal {
value: LiteralValue::Bool(false),
..
}
) {
return Some((receiver, method.clone()));
}
}
}
}
}
}
}
}
None
}
/// Validate substring call: s.substring(i, i + 1)
fn validate_substring_call(arg: &ASTNode, haystack: &str, loop_var: &str) -> bool {
if let ASTNode::MethodCall {
object: substr_obj,
method: substr_method,
arguments: substr_args,
..
} = arg
{
if substr_method != "substring" {
return false;
}
// Object must be haystack
if let ASTNode::Variable { name, .. } = substr_obj.as_ref() {
if name != haystack {
return false;
}
} else {
return false;
}
// Args: (i, i + 1)
if substr_args.len() != 2 {
return false;
}
// Arg 0: loop_var
if !matches!(
&substr_args[0],
ASTNode::Variable { name, .. } if name == loop_var
) {
return false;
}
// Arg 1: loop_var + 1
if let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = &substr_args[1]
{
// Left: loop_var
if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var) {
return false;
}
// Right: Literal(1)
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
) {
return false;
}
true
} else {
false
}
} else {
false
}
}
/// Extract loop increment from body
///
/// Looks for: `i = i + 1`
///
/// Returns: Some(step_lit) or None
fn extract_loop_increment(body: &[ASTNode], loop_var: &str) -> Option<i64> {
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: BinaryOperator::Add,
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()
{
return Some(*lit);
}
}
}
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
fn make_condition(var: &str, haystack: &str) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: haystack.to_string(),
span: Span::unknown(),
}),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
}
}
fn make_predicate_if(receiver: &str, method: &str, haystack: &str, loop_var: &str) -> ASTNode {
ASTNode::If {
condition: Box::new(ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(ASTNode::MethodCall {
object: Box::new(if receiver == "me" {
ASTNode::Me {
span: Span::unknown(),
}
} else {
ASTNode::Variable {
name: receiver.to_string(),
span: Span::unknown(),
}
}),
method: method.to_string(),
arguments: vec![ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: haystack.to_string(),
span: Span::unknown(),
}),
method: "substring".to_string(),
arguments: vec![
ASTNode::Variable {
name: loop_var.to_string(),
span: Span::unknown(),
},
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: loop_var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
},
],
span: Span::unknown(),
}],
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Return {
value: Some(Box::new(ASTNode::Literal {
value: LiteralValue::Bool(false),
span: Span::unknown(),
})),
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
}
}
fn make_loop_increment(var: &str, step: i64) -> ASTNode {
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(step),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}
}
#[test]
fn test_extract_pattern8_success() {
// loop(i < s.length()) { if not me.is_digit(s.substring(i, i + 1)) { return false } i = i + 1 }
let condition = make_condition("i", "s");
let body = vec![
make_predicate_if("me", "is_digit", "s", "i"),
make_loop_increment("i", 1),
];
let result = extract_pattern8_plan(&condition, &body);
assert!(result.is_ok());
let plan = result.unwrap();
assert!(plan.is_some());
if let Some(DomainPlan::Pattern8BoolPredicateScan(p)) = plan {
assert_eq!(p.loop_var, "i");
assert_eq!(p.haystack, "s");
assert_eq!(p.predicate_receiver, "me");
assert_eq!(p.predicate_method, "is_digit");
assert_eq!(p.step_lit, 1);
} else {
panic!("Expected Pattern8BoolPredicateScan");
}
}
#[test]
fn test_extract_pattern8_wrong_step_returns_none() {
// loop(i < s.length()) { ... i = i + 2 } <- wrong step
let condition = make_condition("i", "s");
let body = vec![
make_predicate_if("me", "is_digit", "s", "i"),
make_loop_increment("i", 2),
];
let result = extract_pattern8_plan(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Wrong step → None
}
#[test]
fn test_extract_pattern8_no_predicate_returns_none() {
// loop(i < s.length()) { i = i + 1 } <- no predicate check
let condition = make_condition("i", "s");
let body = vec![make_loop_increment("i", 1)];
let result = extract_pattern8_plan(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // No predicate → None
}
}

View File

@ -185,6 +185,7 @@ enum PlanExtractorVariant {
///
/// Order is important: more specific patterns first
/// - Pattern6/7: Need fn_body for capture analysis
/// - Pattern8: Bool predicate scan (early-exit return, more specific)
/// - Pattern4: Continue loop (more specific than Pattern1)
/// - Pattern9: Accum const loop (2 carriers, more specific than Pattern1)
/// - Pattern1: Simple while (fallback)
@ -197,6 +198,10 @@ static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
name: "Pattern7_SplitScan (Phase 273)",
extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan),
},
PlanExtractorEntry {
name: "Pattern8_BoolPredicateScan (Phase 286 P2.4)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern8::extract_pattern8_plan),
},
PlanExtractorEntry {
name: "Pattern4_Continue (Phase 286 P2)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern4::extract_pattern4_plan),
@ -238,17 +243,31 @@ fn try_plan_extractors(
}
};
// If extraction succeeded, lower and return
// If extraction succeeded, try lowering
if let Some(domain_plan) = plan_opt {
let log_msg = format!("route=plan strategy=extract pattern={}", entry.name);
trace::trace().pattern("route", &log_msg, true);
return lower_via_plan(builder, domain_plan, ctx);
}
// Extraction returned None - try next extractor
if ctx.debug {
let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name);
trace::trace().debug("route", &debug_msg);
// Phase 286 P2.4: Catch normalization errors for PoC fallback
// Pattern8 PoC returns error from normalizer to trigger legacy fallback
match lower_via_plan(builder, domain_plan, ctx) {
Ok(result) => return Ok(result),
Err(e) if e.contains("[normalizer/pattern8]") => {
// Pattern8 PoC: normalization not yet supported, continue to legacy Pattern8
if ctx.debug {
trace::trace().debug("route", &format!("Pattern8 Plan PoC: normalization failed ({}), continuing to legacy Pattern8", e));
}
// Continue to next extractor (or legacy patterns)
continue; // Explicit continue to skip "extraction returned None" log
}
Err(e) => return Err(e), // Real errors propagate
}
} else {
// Extraction returned None - try next extractor
if ctx.debug {
let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name);
trace::trace().debug("route", &debug_msg);
}
}
}