phase29ai(p5): route JoinIR plan extraction via single_planner

This commit is contained in:
2025-12-29 08:04:00 +09:00
parent ff5c122fdc
commit 5ba68da9a0
16 changed files with 243 additions and 160 deletions

View File

@ -407,7 +407,7 @@ mod tests {
},
];
let ctx = LoopPatternContext::new(&condition, &body, "parse_number_like", true);
let ctx = LoopPatternContext::new(&condition, &body, "parse_number_like", true, false);
let builder = MirBuilder::new();
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern2Break);
@ -452,7 +452,7 @@ mod tests {
},
];
let ctx = LoopPatternContext::new(&condition, &body, "_atoi", true);
let ctx = LoopPatternContext::new(&condition, &body, "_atoi", true, false);
let builder = MirBuilder::new();
// Verify pattern classification

View File

@ -28,6 +28,7 @@ use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
use crate::mir::builder::control_flow::plan::lowerer::PlanLowerer;
use crate::mir::builder::control_flow::plan::normalizer::PlanNormalizer;
use crate::mir::builder::control_flow::plan::verifier::PlanVerifier;
use crate::mir::builder::control_flow::plan::single_planner;
/// AST Feature Extractor (declared in mod.rs as pub module, import from parent)
use super::ast_feature_extractor as ast_features;
@ -49,6 +50,9 @@ pub(crate) struct LoopPatternContext<'a> {
/// Debug logging enabled
pub debug: bool,
/// In static box context? (affects Pattern8 routing)
pub in_static_box: bool,
/// Has continue statement(s) in body? (Phase 194+)
#[allow(dead_code)]
pub has_continue: bool,
@ -93,6 +97,7 @@ impl<'a> LoopPatternContext<'a> {
body: &'a [ASTNode],
func_name: &'a str,
debug: bool,
in_static_box: bool,
) -> Self {
// Use AST Feature Extractor for break/continue detection
let has_continue = ast_features::detect_continue_in_body(body);
@ -110,6 +115,7 @@ impl<'a> LoopPatternContext<'a> {
body,
func_name,
debug,
in_static_box,
has_continue,
has_break,
features,
@ -126,9 +132,10 @@ impl<'a> LoopPatternContext<'a> {
body: &'a [ASTNode],
func_name: &'a str,
debug: bool,
in_static_box: bool,
fn_body: &'a [ASTNode],
) -> Self {
let mut ctx = Self::new(condition, body, func_name, debug);
let mut ctx = Self::new(condition, body, func_name, debug, in_static_box);
ctx.fn_body = Some(fn_body);
ctx
}
@ -159,157 +166,7 @@ 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
///
/// NOTE (Phase 286): Currently always called with &[] for post_loop_code.
/// This variant is kept for future extension (post-loop segment analysis).
/// The _post_loop_code parameter in Pattern7 extractor is intentionally unused.
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
/// - Pattern8: Bool predicate scan (early-exit return, more specific)
/// - Pattern3: If-phi merge (carrier with conditional update, more specific than Pattern4)
/// - 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: "Pattern5_InfiniteEarlyExit (Phase 286 P3.2)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern5::extract_pattern5_plan),
},
PlanExtractorEntry {
name: "Pattern8_BoolPredicateScan (Phase 286 P2.4)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern8::extract_pattern8_plan),
},
PlanExtractorEntry {
name: "Pattern3_IfPhi (Phase 286 P2.6)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern3::extract_pattern3_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: "Pattern2_Break (Phase 286 P3.1)",
extractor: PlanExtractorVariant::Simple(super::extractors::pattern2::extract_pattern2_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 {
// Phase 286 P2.6: Pattern1 Plan guard (structural Fail-Fast)
// Pattern1 should only match Pattern1SimpleWhile pattern_kind
// This prevents Pattern1 from incorrectly matching Pattern3 fixtures
if entry.name.contains("Pattern1") {
use crate::mir::loop_pattern_detection::LoopPatternKind;
if ctx.pattern_kind != LoopPatternKind::Pattern1SimpleWhile {
continue;
}
}
// 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, check if we can lower it
if let Some(domain_plan) = plan_opt {
// Phase 286 P3: Pattern8 static box filtering
// Plan extractors are pure (no builder access), so we filter here
// Static box loops with `me.method()` should be handled by ReceiverNormalizeBox, not Pattern8
if entry.name.contains("Pattern8") && builder.comp_ctx.current_static_box.is_some() {
if ctx.debug {
trace::trace().debug(
"route/plan",
&format!("{} extracted but rejected: static box context (fallback to legacy)", entry.name),
);
}
// Skip this pattern, try next extractor
continue;
}
let log_msg = format!("route=plan strategy=extract pattern={}", entry.name);
trace::trace().pattern("route", &log_msg, true);
// Phase 286 P2.6.1: Fail-Fast 統一 - extract 成功 → normalize/lower 失敗は即 Err
// Pattern3 stub fallback 撤去normalizer 実装済み)
return lower_via_plan(builder, domain_plan, ctx);
} 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);
}
}
}
// No extractor matched
Ok(None)
}
// Phase 29ai P5: Plan extractor routing moved to `plan::single_planner`.
/// Phase 272 P0.2 Refactoring: can_lower() strategy classification
///
@ -473,10 +330,9 @@ pub(crate) fn route_loop_pattern(
) -> Result<Option<ValueId>, String> {
use super::super::trace;
// 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 29ai P5: Single entrypoint for plan extraction (router has no rule table).
if let Some(domain_plan) = single_planner::try_build_domain_plan(ctx)? {
return lower_via_plan(builder, domain_plan, ctx);
}
// Phase 183: Route based on pre-classified pattern kind

View File

@ -452,15 +452,16 @@ impl MirBuilder {
}
),
);
let in_static_box = self.comp_ctx.current_static_box.is_some();
let ctx = if let Some(ref fn_body) = fn_body_clone {
trace::trace().routing(
"router",
func_name,
&format!("Creating ctx with fn_body ({} nodes)", fn_body.len()),
);
LoopPatternContext::with_fn_body(condition, body, &func_name, debug, fn_body)
LoopPatternContext::with_fn_body(condition, body, &func_name, debug, in_static_box, fn_body)
} else {
LoopPatternContext::new(condition, body, &func_name, debug)
LoopPatternContext::new(condition, body, &func_name, debug, in_static_box)
};
// Phase 137-4: Router parity verification (after ctx is created)

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 P5: JoinIR router → single plan extraction entrypoint
pub(in crate::mir::builder) mod single_planner;
// ============================================================================
// DomainPlan (Pattern固有)

