phase29ak(p5): ctx-aware planner candidates; fix phase1883 routing

This commit is contained in:
2025-12-29 15:50:57 +09:00
parent afe12ffa35
commit 14013cbe1f
14 changed files with 335 additions and 53 deletions

View File

@ -88,12 +88,22 @@ pub(crate) fn count_control_flow(
ASTNode::Return { .. } if detector.count_returns => {
counts.return_count += 1;
}
ASTNode::Loop { .. } if depth > 0 => {
ASTNode::ScopeBox { body, .. } => {
for stmt in body {
scan_node(stmt, counts, detector, depth);
}
}
ASTNode::Loop { body, .. }
| ASTNode::While { body, .. }
| ASTNode::ForRange { body, .. } => {
counts.has_nested_loop = true;
// Skip nested loop bodies if configured
if detector.skip_nested_control_flow {
return;
}
for stmt in body {
scan_node(stmt, counts, detector, depth + 1);
}
}
ASTNode::If {
then_body,
@ -358,6 +368,17 @@ mod tests {
}
}
fn make_nested_loop() -> ASTNode {
ASTNode::Loop {
condition: Box::new(ASTNode::Variable {
name: "cond".to_string(),
span: Span::unknown(),
}),
body: vec![make_break()],
span: Span::unknown(),
}
}
#[test]
fn test_count_control_flow_break() {
let body = vec![make_break()];
@ -417,6 +438,22 @@ mod tests {
assert!(!has_control_flow_statement(&body));
}
#[test]
fn test_count_control_flow_detects_nested_loop_at_top_level() {
let body = vec![make_nested_loop()];
let counts = count_control_flow(&body, ControlFlowDetector::default());
assert!(counts.has_nested_loop);
}
#[test]
fn test_has_control_flow_statement_break_in_scopebox() {
let body = vec![ASTNode::ScopeBox {
body: vec![make_break()],
span: Span::unknown(),
}];
assert!(has_control_flow_statement(&body));
}
#[test]
fn test_extract_loop_variable_success() {
let condition = ASTNode::BinaryOp {

View File

@ -17,7 +17,7 @@ pub(crate) struct Pattern1Parts {
/// # Detection Criteria (誤マッチ防止強化版)
///
/// 1. **Condition**: 比較演算(<, <=, >, >=, ==, !=)で左辺が変数
/// 2. **Body**: No break/continue/if-else-phi (return is allowed - it's not loop control flow)
/// 2. **Body**: No break/continue/nested-loop/if-else-phi (return is allowed - it's not loop control flow)
/// 3. **Step**: 単純な増減パターン (i = i + 1, i = i - 1 など)
///
/// # Four-Phase Validation
@ -48,6 +48,18 @@ pub(crate) fn extract_simple_while_parts(
return Ok(None);
}
// Phase 29ak: Reject nested loops.
//
// Nested loops are Pattern6NestedLoopMinimal territory; letting Pattern1 match them
// causes routing to short-circuit before JoinIR can select the nested-loop lowerer.
let counts = super::common_helpers::count_control_flow(
body,
super::common_helpers::ControlFlowDetector::default(),
);
if counts.has_nested_loop {
return Ok(None);
}
// Phase 286 P2.6: Reject if-else statements (Pattern3 territory)
// Pattern1 allows simple if without else, but not if-else (which is Pattern3)
if super::common_helpers::has_if_else_statement(body) {

View File

@ -7,6 +7,7 @@ use crate::ast::ASTNode;
use crate::mir::builder::control_flow::plan::normalize::CanonicalLoopFacts;
use super::candidates::{CandidateSet, PlanCandidate};
use super::context::PlannerContext;
use super::outcome::build_plan_with_facts;
use super::Freeze;
use crate::mir::builder::control_flow::plan::{
@ -14,6 +15,7 @@ use crate::mir::builder::control_flow::plan::{
Pattern4ContinuePlan, Pattern5InfiniteEarlyExitPlan, Pattern8BoolPredicateScanPlan,
Pattern9AccumConstLoopPlan, ScanDirection, ScanWithInitPlan, SplitScanPlan,
};
use crate::mir::loop_pattern_detection::LoopPatternKind;
/// Phase 29ai P0: External-ish SSOT entrypoint (skeleton)
///
@ -27,6 +29,13 @@ pub(in crate::mir::builder) fn build_plan(
pub(in crate::mir::builder) fn build_plan_from_facts(
facts: CanonicalLoopFacts,
) -> Result<Option<DomainPlan>, Freeze> {
build_plan_from_facts_ctx(&PlannerContext::default_for_legacy(), facts)
}
pub(in crate::mir::builder) fn build_plan_from_facts_ctx(
ctx: &PlannerContext,
facts: CanonicalLoopFacts,
) -> Result<Option<DomainPlan>, Freeze> {
// Phase 29ai P3: CandidateSet-based boundary (SSOT)
//
@ -34,6 +43,12 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
// unreachable in normal execution today. We still implement the SSOT
// boundary here so that future Facts work cannot drift.
let allow_pattern1 = match ctx.pattern_kind {
Some(LoopPatternKind::Pattern1SimpleWhile) | None => true,
Some(_) => false,
};
let allow_pattern8 = !ctx.in_static_box;
let mut candidates = CandidateSet::new();
if let Some(scan) = &facts.facts.scan_with_init {
@ -133,18 +148,20 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
});
}
if let Some(pattern8) = &facts.facts.pattern8_bool_predicate_scan {
candidates.push(PlanCandidate {
plan: DomainPlan::Pattern8BoolPredicateScan(Pattern8BoolPredicateScanPlan {
loop_var: pattern8.loop_var.clone(),
haystack: pattern8.haystack.clone(),
predicate_receiver: pattern8.predicate_receiver.clone(),
predicate_method: pattern8.predicate_method.clone(),
condition: pattern8.condition.clone(),
step_lit: pattern8.step_lit,
}),
rule: "loop/pattern8_bool_predicate_scan",
});
if allow_pattern8 {
if let Some(pattern8) = &facts.facts.pattern8_bool_predicate_scan {
candidates.push(PlanCandidate {
plan: DomainPlan::Pattern8BoolPredicateScan(Pattern8BoolPredicateScanPlan {
loop_var: pattern8.loop_var.clone(),
haystack: pattern8.haystack.clone(),
predicate_receiver: pattern8.predicate_receiver.clone(),
predicate_method: pattern8.predicate_method.clone(),
condition: pattern8.condition.clone(),
step_lit: pattern8.step_lit,
}),
rule: "loop/pattern8_bool_predicate_scan",
});
}
}
if let Some(pattern9) = &facts.facts.pattern9_accum_const_loop {
@ -160,15 +177,17 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
});
}
if let Some(pattern1) = &facts.facts.pattern1_simplewhile {
candidates.push(PlanCandidate {
plan: DomainPlan::Pattern1SimpleWhile(Pattern1SimpleWhilePlan {
loop_var: pattern1.loop_var.clone(),
condition: pattern1.condition.clone(),
loop_increment: pattern1.loop_increment.clone(),
}),
rule: "loop/pattern1_simplewhile",
});
if allow_pattern1 {
if let Some(pattern1) = &facts.facts.pattern1_simplewhile {
candidates.push(PlanCandidate {
plan: DomainPlan::Pattern1SimpleWhile(Pattern1SimpleWhilePlan {
loop_var: pattern1.loop_var.clone(),
condition: pattern1.condition.clone(),
loop_increment: pattern1.loop_increment.clone(),
}),
rule: "loop/pattern1_simplewhile",
});
}
}
candidates.finalize()

View File

@ -6,3 +6,13 @@ pub(in crate::mir::builder) struct PlannerContext {
pub in_static_box: bool,
pub debug: bool,
}
impl PlannerContext {
pub(in crate::mir::builder) fn default_for_legacy() -> Self {
Self {
pattern_kind: None,
in_static_box: false,
debug: false,
}
}
}

View File

@ -11,7 +11,7 @@ pub(in crate::mir::builder) mod context;
pub(in crate::mir::builder) mod freeze;
pub(in crate::mir::builder) mod outcome;
pub(in crate::mir::builder) use build::build_plan;
pub(in crate::mir::builder) use build::{build_plan, build_plan_from_facts_ctx};
pub(in crate::mir::builder) use context::PlannerContext;
pub(in crate::mir::builder) use freeze::Freeze;
pub(in crate::mir::builder) use outcome::{

View File

@ -9,7 +9,7 @@ use crate::mir::builder::control_flow::plan::normalize::{
};
use crate::mir::builder::control_flow::plan::DomainPlan;
use super::build::build_plan_from_facts;
use super::build::build_plan_from_facts_ctx;
use super::context::PlannerContext;
use super::Freeze;
@ -24,7 +24,8 @@ pub(in crate::mir::builder) fn build_plan_with_facts(
body: &[ASTNode],
) -> Result<PlanBuildOutcome, Freeze> {
let facts = try_build_loop_facts(condition, body)?;
build_plan_from_facts_opt(facts)
let legacy_ctx = PlannerContext::default_for_legacy();
build_plan_from_facts_opt_with(&legacy_ctx, facts)
}
pub(in crate::mir::builder) fn build_plan_with_facts_ctx(
@ -33,10 +34,11 @@ pub(in crate::mir::builder) fn build_plan_with_facts_ctx(
body: &[ASTNode],
) -> Result<PlanBuildOutcome, Freeze> {
let facts = try_build_loop_facts_with_ctx(ctx, condition, body)?;
build_plan_from_facts_opt(facts)
build_plan_from_facts_opt_with(ctx, facts)
}
fn build_plan_from_facts_opt(
fn build_plan_from_facts_opt_with(
ctx: &PlannerContext,
facts: Option<LoopFacts>,
) -> Result<PlanBuildOutcome, Freeze> {
let Some(facts) = facts else {
@ -46,7 +48,7 @@ fn build_plan_from_facts_opt(
});
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical.clone())?;
let plan = build_plan_from_facts_ctx(ctx, canonical.clone())?;
Ok(PlanBuildOutcome {
facts: Some(canonical),

View File

@ -4,7 +4,6 @@
//! (observability/behavior must not change).
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::loop_pattern_detection::LoopPatternKind;
use crate::mir::builder::control_flow::plan::extractors;
use crate::mir::builder::control_flow::plan::facts::pattern2_loopbodylocal_facts::LoopBodyLocalShape;
@ -33,22 +32,11 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result<Option<D
let rule_id = *rule_id;
let name = rule_name(rule_id);
let planner_hit = try_take_planner(&planner_opt, rule_id);
// Pattern1 gating is handled by planner/facts; fallback mirrors the same contract.
let allow_pattern1 = match ctx.pattern_kind {
LoopPatternKind::Pattern1SimpleWhile => true,
_ => false,
};
let allow_pattern8 = !ctx.in_static_box;
let (plan_opt, log_none) = if planner_hit.is_some() {
(planner_hit, false)
} else {
let plan_opt = fallback_extract(ctx, rule_id, allow_pattern1, allow_pattern8)?;
let log_none = if matches!(rule_id, PlanRuleId::Pattern1) {
allow_pattern1
} else {
true
};
(plan_opt, log_none)
(fallback_extract(ctx, rule_id, allow_pattern8)?, true)
};
let promotion_tag = if matches!(rule_id, PlanRuleId::Pattern2)
@ -103,16 +91,10 @@ fn try_take_planner(planner_opt: &Option<DomainPlan>, kind: PlanRuleId) -> Optio
fn fallback_extract(
ctx: &LoopPatternContext,
kind: PlanRuleId,
allow_pattern1: bool,
allow_pattern8: bool,
) -> Result<Option<DomainPlan>, String> {
match kind {
PlanRuleId::Pattern1 => {
if !allow_pattern1 {
return Ok(None);
}
extractors::pattern1::extract_pattern1_plan(ctx.condition, ctx.body)
}
PlanRuleId::Pattern1 => extractors::pattern1::extract_pattern1_plan(ctx.condition, ctx.body),
PlanRuleId::Pattern2 => {
extractors::pattern2_break::extract_pattern2_plan(ctx.condition, ctx.body)
}