feat(plan): Phase 282 P5 - Pattern3 ExtractionBased Migration & Classification

- Pattern3 extraction logic separated to extractors/pattern3.rs
- ExtractionBased strategy: pure functions, Fail-Fast, SSOT
- Pattern classification restored (AST-based pattern detection)
- Pattern1 extractor migrated (extractors/pattern1.rs)
- Documentation: phase-282 README updated, joinir-architecture updated

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 08:13:47 +09:00
parent c3fb3c5833
commit 32ccccd272
7 changed files with 608 additions and 28 deletions

View File

@ -198,10 +198,22 @@ fn detect_if_in_body(body: &[ASTNode]) -> bool {
/// Effect: Loops with conditional assignment fall through to Pattern1.
///
/// Phase 264 P1: TODO - Implement accurate if-sum signature detection.
fn detect_if_else_phi_in_body(_body: &[ASTNode]) -> bool {
// Phase 264 P0: Conservative - always return false
// This prevents simple conditional assignments from being classified as Pattern3IfPhi
false
fn detect_if_else_phi_in_body(body: &[ASTNode]) -> bool {
// Phase 282 P5: Proper if-else PHI detection (re-enabled with ExtractionBased safety)
//
// This function provides initial classification for Pattern3IfPhi.
// The actual validation is done by extractors::pattern3::extract_loop_with_if_phi_parts()
// which performs deep checks (PHI assignments, no control flow, etc.)
//
// Here we just check: Does the loop body contain an if-else statement?
// This allows Pattern3 to be attempted, and extraction will validate.
for stmt in body {
if matches!(stmt, ASTNode::If { else_body: Some(_), .. }) {
return true; // Found if-else
}
}
false // No if-else found
}
/// Count carrier variables (variables assigned in loop body)

View File

@ -14,11 +14,13 @@
//!
//! - Pattern1: ✅ Migrated (Phase 282 P3)
//! - Pattern2: ✅ Migrated (Phase 282 P4)
//! - Pattern3-5: ⏸ Pending (future phases)
//! - Pattern3: ✅ Migrated (Phase 282 P5)
//! - Pattern4-5: ⏸ Pending (future phases)
//! - Pattern6-7: ✅ Plan-based (Phase 273, different path)
//! - Pattern8-9: ✅ Already ExtractionBased (Phase 259/270)
pub(crate) mod pattern1;
pub(crate) mod pattern2; // Phase 282 P4: Pattern2 extraction
pub(crate) mod pattern3; // Phase 282 P5: Pattern3 extraction
// Future: pattern3, pattern4, pattern5
// Future: pattern4, pattern5

View File

@ -55,7 +55,9 @@ pub(crate) fn extract_simple_while_parts(
}
/// Validate condition: 比較演算 (左辺が変数)
fn validate_condition_structure(condition: &ASTNode) -> Option<String> {
///
/// Exported for reuse by Pattern3 (Phase 282 P5)
pub(crate) fn validate_condition_structure(condition: &ASTNode) -> Option<String> {
match condition {
ASTNode::BinaryOp { operator, left, .. } => {
// 比較演算子チェック

View File

@ -0,0 +1,509 @@
//! Phase 282 P5: Pattern3 (Loop with If-Else PHI) Extraction
use crate::ast::{ASTNode, BinaryOperator};
#[derive(Debug, Clone)]
pub(crate) struct Pattern3Parts {
pub loop_var: String, // Loop variable name (e.g., "i")
pub merged_var: String, // Primary PHI carrier (e.g., "sum")
pub carrier_count: usize, // Validation: 1-2 accumulators
// Note: has_else (always true), phi_like_merge (implicit) omitted
// AST reused from ctx - no duplication
}
/// Extract Pattern3 (Loop with If-Else PHI) parts
///
/// # Detection Criteria
///
/// 1. **Condition**: 比較演算 (left=variable)
/// 2. **Body**: At least one if-else statement (else REQUIRED)
/// 3. **Assignments**: Both branches assign to same variable(s)
/// 4. **Control Flow**: NO break/continue/nested-if (return → Ok(None))
///
/// # Four-Phase Validation
///
/// **Phase 1**: Validate condition structure (reuse Pattern1)
/// **Phase 2**: Find if-else statement (else branch REQUIRED)
/// **Phase 3**: Validate PHI assignments (intersection of then/else)
/// **Phase 4**: Validate NO control flow
///
/// # Fail-Fast Rules
///
/// - `Ok(Some(parts))`: Pattern3 confirmed
/// - `Ok(None)`: Not Pattern3 (structural mismatch)
/// - `Err(msg)`: Logic bug (malformed AST)
pub(crate) fn extract_loop_with_if_phi_parts(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<Pattern3Parts>, String> {
// Phase 1: Validate condition (reuse Pattern1)
use super::pattern1::validate_condition_structure;
let loop_var = match validate_condition_structure(condition) {
Some(var) => var,
None => return Ok(None),
};
// Phase 2: Find if-else statement
let if_stmt = match find_if_else_statement(body) {
Some(stmt) => stmt,
None => return Ok(None), // No if-else → Not Pattern3
};
// Phase 3: Validate PHI assignments
let merged_vars = match extract_phi_assignments(if_stmt) {
Some(vars) if !vars.is_empty() => vars,
_ => return Ok(None), // No matching assignments → Not Pattern3
};
// Phase 4a: Check for return (early Ok(None) - let other patterns try)
if has_return_statement(body) {
return Ok(None); // Has return → delegate to other patterns
}
// Phase 4b: Validate NO control flow (break/continue/nested-if only)
if has_control_flow_statement(body) {
return Ok(None); // Has break/continue/nested-if → Not Pattern3
}
// Extract primary carrier (first merged var)
let merged_var = merged_vars[0].clone();
let carrier_count = merged_vars.len();
Ok(Some(Pattern3Parts {
loop_var,
merged_var,
carrier_count,
}))
}
/// Find if-else statement in loop body
///
/// Returns first if statement with else branch.
/// Allows multiple if statements - just finds the first if-else.
fn find_if_else_statement(body: &[ASTNode]) -> Option<&ASTNode> {
for stmt in body {
if matches!(stmt, ASTNode::If { else_body: Some(_), .. }) {
return Some(stmt); // Found first if-else
}
}
None // No if-else found
}
/// Extract variables assigned in BOTH then and else branches
fn extract_phi_assignments(if_stmt: &ASTNode) -> Option<Vec<String>> {
let (then_body, else_body) = match if_stmt {
ASTNode::If {
then_body,
else_body: Some(else_body),
..
} => (then_body, else_body),
_ => return None,
};
let then_assignments = extract_assignment_targets(then_body);
let else_assignments = extract_assignment_targets(else_body);
// Find intersection
let mut merged_vars = Vec::new();
for var in &then_assignments {
if else_assignments.contains(var) {
merged_vars.push(var.clone());
}
}
if merged_vars.is_empty() {
return None;
}
// Use first occurrence (AST order) - deterministic and meaningful SSOT
// Don't sort alphabetically - preserve natural appearance order
Some(merged_vars)
}
fn extract_assignment_targets(body: &[ASTNode]) -> Vec<String> {
let mut targets = Vec::new();
for stmt in body {
if let ASTNode::Assignment { target, .. } = stmt {
if let ASTNode::Variable { name, .. } = target.as_ref() {
targets.push(name.clone());
}
}
}
targets
}
/// Check for return statements (Pattern3 version)
///
/// Return → Ok(None) to let other patterns try.
/// This is checked separately before control flow validation.
fn has_return_statement(body: &[ASTNode]) -> bool {
for stmt in body {
if has_return_recursive(stmt) {
return true;
}
}
false
}
fn has_return_recursive(node: &ASTNode) -> bool {
match node {
ASTNode::Return { .. } => true,
ASTNode::If {
then_body,
else_body,
..
} => {
then_body.iter().any(has_return_recursive)
|| else_body
.as_ref()
.map_or(false, |b| b.iter().any(has_return_recursive))
}
_ => false,
}
}
/// Check for control flow (Pattern3 version)
///
/// Pattern3 allows ONE if-else (that's the PHI pattern).
/// Reject: break, continue, NESTED if only (return checked separately).
fn has_control_flow_statement(body: &[ASTNode]) -> bool {
for stmt in body {
if has_control_flow_recursive_p3(stmt) {
return true;
}
}
false
}
fn has_control_flow_recursive_p3(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } | ASTNode::Continue { .. } => true,
// Return removed - checked separately
ASTNode::If {
then_body,
else_body,
..
} => {
// Check for NESTED if (reject)
let has_nested_then = then_body.iter().any(|n| matches!(n, ASTNode::If { .. }));
let has_nested_else = else_body
.as_ref()
.map_or(false, |b| b.iter().any(|n| matches!(n, ASTNode::If { .. })));
if has_nested_then || has_nested_else {
return true;
}
// Check control flow INSIDE branches
then_body.iter().any(has_control_flow_recursive_p3)
|| else_body
.as_ref()
.map_or(false, |b| b.iter().any(has_control_flow_recursive_p3))
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{LiteralValue, Span};
#[test]
fn test_extract_if_phi_success() {
// loop(i < 3) { if (i > 0) { sum = sum + 1 } else { sum = sum + 0 } i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(3),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![
ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Greater,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}],
else_body: Some(vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}]),
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
let result = extract_loop_with_if_phi_parts(&condition, &body);
assert!(result.is_ok());
let parts = result.unwrap();
assert!(parts.is_some());
let parts = parts.unwrap();
assert_eq!(parts.loop_var, "i");
assert_eq!(parts.merged_var, "sum");
assert_eq!(parts.carrier_count, 1);
}
#[test]
fn test_extract_no_else_returns_none() {
// loop(i < 3) { if (i > 0) { sum = sum + 1 } i = i + 1 } // No else
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(3),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Greater,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}],
else_body: None, // ← No else
span: Span::unknown(),
}];
let result = extract_loop_with_if_phi_parts(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // No else → Not Pattern3
}
#[test]
fn test_extract_different_vars_returns_none() {
// loop(i < 3) { if (i > 0) { sum = 1 } else { count = 1 } i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(3),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Greater,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(), // then: sum
span: Span::unknown(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}],
else_body: Some(vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "count".to_string(), // else: count (different!)
span: Span::unknown(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}]),
span: Span::unknown(),
}];
let result = extract_loop_with_if_phi_parts(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Different vars → Not Pattern3
}
#[test]
fn test_extract_with_break_returns_none() {
// loop(i < 3) { if (i > 0) { sum = sum + 1; break } else { sum = sum + 0 } i = i + 1 }
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(3),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let body = vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Greater,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
ASTNode::Break {
span: Span::unknown(),
}, // ← break
],
else_body: Some(vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}]),
span: Span::unknown(),
}];
let result = extract_loop_with_if_phi_parts(&condition, &body);
assert!(result.is_ok());
assert!(result.unwrap().is_none()); // Has break → Not Pattern3
}
}

