From 9abc726394da8908dde9a148111e96a7805f2c0c Mon Sep 17 00:00:00 2001 From: tomoaki Date: Mon, 29 Dec 2025 09:17:53 +0900 Subject: [PATCH] phase29ai(p9): planner-first pattern7 split-scan subset --- docs/development/current/main/10-Now.md | 7 +- docs/development/current/main/30-Backlog.md | 2 +- ...N2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md | 106 ++++ .../current/main/phases/phase-29ai/README.md | 7 + .../control_flow/plan/facts/loop_facts.rs | 501 +++++++++++++++++- .../control_flow/plan/planner/build.rs | 94 +++- .../control_flow/plan/single_planner/rules.rs | 9 +- 7 files changed, 715 insertions(+), 11 deletions(-) create mode 100644 docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 64bdd513..07bc71b0 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,7 +2,12 @@ ## Current Focus: Phase 29ai(Plan/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 歩だけ接続し、Pattern6(scan-with-init)の subset から吸収を開始(仕様不変) diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index b88ae616..e8de7f05 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -19,7 +19,7 @@ Related: - **Phase 29ai(candidate): Plan/Frag single-planner(Facts SSOT)** - 入口: `docs/development/current/main/phases/phase-29ai/README.md` - - Next: P9(Planner: Pattern7 split-scan subset) + - Next: P10(Move Pattern2 extractor → plan layer) - **Phase 29ae P1(✅ COMPLETE): JoinIR Regression Pack (SSOT固定)** - 入口: `docs/development/current/main/phases/phase-29ae/README.md` diff --git a/docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md new file mode 100644 index 00000000..50e05331 --- /dev/null +++ b/docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md @@ -0,0 +1,106 @@ +# Phase 29ai P10: Move Pattern2 extractor to plan layer(SSOT) + +Date: 2025-12-29 +Status: Ready for execution +Scope: Pattern2(Break / LoopBodyLocal / promotion)抽出の SSOT を plan 層へ移設(仕様不変) +Goal: JoinIR 側の “pattern固有知識” を削減し、依存方向を `joinir → plan` の一方向に固定する + +## Objective + +Pattern2 の DomainPlan 抽出(`DomainPlan::Pattern2Break`)を `joinir/patterns/extractors/*` から `plan/extractors/*` に移し、 +JoinIR 側は薄い wrapper(re-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 extractor(JoinIR 側): + - `src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs` + - `-> Result, 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 ✨ NEW(P10) + +src/mir/builder/control_flow/joinir/patterns/extractors/ + ├── pattern2.rs (縮退: plan 側へ delegate / re-export) + └── mod.rs (必要なら export 更新) +``` + +## Implementation Steps(Critical 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(移設のみ・仕様不変)を明記。 +- Next(P11)候補を 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 増加なし + diff --git a/docs/development/current/main/phases/phase-29ai/README.md b/docs/development/current/main/phases/phase-29ai/README.md index 1973a260..1ff944dd 100644 --- a/docs/development/current/main/phases/phase-29ai/README.md +++ b/docs/development/current/main/phases/phase-29ai/README.md @@ -53,6 +53,13 @@ Goal: pattern 名による分岐を外部APIから消し、Facts(事実)→ - 指示書: `docs/development/current/main/phases/phase-29ai/P9-PLANNER-PATTERN7-SPLITSCAN-WIRE-INSTRUCTIONS.md` - ねらい: Pattern7(split-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 layer(SSOT) + +- 指示書: `docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md` +- ねらい: Pattern2 の抽出(pattern固有知識)を plan 側へ寄せて依存方向を一方向に固定(仕様不変) ## Verification (SSOT) 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 9268a645..2037196d 100644 --- a/src/mir/builder/control_flow/plan/facts/loop_facts.rs +++ b/src/mir/builder/control_flow/plan/facts/loop_facts.rs @@ -19,6 +19,7 @@ pub(in crate::mir::builder) struct LoopFacts { pub condition_shape: ConditionShape, pub step_shape: StepShape, pub scan_with_init: Option, + pub split_scan: Option, } #[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, 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()); + } } diff --git a/src/mir/builder/control_flow/plan/planner/build.rs b/src/mir/builder/control_flow/plan/planner/build.rs index 14800834..e23afb20 100644 --- a/src/mir/builder/control_flow/plan/planner/build.rs +++ b/src/mir/builder/control_flow/plan/planner/build.rs @@ -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), + } + } +} 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 d6b0e8cd..b2d74019 100644 --- a/src/mir/builder/control_flow/plan/single_planner/rules.rs +++ b/src/mir/builder/control_flow/plan/single_planner/rules.rs @@ -73,7 +73,14 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result (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 {