feat(joinir): Phase 286 P2.3 + Phase 287 - Pattern9 Plan化 + Router table-driven

## Phase 286 P2.3: Pattern9 AccumConstLoop Plan化 PoC

- DomainPlan::Pattern9AccumConstLoop 追加
- PlanNormalizer::normalize_pattern9_accum_const_loop() 実装
  - PHI 2本(loop_var, acc_var)
  - const/var 両方 OK(sum = sum + 1 または sum = sum + i)
- Pattern9 は Pattern1 より優先(より具体的なパターン)
- Integration test: phase286_pattern9_frag_poc PASS (return: 3)
- Regression: quick 154 PASS

## Phase 287: Router table-driven Plan extraction

- PLAN_EXTRACTORS static table で Pattern6/7/4/9/1 を統一管理
- PlanExtractorEntry/PlanExtractorVariant 構造体追加
- try_plan_extractors() で ~100行 → 3行に集約
- メンテナンス性向上(新 Pattern 追加はテーブル1行追加のみ)

🤖 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 02:38:09 +09:00
parent 6656098c95
commit 1d24e9a106
11 changed files with 760 additions and 99 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 pattern9; // Phase 286 P2.3: Pattern9 Plan extraction
// Phase 282 P9a: Common extraction helpers
// Provides shared utilities for Pattern1, 2, 4, 5 (~400 lines reduction):

View File