View File

@ -19,24 +19,79 @@ use crate::mir::ValueId;
/// Phase 194: Detection function for Pattern 3
///
/// Phase 192: Updated to structure-based detection
/// Phase 282 P5: Updated to ExtractionBased detection with safety valve
///
/// Pattern 3 matches:
/// - Pattern kind is Pattern3IfPhi (has if-else with PHI, no break/continue)
///
/// NOTE: Priority is now handled by pattern classification, not router order
/// - Pattern kind is Pattern3IfPhi (safety valve, O(1) early rejection)
/// - Extraction validates: if-else PHI + NO break/continue/nested-if (return → Ok(None))
pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use crate::mir::loop_pattern_detection::LoopPatternKind;
ctx.pattern_kind == LoopPatternKind::Pattern3IfPhi
// Step 1: Early rejection guard (safety valve, O(1))
if ctx.pattern_kind != LoopPatternKind::Pattern3IfPhi {
if ctx.debug {
trace::trace().debug(
"pattern3/can_lower",
&format!("reject: pattern_kind={:?}", ctx.pattern_kind),
);
}
return false;
}
// Step 2: ExtractionBased validation (SSOT, deep check)
use super::extractors::pattern3::extract_loop_with_if_phi_parts;
match extract_loop_with_if_phi_parts(ctx.condition, ctx.body) {
Ok(Some(_)) => {
if ctx.debug {
trace::trace().debug("pattern3/can_lower", "accept: extractable (Phase 282 P5)");
}
true
}
Ok(None) => {
if ctx.debug {
trace::trace().debug(
"pattern3/can_lower",
"reject: not Pattern3 (no if-else PHI or has control flow)",
);
}
false
}
Err(e) => {
if ctx.debug {
trace::trace().debug("pattern3/can_lower", &format!("error: {}", e));
}
false
}
}
}
/// Phase 194: Lowering function for Pattern 3
///
/// Phase 282 P5: Re-extracts for SSOT (no caching from can_lower)
///
/// Wrapper around cf_loop_pattern3_with_if_phi to match router signature
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
use super::extractors::pattern3::extract_loop_with_if_phi_parts;
// Re-extract (SSOT principle - no caching from can_lower)
let parts = extract_loop_with_if_phi_parts(ctx.condition, ctx.body)?
.ok_or_else(|| "[pattern3] Not a loop with if-phi pattern in lower()".to_string())?;
if ctx.debug {
trace::trace().debug(
"pattern3/lower",
&format!(
"loop_var={}, merged_var={}, carriers={} (Phase 282 P5)",
parts.loop_var, parts.merged_var, parts.carrier_count
),
);
}
// Call existing orchestrator implementation (completely unchanged)
builder.cf_loop_pattern3_with_if_phi(ctx.condition, ctx.body, ctx.func_name, ctx.debug)
}