diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index d3e9dc3d..2a9c808e 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -21,6 +21,9 @@ JoinIR 回帰の integration gate を phase29ae pack に固定し、phase143_* **2025-12-29: Phase 29aj P7 COMPLETE (Pattern8 planner-first)** Pattern8 BoolPredicateScan の Facts→Planner-first を導入し、single_planner の extractor 依存を縮小。 +**2025-12-29: Phase 29aj P8 COMPLETE (Pattern9 planner-first)** +Pattern9 AccumConstLoop の Facts→Planner-first を導入し、single_planner の extractor 依存を縮小。 + **2025-12-29: Phase 29ai P15 COMPLETE (Pattern2 promotion hint observe)** strict/dev 時のみ `[plan/pattern2/promotion_hint:{TrimSeg|DigitPos}]` を観測できるようにし、次は promotion hint の Plan/Frag 吸収残作業(P16候補)。 diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 69882b6d..5184653b 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,9 +2,14 @@ ## Current Focus: Phase 29aj(PlannerOutcome SSOT) -Next: Phase 29aj P8(TBD) +Next: Phase 29aj P9(TBD) 運用ルール: integration filter で phase143_* は回さない(JoinIR 回帰は phase29ae pack のみ) +**2025-12-29: Phase 29aj P8 完了** ✅ +- 目的: Pattern9(AccumConstLoop)を Facts→Planner-first に移行(仕様不変) +- 実装: `src/mir/builder/control_flow/plan/facts/pattern9_accum_const_loop_facts.rs` / `src/mir/builder/control_flow/plan/planner/build.rs` / `src/mir/builder/control_flow/plan/single_planner/rules.rs` +- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` PASS + **2025-12-29: Phase 29aj P7 完了** ✅ - 目的: Pattern8(BoolPredicateScan)を Facts→Planner-first に移行(仕様不変) - 実装: `src/mir/builder/control_flow/plan/facts/pattern8_bool_predicate_scan_facts.rs` / `src/mir/builder/control_flow/plan/planner/build.rs` / `src/mir/builder/control_flow/plan/single_planner/rules.rs` diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 9f1b17e9..fc606c85 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -19,8 +19,8 @@ Related: - **Phase 29aj(candidate): PlannerOutcome observability SSOT** - 入口: `docs/development/current/main/phases/phase-29aj/README.md` - - 状況: P0/P1/P2/P3/P4/P5/P6/P7 ✅ 完了 - - Next: Phase 29aj P8(TBD) + - 状況: P0/P1/P2/P3/P4/P5/P6/P7/P8 ✅ 完了 + - Next: Phase 29aj P9(TBD) - 運用: integration filter で phase143_* は回さない(JoinIR 回帰は phase29ae pack のみ) - **Phase 29ai(candidate): Plan/Frag single-planner(Facts SSOT)** diff --git a/docs/development/current/main/phases/phase-29aj/P8-PATTERN9-ACCUM-CONST-PLANNER-FIRST-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29aj/P8-PATTERN9-ACCUM-CONST-PLANNER-FIRST-INSTRUCTIONS.md new file mode 100644 index 00000000..384a14c5 --- /dev/null +++ b/docs/development/current/main/phases/phase-29aj/P8-PATTERN9-ACCUM-CONST-PLANNER-FIRST-INSTRUCTIONS.md @@ -0,0 +1,94 @@ +# Phase 29aj P8: Pattern9 AccumConstLoop planner-first via Facts (subset) + +Date: 2025-12-29 +Status: Ready for execution +Scope: Pattern9 facts → planner candidate → single_planner planner-first(仕様不変) +Goal: Pattern9 を Facts→Planner に乗せ、extractor 依存を 1 本減らす + +## Objective + +- Pattern9(AccumConstLoop)を LoopFacts に追加し、planner が `DomainPlan::Pattern9AccumConstLoop` を返せるようにする +- single_planner は Pattern9 の型一致時のみ planner-first 採用 +- 既定挙動・観測・エラー文字列は不変 + +## Non-goals + +- Pattern9 サブセット拡張(acc_update の複雑化、複数acc、step=-1 等) +- ルール順序 SSOT の CandidateSet 移管 +- 新 env var / 新ログ追加 + +## Implementation Steps + +### Step 1: Facts SSOT 追加(Pattern9) + +Files: +- `src/mir/builder/control_flow/plan/facts/pattern9_accum_const_loop_facts.rs` (new) + +Facts: +- `Pattern9AccumConstLoopFacts { loop_var, acc_var, condition, acc_update, loop_increment }` + +Extraction rules (Ok(None) fallback only): +- condition は ` < ` のみ +- body に break/continue/if-else が無い +- acc update は `acc = acc + ` のみ +- loop_increment は `extract_loop_increment_plan(body, loop_var)` が取れる +- acc_var != loop_var + +Unit tests: +- const accumulation 成功 +- break/continue/if-else 混入は Ok(None) +- `sum = sum + i` は Ok(None) + +### Step 2: LoopFacts に接続 + +Files: +- `src/mir/builder/control_flow/plan/facts/mod.rs` +- `src/mir/builder/control_flow/plan/facts/loop_facts.rs` + +Changes: +- LoopFacts に `pattern9_accum_const_loop` を追加 +- `try_build_loop_facts()` に抽出を追加 +- all-none 判定に `pattern9_accum_const_loop` を含める + +### Step 3: Planner candidate 追加 + +File: +- `src/mir/builder/control_flow/plan/planner/build.rs` + +Changes: +- facts が Some のとき `DomainPlan::Pattern9AccumConstLoop` を候補に追加 +- rule 名は `loop/pattern9_accum_const_loop` +- unit test 追加 + +### Step 4: single_planner を Pattern9 planner-first に + +File: +- `src/mir/builder/control_flow/plan/single_planner/rules.rs` + +Changes: +- RuleKind::Pattern9 を追加 +- planner_opt が Pattern9 のとき採用 +- それ以外は extractor へフォールバック + +### Step 5: docs / CURRENT_TASK 更新 + +Files: +- `docs/development/current/main/phases/phase-29aj/README.md` +- `docs/development/current/main/10-Now.md` +- `docs/development/current/main/30-Backlog.md` +- `CURRENT_TASK.md` + +## Acceptance Criteria + +- `cargo build --release` +- `./tools/smokes/v2/run.sh --profile quick` +- `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase286_pattern9_"` (任意) + +## Commit + +- `git add -A && git commit -m "phase29aj(p8): planner-first pattern9 accum const loop subset"` + +## Next (P9 candidate) + +- TBD diff --git a/docs/development/current/main/phases/phase-29aj/README.md b/docs/development/current/main/phases/phase-29aj/README.md index d4e54ef3..9e76789e 100644 --- a/docs/development/current/main/phases/phase-29aj/README.md +++ b/docs/development/current/main/phases/phase-29aj/README.md @@ -59,6 +59,13 @@ Goal: planner の facts/plan を 1 本の outcome に集約し、観測の SSOT - 完了: Pattern8 facts/planner/single_planner を接続 - 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` +## P8: Pattern9 (AccumConstLoop) planner-first(subset) + +- 指示書: `docs/development/current/main/phases/phase-29aj/P8-PATTERN9-ACCUM-CONST-PLANNER-FIRST-INSTRUCTIONS.md` +- ねらい: Pattern9 を Facts→Planner-first に接続し、extractor 依存を削減 +- 完了: Pattern9 facts/planner/single_planner を接続 +- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` + ## Verification (SSOT) - `cargo build --release` diff --git a/src/mir/builder/control_flow/plan/facts/loop_facts.rs b/src/mir/builder/control_flow/plan/facts/loop_facts.rs index 6de5a801..34005d60 100644 --- a/src/mir/builder/control_flow/plan/facts/loop_facts.rs +++ b/src/mir/builder/control_flow/plan/facts/loop_facts.rs @@ -28,6 +28,9 @@ use super::pattern5_infinite_early_exit_facts::{ use super::pattern8_bool_predicate_scan_facts::{ Pattern8BoolPredicateScanFacts, try_extract_pattern8_bool_predicate_scan_facts, }; +use super::pattern9_accum_const_loop_facts::{ + Pattern9AccumConstLoopFacts, try_extract_pattern9_accum_const_loop_facts, +}; use super::pattern2_break_facts::{Pattern2BreakFacts, try_extract_pattern2_break_facts}; use super::pattern2_loopbodylocal_facts::{ Pattern2LoopBodyLocalFacts, try_extract_pattern2_loopbodylocal_facts, @@ -44,6 +47,7 @@ pub(in crate::mir::builder) struct LoopFacts { pub pattern4_continue: Option, pub pattern5_infinite_early_exit: Option, pub pattern8_bool_predicate_scan: Option, + pub pattern9_accum_const_loop: Option, pub pattern2_break: Option, pub pattern2_loopbodylocal: Option, } @@ -88,6 +92,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts( &condition_shape, &step_shape, )?; + let pattern9_accum_const_loop = try_extract_pattern9_accum_const_loop_facts(condition, body)?; let pattern2_break = try_extract_pattern2_break_facts(condition, body)?; let pattern2_loopbodylocal = try_extract_pattern2_loopbodylocal_facts(condition, body)?; @@ -98,6 +103,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts( && pattern4_continue.is_none() && pattern5_infinite_early_exit.is_none() && pattern8_bool_predicate_scan.is_none() + && pattern9_accum_const_loop.is_none() && pattern2_break.is_none() && pattern2_loopbodylocal.is_none() { @@ -114,6 +120,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts( pattern4_continue, pattern5_infinite_early_exit, pattern8_bool_predicate_scan, + pattern9_accum_const_loop, pattern2_break, pattern2_loopbodylocal, })) diff --git a/src/mir/builder/control_flow/plan/facts/mod.rs b/src/mir/builder/control_flow/plan/facts/mod.rs index f3cf834e..5e745c92 100644 --- a/src/mir/builder/control_flow/plan/facts/mod.rs +++ b/src/mir/builder/control_flow/plan/facts/mod.rs @@ -12,6 +12,7 @@ pub(in crate::mir::builder) mod pattern3_ifphi_facts; pub(in crate::mir::builder) mod pattern4_continue_facts; pub(in crate::mir::builder) mod pattern5_infinite_early_exit_facts; pub(in crate::mir::builder) mod pattern8_bool_predicate_scan_facts; +pub(in crate::mir::builder) mod pattern9_accum_const_loop_facts; pub(in crate::mir::builder) mod pattern2_break_facts; pub(in crate::mir::builder) mod pattern2_loopbodylocal_facts; pub(in crate::mir::builder) mod scan_shapes; diff --git a/src/mir/builder/control_flow/plan/facts/pattern9_accum_const_loop_facts.rs b/src/mir/builder/control_flow/plan/facts/pattern9_accum_const_loop_facts.rs new file mode 100644 index 00000000..977074a1 --- /dev/null +++ b/src/mir/builder/control_flow/plan/facts/pattern9_accum_const_loop_facts.rs @@ -0,0 +1,245 @@ +//! Phase 29aj P8: Pattern9AccumConstLoopFacts (Facts SSOT) + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; +use crate::mir::builder::control_flow::plan::extractors::common_helpers::{ + extract_loop_increment_plan, has_break_statement, has_continue_statement, has_if_else_statement, +}; +use crate::mir::builder::control_flow::plan::planner::Freeze; + +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern9AccumConstLoopFacts { + pub loop_var: String, + pub acc_var: String, + pub condition: ASTNode, + pub acc_update: ASTNode, + pub loop_increment: ASTNode, +} + +pub(in crate::mir::builder) fn try_extract_pattern9_accum_const_loop_facts( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, Freeze> { + let Some(loop_var) = extract_loop_var_for_subset(condition) else { + return Ok(None); + }; + + if has_break_statement(body) || has_continue_statement(body) || has_if_else_statement(body) { + return Ok(None); + } + + if body.len() != 2 { + return Ok(None); + } + + let (acc_var, acc_update) = match extract_accum_const_update(&body[0], &loop_var) { + Some(values) => values, + None => return Ok(None), + }; + + let loop_increment = match extract_loop_increment_plan(&body[1..], &loop_var) { + Ok(Some(inc)) => inc, + _ => return Ok(None), + }; + + Ok(Some(Pattern9AccumConstLoopFacts { + loop_var, + acc_var, + condition: condition.clone(), + acc_update, + loop_increment, + })) +} + +fn extract_loop_var_for_subset(condition: &ASTNode) -> Option { + let ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left, + right, + .. + } = condition + else { + return None; + }; + + let ASTNode::Variable { name, .. } = left.as_ref() else { + return None; + }; + + if !matches!( + right.as_ref(), + ASTNode::Literal { + value: LiteralValue::Integer(_), + .. + } + ) { + return None; + } + + Some(name.clone()) +} + +fn extract_accum_const_update( + stmt: &ASTNode, + loop_var: &str, +) -> Option<(String, ASTNode)> { + let ASTNode::Assignment { target, value, .. } = stmt else { + return None; + }; + let ASTNode::Variable { name: acc_var, .. } = target.as_ref() else { + return None; + }; + if acc_var == loop_var { + return None; + } + + let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } = value.as_ref() + else { + return None; + }; + + if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == acc_var) { + return None; + } + + if !matches!( + right.as_ref(), + ASTNode::Literal { + value: LiteralValue::Integer(_), + .. + } + ) { + return None; + } + + Some((acc_var.clone(), value.as_ref().clone())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + + fn v(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + fn lit_int(value: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(value), + span: Span::unknown(), + } + } + + fn condition_lt(loop_var: &str, bound: i64) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(v(loop_var)), + right: Box::new(lit_int(bound)), + span: Span::unknown(), + } + } + + fn accum_const(acc_var: &str, value: i64) -> ASTNode { + ASTNode::Assignment { + target: Box::new(v(acc_var)), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v(acc_var)), + right: Box::new(lit_int(value)), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn accum_var(acc_var: &str, rhs: &str) -> ASTNode { + ASTNode::Assignment { + target: Box::new(v(acc_var)), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v(acc_var)), + right: Box::new(v(rhs)), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn increment(loop_var: &str, step: i64) -> ASTNode { + ASTNode::Assignment { + target: Box::new(v(loop_var)), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v(loop_var)), + right: Box::new(lit_int(step)), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn if_else_stub() -> ASTNode { + ASTNode::If { + condition: Box::new(v("cond")), + then_body: vec![ASTNode::Return { + value: None, + span: Span::unknown(), + }], + else_body: Some(vec![ASTNode::Return { + value: None, + span: Span::unknown(), + }]), + span: Span::unknown(), + } + } + + #[test] + fn facts_extracts_pattern9_const_accum_success() { + let condition = condition_lt("i", 3); + let body = vec![accum_const("sum", 1), increment("i", 1)]; + + let facts = try_extract_pattern9_accum_const_loop_facts(&condition, &body).expect("Ok"); + let facts = facts.expect("Some"); + + assert_eq!(facts.loop_var, "i"); + assert_eq!(facts.acc_var, "sum"); + } + + #[test] + fn facts_rejects_break_or_continue() { + let condition = condition_lt("i", 3); + let body = vec![ + ASTNode::Break { span: Span::unknown() }, + increment("i", 1), + ]; + + let facts = try_extract_pattern9_accum_const_loop_facts(&condition, &body).expect("Ok"); + assert!(facts.is_none()); + } + + #[test] + fn facts_rejects_if_else() { + let condition = condition_lt("i", 3); + let body = vec![if_else_stub(), increment("i", 1)]; + + let facts = try_extract_pattern9_accum_const_loop_facts(&condition, &body).expect("Ok"); + assert!(facts.is_none()); + } + + #[test] + fn facts_rejects_var_accumulation() { + let condition = condition_lt("i", 3); + let body = vec![accum_var("sum", "i"), increment("i", 1)]; + + let facts = try_extract_pattern9_accum_const_loop_facts(&condition, &body).expect("Ok"); + assert!(facts.is_none()); + } +} diff --git a/src/mir/builder/control_flow/plan/planner/build.rs b/src/mir/builder/control_flow/plan/planner/build.rs index a79226c9..6b139360 100644 --- a/src/mir/builder/control_flow/plan/planner/build.rs +++ b/src/mir/builder/control_flow/plan/planner/build.rs @@ -12,7 +12,7 @@ use super::Freeze; use crate::mir::builder::control_flow::plan::{ DomainPlan, Pattern1SimpleWhilePlan, Pattern2BreakPlan, Pattern2PromotionHint, Pattern3IfPhiPlan, Pattern4ContinuePlan, Pattern5InfiniteEarlyExitPlan, Pattern8BoolPredicateScanPlan, - ScanDirection, ScanWithInitPlan, SplitScanPlan, + Pattern9AccumConstLoopPlan, ScanDirection, ScanWithInitPlan, SplitScanPlan, }; /// Phase 29ai P0: External-ish SSOT entrypoint (skeleton) @@ -147,6 +147,19 @@ pub(in crate::mir::builder) fn build_plan_from_facts( }); } + if let Some(pattern9) = &facts.facts.pattern9_accum_const_loop { + candidates.push(PlanCandidate { + plan: DomainPlan::Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan { + loop_var: pattern9.loop_var.clone(), + acc_var: pattern9.acc_var.clone(), + condition: pattern9.condition.clone(), + acc_update: pattern9.acc_update.clone(), + loop_increment: pattern9.loop_increment.clone(), + }), + rule: "loop/pattern9_accum_const_loop", + }); + } + if let Some(pattern1) = &facts.facts.pattern1_simplewhile { candidates.push(PlanCandidate { plan: DomainPlan::Pattern1SimpleWhile(Pattern1SimpleWhilePlan { @@ -185,6 +198,9 @@ mod tests { use crate::mir::builder::control_flow::plan::facts::pattern8_bool_predicate_scan_facts::{ Pattern8BoolPredicateScanFacts, }; + use crate::mir::builder::control_flow::plan::facts::pattern9_accum_const_loop_facts::{ + Pattern9AccumConstLoopFacts, + }; use crate::mir::builder::control_flow::plan::facts::pattern2_break_facts::Pattern2BreakFacts; use crate::mir::builder::control_flow::plan::facts::pattern2_loopbodylocal_facts::{ LoopBodyLocalShape, Pattern2LoopBodyLocalFacts, @@ -226,6 +242,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -256,6 +273,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -281,6 +299,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -323,6 +342,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -388,6 +408,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -446,6 +467,7 @@ mod tests { }), pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -494,6 +516,7 @@ mod tests { loop_increment: loop_increment.clone(), }), pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -540,6 +563,7 @@ mod tests { condition: condition.clone(), step_lit: 1, }), + pattern9_accum_const_loop: None, pattern2_break: None, pattern2_loopbodylocal: None, }; @@ -557,6 +581,59 @@ mod tests { } } + #[test] + fn planner_builds_pattern9_accum_const_loop_plan_from_facts() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(v("i")), + right: Box::new(lit_int(3)), + span: Span::unknown(), + }; + let acc_update = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("sum")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }; + let loop_increment = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("i")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }; + + let facts = LoopFacts { + condition_shape: ConditionShape::Unknown, + step_shape: StepShape::Unknown, + scan_with_init: None, + split_scan: None, + pattern1_simplewhile: None, + pattern3_ifphi: None, + pattern4_continue: None, + pattern5_infinite_early_exit: None, + pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: Some(Pattern9AccumConstLoopFacts { + loop_var: "i".to_string(), + acc_var: "sum".to_string(), + condition: condition.clone(), + acc_update: acc_update.clone(), + loop_increment: loop_increment.clone(), + }), + pattern2_break: None, + pattern2_loopbodylocal: None, + }; + let canonical = canonicalize_loop_facts(facts); + let plan = build_plan_from_facts(canonical).expect("Ok"); + + match plan { + Some(DomainPlan::Pattern9AccumConstLoop(plan)) => { + assert_eq!(plan.loop_var, "i"); + assert_eq!(plan.acc_var, "sum"); + } + other => panic!("expected pattern9 accum const loop plan, got {:?}", other), + } + } + #[test] fn planner_sets_promotion_hint_for_pattern2_loopbodylocal() { let loop_condition = ASTNode::BinaryOp { @@ -597,6 +674,7 @@ mod tests { pattern4_continue: None, pattern5_infinite_early_exit: None, pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, pattern2_break: Some(Pattern2BreakFacts { loop_var: "i".to_string(), carrier_var: "sum".to_string(), diff --git a/src/mir/builder/control_flow/plan/single_planner/rules.rs b/src/mir/builder/control_flow/plan/single_planner/rules.rs index 296eed1d..435fd6d7 100644 --- a/src/mir/builder/control_flow/plan/single_planner/rules.rs +++ b/src/mir/builder/control_flow/plan/single_planner/rules.rs @@ -3,7 +3,6 @@ //! IMPORTANT: Keep rule order identical to the legacy `PLAN_EXTRACTORS` table //! (observability/behavior must not change). -use crate::ast::ASTNode; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::loop_pattern_detection::LoopPatternKind; @@ -52,7 +51,7 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result Result (extract(ctx.condition, ctx.body)?, true), RuleKind::Pattern6 => { match planner_opt.as_ref() { Some(DomainPlan::ScanWithInit(_)) => (planner_opt.clone(), false), @@ -113,6 +111,12 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result (extractors::pattern8::extract_pattern8_plan(ctx.condition, ctx.body)?, true), } } + RuleKind::Pattern9 => { + match planner_opt.as_ref() { + Some(DomainPlan::Pattern9AccumConstLoop(_)) => (planner_opt.clone(), false), + _ => (extractors::pattern9::extract_pattern9_plan(ctx.condition, ctx.body)?, true), + } + } RuleKind::Pattern2 => { match planner_opt.as_ref() { Some(DomainPlan::Pattern2Break(_)) => (planner_opt.clone(), false), @@ -195,11 +199,11 @@ struct RuleEntry { #[derive(Debug, Clone, Copy)] enum RuleKind { - Simple(fn(&ASTNode, &[ASTNode]) -> Result, String>), Pattern6, Pattern7, Pattern5, Pattern8, + Pattern9, Pattern2, Pattern3, Pattern4,