phase29ai(p9): planner-first pattern7 split-scan subset

This commit is contained in:
2025-12-29 09:17:53 +09:00
parent 82b0a87599
commit 9abc726394
7 changed files with 715 additions and 11 deletions

View File

@ -2,7 +2,12 @@
## Current Focus: Phase 29aiPlan/Frag single-planner
Next: `docs/development/current/main/phases/phase-29ai/P9-PLANNER-PATTERN7-SPLITSCAN-WIRE-INSTRUCTIONS.md`
Next: `docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md`
**2025-12-29: Phase 29ai P9 完了**
- 目的: Pattern7 split-scan subset を Facts→Planner→DomainPlan まで到達させ、single_planner の Pattern7 で planner-first を開始(仕様不変)
- 実装: `src/mir/builder/control_flow/plan/facts/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 29ai P8 完了**
- 目的: Facts→Planner を実行経路へ 1 歩だけ接続し、Pattern6scan-with-initの subset から吸収を開始(仕様不変)

View File

@ -19,7 +19,7 @@ Related:
- **Phase 29aicandidate: Plan/Frag single-plannerFacts SSOT**
- 入口: `docs/development/current/main/phases/phase-29ai/README.md`
- Next: P9Planner: Pattern7 split-scan subset
- Next: P10Move Pattern2 extractor → plan layer
- **Phase 29ae P1✅ COMPLETE: JoinIR Regression Pack (SSOT固定)**
- 入口: `docs/development/current/main/phases/phase-29ae/README.md`

View File

@ -0,0 +1,106 @@
# Phase 29ai P10: Move Pattern2 extractor to plan layerSSOT
Date: 2025-12-29
Status: Ready for execution
Scope: Pattern2Break / LoopBodyLocal / promotion抽出の SSOT を plan 層へ移設(仕様不変)
Goal: JoinIR 側の “pattern固有知識” を削減し、依存方向を `joinir → plan` の一方向に固定する
## Objective
Pattern2 の DomainPlan 抽出(`DomainPlan::Pattern2Break`)を `joinir/patterns/extractors/*` から `plan/extractors/*` に移し、
JoinIR 側は薄い wrapperre-exportに縮退させる。`single_planner` の legacy_rules も plan 側 extractor を参照するように統一する。
この P10 は “移設だけ” を目的にし、抽出ロジックの意味論は変えないpure extraction のまま)。
## Non-goalsこの P10 ではやらない)
- Facts→Planner へ Pattern2 を吸収するP11+
- promotion policy / NotApplicable / Freeze など契約内容の変更
- 新しい fixture/smoke 追加(既存回帰で担保)
- env var / デバッグトグル追加
- by-name ディスパッチの追加(禁止)
## Current State
- Pattern2 extractorJoinIR 側):
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs`
- `-> Result<Option<DomainPlan>, String>``DomainPlan::Pattern2Break(Pattern2BreakPlan { ... })` を返す
- single_planner legacy:
- `src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs`
- 現状は JoinIR 側 extractor を呼んでいる
## Target Architecture
```
src/mir/builder/control_flow/plan/extractors/
├── mod.rs
├── pattern6_scan_with_init.rs
├── pattern7_split_scan.rs
└── pattern2_break.rs ✨ NEWP10
src/mir/builder/control_flow/joinir/patterns/extractors/
├── pattern2.rs (縮退: plan 側へ delegate / re-export)
└── mod.rs (必要なら export 更新)
```
## Implementation StepsCritical Order
### Step 1: plan 側へ extractor を追加pure, 既存コードの移設)
追加:
- `src/mir/builder/control_flow/plan/extractors/pattern2_break.rs`
方針:
- `joinir/patterns/extractors/pattern2.rs` の実装を **意味論そのまま** 移植import path を plan 側に合わせるだけ)。
- 返り値型/引数/エラーメッセージは変更しない。
### Step 2: plan/extractors/mod.rs に登録
更新:
- `src/mir/builder/control_flow/plan/extractors/mod.rs`
追加:
- `pub(in crate::mir::builder) mod pattern2_break;`
- `pub(in crate::mir::builder) use pattern2_break::extract_pattern2_break_plan;`(既存命名に合わせる)
### Step 3: JoinIR 側 extractor を wrapper 化
更新:
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs`
方針:
- 関数はそのまま残す(外部呼び出し互換)。
- 中身は plan 側 extractor を呼ぶだけにする(または re-export
- JoinIR 側の helper/private 関数が必要なら、先に plan 側へ移して SSOT を plan に寄せる。
### Step 4: single_planner legacy_rules の参照先を plan 側に統一
更新:
- `src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs`
方針:
- `crate::mir::builder::control_flow::plan::extractors::pattern2_break::*` を呼ぶように変更。
- 返り値/ログ/順序は維持。
### Step 5: SSOT docs 更新
更新:
- `docs/development/current/main/phases/phase-29ai/README.md`
- `docs/development/current/main/10-Now.md`
- `docs/development/current/main/30-Backlog.md`
最低限:
- P10 の scope移設のみ・仕様不変を明記。
- NextP11候補を 1 行だけ(例: Pattern2 を Facts→Planner の subset に吸収)。
## Verification (SSOT)
- `cargo build --release`
- `./tools/smokes/v2/run.sh --profile quick`
- `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh`
期待:
- 既定挙動不変で PASS
- 新しいログ増加なし
- 新しい warning 増加なし

View File

@ -53,6 +53,13 @@ Goal: pattern 名による分岐を外部APIから消し、Facts事実
- 指示書: `docs/development/current/main/phases/phase-29ai/P9-PLANNER-PATTERN7-SPLITSCAN-WIRE-INSTRUCTIONS.md`
- ねらい: Pattern7split-scanの最小ケースを Facts→Planner で `Ok(Some(DomainPlan::SplitScan))` まで到達させ、single_planner で planner-first を開始(仕様不変を維持しつつ段階吸収)
- 完了: Facts split-scan subset / planner candidate / single_planner Pattern7 planner-first を接続
- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh`
## P10: Move Pattern2 extractor to plan layerSSOT
- 指示書: `docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md`
- ねらい: Pattern2 の抽出pattern固有知識を plan 側へ寄せて依存方向を一方向に固定(仕様不変)
## Verification (SSOT)

View File

@ -19,6 +19,7 @@ pub(in crate::mir::builder) struct LoopFacts {
pub condition_shape: ConditionShape,
pub step_shape: StepShape,
pub scan_with_init: Option<ScanWithInitFacts>,
pub split_scan: Option<SplitScanFacts>,
}
#[derive(Debug, Clone)]
@ -29,6 +30,15 @@ pub(in crate::mir::builder) struct ScanWithInitFacts {
pub step_lit: i64,
}
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct SplitScanFacts {
pub s_var: String,
pub sep_var: String,
pub result_var: String,
pub i_var: String,
pub start_var: String,
}
pub(in crate::mir::builder) fn try_build_loop_facts(
condition: &ASTNode,
body: &[ASTNode],
@ -36,15 +46,13 @@ pub(in crate::mir::builder) fn try_build_loop_facts(
// Phase 29ai P4/P7: keep Facts conservative; only return Some when we can
// build a concrete pattern fact set (no guesses / no hardcoded names).
let Some(condition_shape) = try_extract_condition_shape(condition)? else {
return Ok(None);
};
let Some(step_shape) = try_extract_step_shape(body)? else {
return Ok(None);
};
let condition_shape =
try_extract_condition_shape(condition)?.unwrap_or(ConditionShape::Unknown);
let step_shape = try_extract_step_shape(body)?.unwrap_or(StepShape::Unknown);
let scan_with_init = try_extract_scan_with_init_facts(body, &condition_shape, &step_shape)?;
let split_scan = try_extract_split_scan_facts(condition, body)?;
if scan_with_init.is_none() {
if scan_with_init.is_none() && split_scan.is_none() {
return Ok(None);
}
@ -52,6 +60,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts(
condition_shape,
step_shape,
scan_with_init,
split_scan,
}))
}
@ -265,6 +274,308 @@ fn try_extract_scan_with_init_facts(
Ok(None)
}
fn try_extract_split_scan_facts(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<SplitScanFacts>, Freeze> {
let Some((i_var, s_var, sep_var)) = try_extract_split_scan_condition_vars(condition) else {
return Ok(None);
};
let Some(if_stmt) = body.iter().find_map(|stmt| match stmt {
ASTNode::If {
condition,
then_body,
else_body,
..
} => Some((condition.as_ref(), then_body, else_body.as_ref())),
_ => None,
}) else {
return Ok(None);
};
let (match_condition, then_body, else_body) = if_stmt;
let Some(else_body) = else_body else {
return Ok(None);
};
if !is_split_scan_match_condition(match_condition, &s_var, &i_var, &sep_var) {
return Ok(None);
}
let Some((result_var, start_var)) =
then_body
.iter()
.find_map(|stmt| extract_split_scan_then_push(stmt, &s_var, &i_var))
else {
return Ok(None);
};
let has_start_update = then_body
.iter()
.any(|stmt| is_start_update(stmt, &start_var, &i_var, &sep_var));
if !has_start_update {
return Ok(None);
}
let has_i_set_to_start = then_body
.iter()
.any(|stmt| is_i_set_to_start(stmt, &i_var, &start_var));
if !has_i_set_to_start {
return Ok(None);
}
let has_else_increment = else_body
.iter()
.any(|stmt| is_i_increment_by_one(stmt, &i_var));
if !has_else_increment {
return Ok(None);
}
Ok(Some(SplitScanFacts {
s_var,
sep_var,
result_var,
i_var,
start_var,
}))
}
fn try_extract_split_scan_condition_vars(condition: &ASTNode) -> Option<(String, String, String)> {
let ASTNode::BinaryOp {
operator: BinaryOperator::LessEqual,
left,
right,
..
} = condition
else {
return None;
};
let ASTNode::Variable { name: i_var, .. } = left.as_ref() else {
return None;
};
let ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: minus_left,
right: minus_right,
..
} = right.as_ref()
else {
return None;
};
let s_var = match minus_left.as_ref() {
ASTNode::MethodCall {
object,
method,
arguments,
..
} if method == "length" && arguments.is_empty() => match object.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return None,
},
_ => return None,
};
let sep_var = match minus_right.as_ref() {
ASTNode::MethodCall {
object,
method,
arguments,
..
} if method == "length" && arguments.is_empty() => match object.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return None,
},
_ => return None,
};
Some((i_var.clone(), s_var, sep_var))
}
fn is_split_scan_match_condition(
condition: &ASTNode,
s_var: &str,
i_var: &str,
sep_var: &str,
) -> bool {
let ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left,
right,
..
} = condition
else {
return false;
};
let matches_left = is_substring_i_plus_sep_len(left.as_ref(), s_var, i_var, sep_var)
&& matches!(
right.as_ref(),
ASTNode::Variable { name, .. } if name == sep_var
);
let matches_right = is_substring_i_plus_sep_len(right.as_ref(), s_var, i_var, sep_var)
&& matches!(
left.as_ref(),
ASTNode::Variable { name, .. } if name == sep_var
);
matches_left || matches_right
}
fn is_substring_i_plus_sep_len(
expr: &ASTNode,
s_var: &str,
i_var: &str,
sep_var: &str,
) -> bool {
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr
else {
return false;
};
if method != "substring" || arguments.len() != 2 {
return false;
}
let ASTNode::Variable { name: obj, .. } = object.as_ref() else {
return false;
};
if obj != s_var {
return false;
}
match &arguments[0] {
ASTNode::Variable { name, .. } if name == i_var => {}
_ => return false,
}
let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = &arguments[1]
else {
return false;
};
match left.as_ref() {
ASTNode::Variable { name, .. } if name == i_var => {}
_ => return false,
}
matches!(
right.as_ref(),
ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var)
)
}
fn extract_split_scan_then_push(
stmt: &ASTNode,
s_var: &str,
i_var: &str,
) -> Option<(String, String)> {
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = stmt
else {
return None;
};
if method != "push" || arguments.len() != 1 {
return None;
}
let ASTNode::Variable { name: result_var, .. } = object.as_ref() else {
return None;
};
let ASTNode::MethodCall {
object: substr_object,
method: substr_method,
arguments: substr_args,
..
} = &arguments[0]
else {
return None;
};
if substr_method != "substring" || substr_args.len() != 2 {
return None;
}
let ASTNode::Variable { name: substr_obj, .. } = substr_object.as_ref() else {
return None;
};
if substr_obj != s_var {
return None;
}
let ASTNode::Variable { name: start_var, .. } = &substr_args[0] else {
return None;
};
match &substr_args[1] {
ASTNode::Variable { name, .. } if name == i_var => {}
_ => return None,
}
Some((result_var.clone(), start_var.clone()))
}
fn is_start_update(stmt: &ASTNode, start_var: &str, i_var: &str, sep_var: &str) -> bool {
let ASTNode::Assignment { target, value, .. } = stmt else {
return false;
};
match target.as_ref() {
ASTNode::Variable { name, .. } if name == start_var => {}
_ => return false,
}
let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = value.as_ref()
else {
return false;
};
match left.as_ref() {
ASTNode::Variable { name, .. } if name == i_var => {}
_ => return false,
}
matches!(
right.as_ref(),
ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var)
)
}
fn is_i_set_to_start(stmt: &ASTNode, i_var: &str, start_var: &str) -> bool {
let ASTNode::Assignment { target, value, .. } = stmt else {
return false;
};
matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var)
&& matches!(value.as_ref(), ASTNode::Variable { name, .. } if name == start_var)
}
fn is_i_increment_by_one(stmt: &ASTNode, i_var: &str) -> bool {
let ASTNode::Assignment { target, value, .. } = stmt else {
return false;
};
let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var);
let value_ok = matches!(
value.as_ref(),
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var)
&& matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(1), .. })
);
target_ok && value_ok
}
#[cfg(test)]
mod tests {
use super::*;
@ -375,4 +686,180 @@ mod tests {
let facts = try_build_loop_facts(&condition, &[step]).expect("Ok");
assert!(facts.is_none());
}
#[test]
fn loopfacts_ok_some_for_canonical_split_scan_minimal() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::LessEqual,
left: Box::new(v("i")),
right: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: Box::new(ASTNode::MethodCall {
object: Box::new(v("s")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
right: Box::new(ASTNode::MethodCall {
object: Box::new(v("separator")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let if_stmt = ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::MethodCall {
object: Box::new(v("s")),
method: "substring".to_string(),
arguments: vec![
v("i"),
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(v("i")),
right: Box::new(ASTNode::MethodCall {
object: Box::new(v("separator")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
},
],
span: Span::unknown(),
}),
right: Box::new(v("separator")),
span: Span::unknown(),
}),
then_body: vec![
ASTNode::MethodCall {
object: Box::new(v("result")),
method: "push".to_string(),
arguments: vec![ASTNode::MethodCall {
object: Box::new(v("s")),
method: "substring".to_string(),
arguments: vec![v("start"), v("i")],
span: Span::unknown(),
}],
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(v("start")),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(v("i")),
right: Box::new(ASTNode::MethodCall {
object: Box::new(v("separator")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(v("i")),
value: Box::new(v("start")),
span: Span::unknown(),
},
],
else_body: Some(vec![ASTNode::Assignment {
target: Box::new(v("i")),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(v("i")),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}]),
span: Span::unknown(),
};
let facts = try_build_loop_facts(&condition, &[if_stmt]).expect("Ok");
let split_scan = facts.and_then(|facts| facts.split_scan);
let split_scan = split_scan.expect("SplitScan facts");
assert_eq!(split_scan.s_var, "s");
assert_eq!(split_scan.sep_var, "separator");
assert_eq!(split_scan.result_var, "result");
assert_eq!(split_scan.i_var, "i");
assert_eq!(split_scan.start_var, "start");
}
#[test]
fn loopfacts_ok_none_when_split_scan_missing_else() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::LessEqual,
left: Box::new(v("i")),
right: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: Box::new(ASTNode::MethodCall {
object: Box::new(v("s")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
right: Box::new(ASTNode::MethodCall {
object: Box::new(v("separator")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let if_stmt = ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::MethodCall {
object: Box::new(v("s")),
method: "substring".to_string(),
arguments: vec![
v("i"),
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(v("i")),
right: Box::new(ASTNode::MethodCall {
object: Box::new(v("separator")),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
},
],
span: Span::unknown(),
}),
right: Box::new(v("separator")),
span: Span::unknown(),
}),
then_body: vec![ASTNode::MethodCall {
object: Box::new(v("result")),
method: "push".to_string(),
arguments: vec![ASTNode::MethodCall {
object: Box::new(v("s")),
method: "substring".to_string(),
arguments: vec![v("start"), v("i")],
span: Span::unknown(),
}],
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
};
let facts = try_build_loop_facts(&condition, &[if_stmt]).expect("Ok");
assert!(facts.is_none());
}
}

View File

@ -9,7 +9,9 @@ use crate::mir::builder::control_flow::plan::normalize::{canonicalize_loop_facts
use super::candidates::{CandidateSet, PlanCandidate};
use super::Freeze;
use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection, ScanWithInitPlan};
use crate::mir::builder::control_flow::plan::{
DomainPlan, ScanDirection, ScanWithInitPlan, SplitScanPlan,
};
/// Phase 29ai P0: External-ish SSOT entrypoint (skeleton)
///
@ -55,5 +57,95 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
});
}
if let Some(split_scan) = &facts.facts.split_scan {
candidates.push(PlanCandidate {
plan: DomainPlan::SplitScan(SplitScanPlan {
s_var: split_scan.s_var.clone(),
sep_var: split_scan.sep_var.clone(),
result_var: split_scan.result_var.clone(),
i_var: split_scan.i_var.clone(),
start_var: split_scan.start_var.clone(),
}),
rule: "loop/split_scan",
});
}
candidates.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::builder::control_flow::plan::facts::scan_shapes::{
ConditionShape, StepShape,
};
use crate::mir::builder::control_flow::plan::facts::loop_facts::{
LoopFacts, ScanWithInitFacts, SplitScanFacts,
};
use crate::mir::builder::control_flow::plan::normalize::canonicalize_loop_facts;
#[test]
fn planner_builds_split_scan_plan_from_facts() {
let facts = LoopFacts {
condition_shape: ConditionShape::Unknown,
step_shape: StepShape::Unknown,
scan_with_init: None,
split_scan: Some(SplitScanFacts {
s_var: "s".to_string(),
sep_var: "separator".to_string(),
result_var: "result".to_string(),
i_var: "i".to_string(),
start_var: "start".to_string(),
}),
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");
match plan {
Some(DomainPlan::SplitScan(plan)) => {
assert_eq!(plan.s_var, "s");
assert_eq!(plan.sep_var, "separator");
assert_eq!(plan.result_var, "result");
assert_eq!(plan.i_var, "i");
assert_eq!(plan.start_var, "start");
}
other => panic!("expected split scan plan, got {:?}", other),
}
}
#[test]
fn planner_prefers_none_when_no_candidates() {
let facts = LoopFacts {
condition_shape: ConditionShape::Unknown,
step_shape: StepShape::Unknown,
scan_with_init: None,
split_scan: None,
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");
assert!(plan.is_none());
}
#[test]
fn planner_retains_scan_with_init_plan_path() {
let facts = LoopFacts {
condition_shape: ConditionShape::Unknown,
step_shape: StepShape::Unknown,
scan_with_init: Some(ScanWithInitFacts {
loop_var: "i".to_string(),
haystack: "s".to_string(),
needle: "ch".to_string(),
step_lit: 1,
}),
split_scan: None,
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");
match plan {
Some(DomainPlan::ScanWithInit(plan)) => {
assert_eq!(plan.loop_var, "i");
}
other => panic!("expected scan_with_init plan, got {:?}", other),
}
}
}

View File

@ -73,7 +73,14 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result<Option<D
None => (legacy_rules::pattern6::extract(ctx)?, true),
}
}
RuleKind::Pattern7 => (legacy_rules::pattern7::extract(ctx)?, true),
RuleKind::Pattern7 => {
let from_planner = planner::build_plan(ctx.condition, ctx.body)
.map_err(|freeze| freeze.to_string())?;
match from_planner {
Some(plan) => (Some(plan), false),
None => (legacy_rules::pattern7::extract(ctx)?, true),
}
}
};
if let Some(domain_plan) = plan_opt {