View File

@ -0,0 +1,14 @@
//! Phase 29ai P5: Legacy rule wrappers (thin bridge)
//!
//! These wrappers must preserve the legacy extractor behavior and error strings.
pub(in crate::mir::builder) mod pattern1;
pub(in crate::mir::builder) mod pattern2;
pub(in crate::mir::builder) mod pattern3;
pub(in crate::mir::builder) mod pattern4;
pub(in crate::mir::builder) mod pattern5;
pub(in crate::mir::builder) mod pattern6;
pub(in crate::mir::builder) mod pattern7;
pub(in crate::mir::builder) mod pattern8;
pub(in crate::mir::builder) mod pattern9;

View File

@ -0,0 +1,9 @@
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::extractors::pattern1::extract_pattern1_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern1_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern2::extract_pattern2_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern2_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern3::extract_pattern3_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern3_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern4::extract_pattern4_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern4_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern5::extract_pattern5_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern5_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_scan_with_init_plan(ctx.condition, ctx.body, ctx.fn_body)
}

View File

@ -0,0 +1,9 @@
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;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_split_scan_plan(ctx.condition, ctx.body, &[])
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern8::extract_pattern8_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern8_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,9 @@
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::extractors::pattern9::extract_pattern9_plan;
pub(in crate::mir::builder) fn extract(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
extract_pattern9_plan(ctx.condition, ctx.body)
}

View File

@ -0,0 +1,18 @@
//! Phase 29ai P5: Single-planner bridge (router → 1 entrypoint)
//!
//! SSOT entrypoint for DomainPlan extraction. Router should call only this.
//! Contract: keep `Result<_, String>` to preserve existing behavior/messages.
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use super::DomainPlan;
pub(in crate::mir::builder) mod legacy_rules;
mod rules;
pub(in crate::mir::builder) fn try_build_domain_plan(
ctx: &LoopPatternContext,
) -> Result<Option<DomainPlan>, String> {
rules::try_build_domain_plan(ctx)
}