@ -0,0 +1,318 @@
//! Phase 286 P2.3: Pattern9 (AccumConstLoop) Extraction
//!
//! Minimal subset extractor for Pattern9 Plan line.
//!
//! # Supported subset (PoC safety)
//!
//! - Loop condition: `<var> < <int_lit>`
//! - Body: 2 assignments only (順序固定)
//! 1. `<acc_var> = <acc_var> + <expr>` (const OR var accumulation)
//! 2. `<loop_var> = <loop_var> + <int_lit>` (increment)
//! - No break/continue/if/return
//!
//! Returns Ok(None) for unsupported patterns → legacy fallback
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern9AccumConstLoopPlan};
// Phase 286 P2.3: Use common_helpers
use super::common_helpers::{extract_loop_increment_plan, has_control_flow_statement};
/// Phase 286 P2.3: Minimal subset extractor for Pattern9 Plan line
///
/// # Detection Criteria
///
/// 1. **Condition**: `<var> < <int_lit>`
/// 2. **Body**: Exactly 2 assignments in fixed order
/// - 1st: `<acc_var> = <acc_var> + <expr>` (accumulation)
/// - 2nd: `<loop_var> = <loop_var> + <int_lit>` (increment)
/// 3. **No control flow**: No break/continue/if-else
///
/// # Returns
///
/// - `Ok(Some(plan))`: Pattern9 match confirmed
/// - `Ok(None)`: Not Pattern9 (構造不一致 or unsupported)
/// - `Err(msg)`: Logic bug (malformed AST)
pub(crate) fn extract_pattern9_plan(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<DomainPlan>, String> {
// Step 1: Validate loop condition is `<var> < <int_lit>`
let loop_var = match validate_loop_condition_plan(condition) {
Some(var) => var,
None => return Ok(None), // Unsupported condition format
};
// Step 2: Reject control flow (break/continue/if-else)
if has_control_flow_statement(body) {
return Ok(None); // Has break/continue → Not Pattern9 for Plan line
}
// Step 3: Validate body has exactly 2 assignments
if body.len() != 2 {
return Ok(None); // Must be exactly 2 statements
}
// Step 4: Extract accumulator update (1st statement)
// Supports: `<acc_var> = <acc_var> + <expr>` (const OR var)
let (acc_var, acc_update) = match extract_accum_update(&body[0], &loop_var) {
Some((var, update)) => (var, update),
None => return Ok(None), // Not a valid accumulator update
};
// Step 5: Extract loop increment (2nd statement)
// Uses common_helpers::extract_loop_increment_plan
let loop_increment = match extract_loop_increment_plan(&body[1..], &loop_var)? {
Some(inc) => inc,
None => return Ok(None), // No loop increment found
};
Ok(Some(DomainPlan::Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan {
loop_var,
acc_var,
condition: condition.clone(),
acc_update,
loop_increment,
})))
}
/// Validate loop condition: supports `<var> < <int_lit>` only
fn validate_loop_condition_plan(cond: &ASTNode) -> Option<String> {
if let ASTNode::BinaryOp { operator, left, right, .. } = cond {
if !matches!(operator, BinaryOperator::Less) {
return None; // Only < supported for PoC
}
// Left must be a variable
let var_name = if let ASTNode::Variable { name, .. } = left.as_ref() {
name.clone()
} else {
return None;
};
// Right must be integer literal
if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) {
return None;
}
Some(var_name)
} else {
None
}
}
/// Extract accumulator update from assignment
///
/// Supports: `<acc_var> = <acc_var> + <expr>` where <expr> is const OR var
///
/// Returns: Some((acc_var, update_expr)) or None
fn extract_accum_update(stmt: &ASTNode, loop_var: &str) -> Option<(String, ASTNode)> {
if let ASTNode::Assignment { target, value, .. } = stmt {
// target must be a variable (different from loop_var)
if let ASTNode::Variable { name: target_var, .. } = target.as_ref() {
if target_var == loop_var {
return None; // This is the loop increment, not accumulator
}
// value must be `<acc_var> + <expr>`
if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() {
if !matches!(operator, BinaryOperator::Add) {
return None; // Only + supported
}
// Left must be same as target (acc_var = acc_var + ...)
if let ASTNode::Variable { name: left_var, .. } = left.as_ref() {
if left_var != target_var {
return None; // Not a self-update
}
} else {
return None;
}
// Right can be const OR var (ChatGPT feedback: both OK)
// Accept: Literal (const) or Variable (var)
match right.as_ref() {
ASTNode::Literal { value: LiteralValue::Integer(_), .. } => {
// const accumulation: sum = sum + 1
return Some((target_var.clone(), value.as_ref().clone()));
}
ASTNode::Variable { .. } => {
// var accumulation: sum = sum + i
return Some((target_var.clone(), value.as_ref().clone()));
}
_ => return None, // Complex expression not supported
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
fn make_condition(var: &str, bound: i64) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(bound),
span: Span::unknown(),
}),
span: Span::unknown(),
}
}
fn make_accum_const_update(acc_var: &str, val: i64) -> ASTNode {
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: acc_var.to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: acc_var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(val),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}
}
fn make_accum_var_update(acc_var: &str, rhs_var: &str) -> ASTNode {
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: acc_var.to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: acc_var.to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: rhs_var.to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
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_pattern9_const_accum_success() {
// loop(i < 3) { sum = sum + 1; i = i + 1 }
let condition = make_condition("i", 3);
let body = vec![
make_accum_const_update("sum", 1),
make_loop_increment("i", 1),
];
let result = extract_pattern9_plan(&condition, &body);
assert!(result.is_ok());
let plan = result.unwrap();
assert!(plan.is_some());
if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan {
assert_eq!(p.loop_var, "i");
assert_eq!(p.acc_var, "sum");
} else {
panic!("Expected Pattern9AccumConstLoop");
}
}
#[test]
fn test_extract_pattern9_var_accum_success() {
// loop(i < 3) { sum = sum + i; i = i + 1 }
let condition = make_condition("i", 3);
let body = vec![
make_accum_var_update("sum", "i"),
make_loop_increment("i", 1),
];
let result = extract_pattern9_plan(&condition, &body);
assert!(result.is_ok());
let plan = result.unwrap();
assert!(plan.is_some());
if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan {
assert_eq!(p.loop_var, "i");
assert_eq!(p.acc_var, "sum");
} else {
panic!("Expected Pattern9AccumConstLoop");
}
}
#[test]
fn test_extract_pattern9_wrong_order_returns_none() {
// loop(i < 3) { i = i + 1; sum = sum + 1 } <- wrong order
let condition = make_condition("i", 3);
let body = vec![
make_loop_increment("i", 1),
make_accum_const_update("sum", 1),
];
let result = extract_pattern9_plan(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Wrong order → None
}
#[test]
fn test_extract_pattern9_with_break_returns_none() {
let condition = make_condition("i", 3);
let body = vec![
ASTNode::Break { span: Span::unknown() },
make_accum_const_update("sum", 1),
make_loop_increment("i", 1),
];
let result = extract_pattern9_plan(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Has break → None
}
#[test]
fn test_extract_pattern9_single_stmt_returns_none() {
let condition = make_condition("i", 3);
let body = vec![make_loop_increment("i", 1)];
let result = extract_pattern9_plan(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Only 1 statement → None
}
}

View File

@ -153,6 +153,109 @@ fn lower_via_plan(
PlanLowerer::lower(builder, core_plan, ctx)
}
/// Phase 287: Plan extractor function type for routing table
///
/// Extractors have two signatures:
/// - Simple: (condition, body) - Pattern1/4/9
/// - WithFnBody: (condition, body, fn_body) - Pattern6/7
type SimplePlanExtractor = fn(&ASTNode, &[ASTNode]) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>;
/// Phase 287: Plan extractor table entry
struct PlanExtractorEntry {
/// Pattern name for debug logging
name: &'static str,
/// Extractor function
extractor: PlanExtractorVariant,
}
/// Phase 287: Extractor function variant (handles different signatures)
enum PlanExtractorVariant {
/// Simple extractor: (condition, body)
Simple(SimplePlanExtractor),
/// Extractor with fn_body: (condition, body, fn_body) - Pattern6
WithFnBody(fn(&ASTNode, &[ASTNode], Option<&[ASTNode]>) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>),
/// Extractor with post_loop_code: (condition, body, post_loop_code) - Pattern7
WithPostLoop(fn(&ASTNode, &[ASTNode], &[ASTNode]) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>),
}
/// Phase 287: Plan extractor routing table (SSOT)
///
/// Order is important: more specific patterns first
/// - Pattern6/7: Need fn_body for capture analysis
/// - Pattern4: Continue loop (more specific than Pattern1)
/// - Pattern9: Accum const loop (2 carriers, more specific than Pattern1)
/// - Pattern1: Simple while (fallback)
static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
PlanExtractorEntry {
name: "Pattern6_ScanWithInit (Phase 273)",
extractor: PlanExtractorVariant::WithFnBody(super::pattern6_scan_with_init::extract_scan_with_init_plan),
},
PlanExtractorEntry {
name: "Pattern7_SplitScan (Phase 273)",
extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan),
},
PlanExtractorEntry {
name: "Pattern4_Continue (Phase 286 P2)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern4::extract_pattern4_plan),
},
PlanExtractorEntry {
name: "Pattern9_AccumConstLoop (Phase 286 P2.3)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern9::extract_pattern9_plan),
},
PlanExtractorEntry {
name: "Pattern1_SimpleWhile (Phase 286 P2.1)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern1::extract_pattern1_plan),
},
];
/// Phase 287: Try all plan extractors in priority order
///
/// Returns Ok(Some(value_id)) if any extractor succeeds
/// Returns Ok(None) if no extractor matches
/// Returns Err if extraction or lowering fails
fn try_plan_extractors(
builder: &mut MirBuilder,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
use super::super::trace;
for entry in PLAN_EXTRACTORS {
// Try extraction based on variant
let plan_opt = match &entry.extractor {
PlanExtractorVariant::Simple(extractor) => {
extractor(ctx.condition, ctx.body)?
}
PlanExtractorVariant::WithFnBody(extractor) => {
// Pattern6: needs fn_body for capture analysis
extractor(ctx.condition, ctx.body, ctx.fn_body)?
}
PlanExtractorVariant::WithPostLoop(extractor) => {
// Pattern7: uses empty slice for post_loop_code (3rd param)
extractor(ctx.condition, ctx.body, &[])?
}
};
// If extraction succeeded, lower and return
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);
}
}
// No extractor matched
Ok(None)
}
/// Phase 272 P0.2 Refactoring: can_lower() strategy classification
///
/// Clarifies the two main detection strategies used across patterns:
@ -318,98 +421,10 @@ pub(crate) fn route_loop_pattern(
) -> Result<Option<ValueId>, String> {
use super::super::trace;
// Phase 273 P1: Try Plan-based Pattern6 first (before table iteration)
// Flow: Extract → Normalize → Verify → Lower
match super::pattern6_scan_with_init::extract_scan_with_init_plan(
ctx.condition,
ctx.body,
ctx.fn_body,
)? {
Some(domain_plan) => {
// DomainPlan extracted successfully
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern6_ScanWithInit (Phase 273)", true);
// Phase 286 P2.2: Use common helper
return lower_via_plan(builder, domain_plan, ctx);
}
None => {
// Not Pattern6 - continue to other patterns
if ctx.debug {
trace::trace().debug(
"route",
"Pattern6 Plan extraction returned None, trying other patterns",
);
}
}
}
// Phase 273 P2: Try Plan-based Pattern7 (SplitScan)
// Flow: Extract → Normalize → Verify → Lower
match super::pattern7_split_scan::extract_split_scan_plan(
ctx.condition,
ctx.body,
&[],
)? {
Some(domain_plan) => {
// DomainPlan extracted successfully
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern7_SplitScan (Phase 273)", true);
// Phase 286 P2.2: Use common helper
return lower_via_plan(builder, domain_plan, ctx);
}
None => {
// Not Pattern7 - continue to other patterns
if ctx.debug {
trace::trace().debug(
"route",
"Pattern7 Plan extraction returned None, trying other patterns",
);
}
}
}
// Phase 286 P2: Try Plan-based Pattern4 (Loop with Continue)
// Flow: Extract → Normalize → Verify → Lower
match super::extractors::pattern4::extract_pattern4_plan(
ctx.condition,
ctx.body,
)? {
Some(domain_plan) => {
// DomainPlan extracted successfully
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern4_Continue (Phase 286 P2)", true);
// Phase 286 P2.2: Use common helper
return lower_via_plan(builder, domain_plan, ctx);
}
None => {
// Not Pattern4 Plan - continue to legacy Pattern4
if ctx.debug {
trace::trace().debug(
"route",
"Pattern4 Plan extraction returned None, trying legacy Pattern4",
);
}
}
}
// Phase 286 P2.1: Try Plan-based Pattern1 (Simple While Loop)
// Flow: Extract → Normalize → Verify → Lower
match super::extractors::pattern1::extract_pattern1_plan(
ctx.condition,
ctx.body,
)? {
Some(domain_plan) => {
// DomainPlan extracted successfully
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern1_SimpleWhile (Phase 286 P2.1)", true);
// Phase 286 P2.2: Use common helper
return lower_via_plan(builder, domain_plan, ctx);
}
None => {
// Not Pattern1 Plan - continue to legacy Pattern1
if ctx.debug {
trace::trace().debug(
"route",
"Pattern1 Plan extraction returned None, trying legacy Pattern1",
);
}
}
// Phase 287: Try all Plan extractors in priority order (Pattern6/7/4/9/1)
// This replaces ~100 lines of repetitive extraction blocks
if let Some(value_id) = try_plan_extractors(builder, ctx)? {
return Ok(Some(value_id));
}
// Phase 183: Route based on pre-classified pattern kind