View File

@ -0,0 +1,111 @@
//! Phase 29ai P5: Rule list (order SSOT) + guards
//!
//! IMPORTANT: Keep rule order identical to the legacy `PLAN_EXTRACTORS` table
//! (observability/behavior must not change).
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::loop_pattern_detection::LoopPatternKind;
use super::legacy_rules;
use crate::mir::builder::control_flow::plan::DomainPlan;
pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result<Option<DomainPlan>, String> {
use crate::mir::builder::control_flow::joinir::trace;
// NOTE: Names must match the previous PLAN_EXTRACTORS entries exactly.
// Order must match exactly.
let rules: &[RuleEntry] = &[
RuleEntry {
name: "Pattern6_ScanWithInit (Phase 273)",
kind: RuleKind::Pattern6,
},
RuleEntry {
name: "Pattern7_SplitScan (Phase 273)",
kind: RuleKind::Pattern7,
},
RuleEntry {
name: "Pattern5_InfiniteEarlyExit (Phase 286 P3.2)",
kind: RuleKind::Simple(legacy_rules::pattern5::extract),
},
RuleEntry {
name: "Pattern8_BoolPredicateScan (Phase 286 P2.4)",
kind: RuleKind::Simple(legacy_rules::pattern8::extract),
},
RuleEntry {
name: "Pattern3_IfPhi (Phase 286 P2.6)",
kind: RuleKind::Simple(legacy_rules::pattern3::extract),
},
RuleEntry {
name: "Pattern4_Continue (Phase 286 P2)",
kind: RuleKind::Simple(legacy_rules::pattern4::extract),
},
RuleEntry {
name: "Pattern9_AccumConstLoop (Phase 286 P2.3)",
kind: RuleKind::Simple(legacy_rules::pattern9::extract),
},
RuleEntry {
name: "Pattern2_Break (Phase 286 P3.1)",
kind: RuleKind::Simple(legacy_rules::pattern2::extract),
},
RuleEntry {
name: "Pattern1_SimpleWhile (Phase 286 P2.1)",
kind: RuleKind::Simple(legacy_rules::pattern1::extract),
},
];
for entry in rules {
// Phase 286 P2.6: Pattern1 Plan guard (structural Fail-Fast)
// Pattern1 should only match Pattern1SimpleWhile pattern_kind
// This prevents Pattern1 from incorrectly matching Pattern3 fixtures
if entry.name.contains("Pattern1") && ctx.pattern_kind != LoopPatternKind::Pattern1SimpleWhile
{
continue;
}
let plan_opt = match entry.kind {
RuleKind::Simple(extract) => extract(ctx),
RuleKind::Pattern6 => legacy_rules::pattern6::extract(ctx),
RuleKind::Pattern7 => legacy_rules::pattern7::extract(ctx),
}?;
if let Some(domain_plan) = plan_opt {
// Phase 286 P3: Pattern8 static box filtering
// Plan extractors are pure (no builder access), so we filter here.
if entry.name.contains("Pattern8") && ctx.in_static_box {
if ctx.debug {
trace::trace().debug(
"route/plan",
&format!(
"{} extracted but rejected: static box context (fallback to legacy)",
entry.name
),
);
}
continue;
}
let log_msg = format!("route=plan strategy=extract pattern={}", entry.name);
trace::trace().pattern("route", &log_msg, true);
return Ok(Some(domain_plan));
} else if ctx.debug {
let debug_msg =
format!("{} extraction returned None, trying next pattern", entry.name);
trace::trace().debug("route", &debug_msg);
}
}
Ok(None)
}
#[derive(Debug, Clone, Copy)]
struct RuleEntry {
name: &'static str,
kind: RuleKind,
}
#[derive(Debug, Clone, Copy)]
enum RuleKind {
Simple(fn(&LoopPatternContext) -> Result<Option<DomainPlan>, String>),
Pattern6,
Pattern7,
}