diff --git a/docs/development/current/main/design/planfrag-ssot-registry.md b/docs/development/current/main/design/planfrag-ssot-registry.md index 3d3e7dd9..0525bd42 100644 --- a/docs/development/current/main/design/planfrag-ssot-registry.md +++ b/docs/development/current/main/design/planfrag-ssot-registry.md @@ -12,7 +12,7 @@ Scope: JoinIR plan/frag 導線(仕様不変) | Facts | CFG/Terminator/境界情報から抽出した “観測” と “導出” を分離した Facts | planner が CFG を再走査する前提の不足した Facts を作る / emit が CFG を覗いて “穴埋め” | Facts 収集時: 契約違反は `Freeze(contract)`(strict/dev は即Fail) | | Normalize | Facts の表現ゆれ除去(純変換) | 追加の解析(CFG/AST を見に行く) / 値の意味を変える変形 | normalize 後の不変条件を `verify_*` で検証(strict/dev) | | Planner | Canonical Facts → Plan(候補集合→一意化) | pattern 名で入口分岐を公開APIに漏らす / emit の都合で再解析 | 0候補=Ok(None), 1候補=Ok(Some), 2+=Freeze(ambiguous) | -| Plan | emit に必要な骨格(entry/exit/join/region参照) | CFG 再解析が必要な “情報欠落” Plan | emit 前に Plan の構造不変条件を検証(strict/dev) | +| Plan | DomainPlan(pattern固有のSSOT語彙) | CFG 再解析が必要な “情報欠落” Plan / 二重Plan語彙 | emit 前に Plan の構造不変条件を検証(strict/dev) | | Emit | Plan → Frag(生成のみ) | Facts/CFG に戻って再推論 / silent fallback | emit は入力不足を Freeze(bug/contract) で落とす(strict/dev) | | Frag | 生成結果(EdgeCFG/JoinIR lowering の出力) | Frag が “真実” として再利用されること(派生物) | 既存の frag verifier / contract_checks を入口で実行 | 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 1cc7f43e..9268a645 100644 --- a/src/mir/builder/control_flow/plan/facts/loop_facts.rs +++ b/src/mir/builder/control_flow/plan/facts/loop_facts.rs @@ -12,38 +12,46 @@ use crate::ast::ASTNode; use super::scan_shapes::{ConditionShape, StepShape}; use crate::mir::builder::control_flow::plan::planner::Freeze; use crate::ast::{BinaryOperator, LiteralValue}; +use super::scan_shapes::LengthMethod; #[derive(Debug, Clone)] pub(in crate::mir::builder) struct LoopFacts { pub condition_shape: ConditionShape, pub step_shape: StepShape, + pub scan_with_init: Option, +} + +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct ScanWithInitFacts { + pub loop_var: String, + pub haystack: String, + pub needle: String, + pub step_lit: i64, } pub(in crate::mir::builder) fn try_build_loop_facts( condition: &ASTNode, body: &[ASTNode], ) -> Result, Freeze> { + // 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 scan_with_init = try_extract_scan_with_init_facts(body, &condition_shape, &step_shape)?; - // Phase 29ai P4: minimal canonical guard (avoid false-positive facts). - if let ( - ConditionShape::VarLessVar { left, .. }, - StepShape::AssignAddConst { var, k: 1 }, - ) = (&condition_shape, &step_shape) - { - if var != left { - return Ok(None); - } + if scan_with_init.is_none() { + return Ok(None); } Ok(Some(LoopFacts { condition_shape, step_shape, + scan_with_init, })) } @@ -58,17 +66,39 @@ fn try_extract_condition_shape(condition: &ASTNode) -> Result LengthMethod::Length, + "size" => LengthMethod::Size, + _ => return Ok(None), + }; + let ASTNode::Variable { + name: haystack_var, + .. + } = object.as_ref() + else { return Ok(None); }; - Ok(Some(ConditionShape::VarLessVar { - left: left.clone(), - right: right.clone(), + Ok(Some(ConditionShape::VarLessLength { + idx_var: idx_var.clone(), + haystack_var: haystack_var.clone(), + method, })) } @@ -117,6 +147,124 @@ fn try_extract_step_shape(body: &[ASTNode]) -> Result, Freeze> })) } +fn try_extract_scan_with_init_facts( + body: &[ASTNode], + condition_shape: &ConditionShape, + step_shape: &StepShape, +) -> Result, Freeze> { + let (ConditionShape::VarLessLength { + idx_var, + haystack_var, + .. + }, StepShape::AssignAddConst { var: step_var, k: 1 }) = (condition_shape, step_shape) + else { + return Ok(None); + }; + + if step_var != idx_var { + return Ok(None); + } + + // Find `if s.substring(i, i + 1) == ch { return i }` anywhere except the last step. + for stmt in body.iter().take(body.len().saturating_sub(1)) { + let ASTNode::If { + condition, + then_body, + else_body, + .. + } = stmt + else { + continue; + }; + if else_body.is_some() { + continue; + } + + let ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } = condition.as_ref() + else { + continue; + }; + + let ASTNode::MethodCall { + object, + method, + arguments, + .. + } = left.as_ref() + else { + continue; + }; + if method != "substring" || arguments.len() != 2 { + continue; + } + + let ASTNode::Variable { name: obj, .. } = object.as_ref() else { + continue; + }; + if obj != haystack_var { + continue; + } + + // substring(i, i + 1) + let (start, end) = (&arguments[0], &arguments[1]); + match start { + ASTNode::Variable { name, .. } if name == idx_var => {} + _ => continue, + } + let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: end_left, + right: end_right, + .. + } = end + else { + continue; + }; + match end_left.as_ref() { + ASTNode::Variable { name, .. } if name == idx_var => {} + _ => continue, + } + match end_right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(1), + .. + } => {} + _ => continue, + } + + let ASTNode::Variable { name: needle, .. } = right.as_ref() else { + continue; + }; + + // then-body must contain `return i` (minimal) + if !then_body.iter().any(|n| { + matches!( + n, + ASTNode::Return { + value: Some(v), + .. + } if matches!(v.as_ref(), ASTNode::Variable { name, .. } if name == idx_var) + ) + }) { + continue; + } + + return Ok(Some(ScanWithInitFacts { + loop_var: idx_var.clone(), + haystack: haystack_var.clone(), + needle: needle.clone(), + step_lit: 1, + })); + } + + Ok(None) +} + #[cfg(test)] mod tests { use super::*; @@ -134,7 +282,42 @@ mod tests { let condition = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(v("i")), - right: Box::new(v("n")), + right: Box::new(ASTNode::MethodCall { + object: Box::new(v("s")), + method: "length".to_string(), + arguments: vec![], + 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::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }), + right: Box::new(v("ch")), + span: Span::unknown(), + }), + then_body: vec![ASTNode::Return { + value: Some(Box::new(v("i"))), + span: Span::unknown(), + }], + else_body: None, span: Span::unknown(), }; let step = ASTNode::Assignment { @@ -151,7 +334,7 @@ mod tests { span: Span::unknown(), }; - let facts = try_build_loop_facts(&condition, &[step]).expect("Ok"); + let facts = try_build_loop_facts(&condition, &[if_stmt, step]).expect("Ok"); assert!(facts.is_some()); } @@ -167,7 +350,12 @@ mod tests { let condition = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(v("i")), - right: Box::new(v("n")), + right: Box::new(ASTNode::MethodCall { + object: Box::new(v("s")), + method: "length".to_string(), + arguments: vec![], + span: Span::unknown(), + }), span: Span::unknown(), }; let step = ASTNode::Assignment { diff --git a/src/mir/builder/control_flow/plan/facts/mod.rs b/src/mir/builder/control_flow/plan/facts/mod.rs index c22fa7d4..c20fb3df 100644 --- a/src/mir/builder/control_flow/plan/facts/mod.rs +++ b/src/mir/builder/control_flow/plan/facts/mod.rs @@ -10,4 +10,3 @@ pub(in crate::mir::builder) mod loop_facts; pub(in crate::mir::builder) mod scan_shapes; pub(in crate::mir::builder) use loop_facts::{try_build_loop_facts, LoopFacts}; -pub(in crate::mir::builder) use scan_shapes::{ConditionShape, StepShape}; diff --git a/src/mir/builder/control_flow/plan/facts/scan_shapes.rs b/src/mir/builder/control_flow/plan/facts/scan_shapes.rs index 4c9a0d50..2b11dcbe 100644 --- a/src/mir/builder/control_flow/plan/facts/scan_shapes.rs +++ b/src/mir/builder/control_flow/plan/facts/scan_shapes.rs @@ -2,6 +2,12 @@ #![allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::mir::builder) enum LengthMethod { + Length, + Size, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(in crate::mir::builder) enum StepShape { AssignAddConst { var: String, k: i64 }, @@ -10,6 +16,10 @@ pub(in crate::mir::builder) enum StepShape { #[derive(Debug, Clone, PartialEq, Eq)] pub(in crate::mir::builder) enum ConditionShape { - VarLessVar { left: String, right: String }, + VarLessLength { + idx_var: String, + haystack_var: String, + method: LengthMethod, + }, Unknown, } diff --git a/src/mir/builder/control_flow/plan/planner/build.rs b/src/mir/builder/control_flow/plan/planner/build.rs index c4574eb0..14800834 100644 --- a/src/mir/builder/control_flow/plan/planner/build.rs +++ b/src/mir/builder/control_flow/plan/planner/build.rs @@ -7,10 +7,9 @@ use crate::ast::ASTNode; use crate::mir::builder::control_flow::plan::facts::try_build_loop_facts; use crate::mir::builder::control_flow::plan::normalize::{canonicalize_loop_facts, CanonicalLoopFacts}; -use super::candidates::CandidateSet; -use super::candidates::PlanCandidate; -use super::{Freeze, Plan, PlanKind}; -use crate::mir::builder::control_flow::plan::facts::{ConditionShape, StepShape}; +use super::candidates::{CandidateSet, PlanCandidate}; +use super::Freeze; +use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection, ScanWithInitPlan}; /// Phase 29ai P0: External-ish SSOT entrypoint (skeleton) /// @@ -18,7 +17,7 @@ use crate::mir::builder::control_flow::plan::facts::{ConditionShape, StepShape}; pub(in crate::mir::builder) fn build_plan( condition: &ASTNode, body: &[ASTNode], -) -> Result, Freeze> { +) -> Result, Freeze> { let Some(facts) = try_build_loop_facts(condition, body)? else { return Ok(None); }; @@ -28,7 +27,7 @@ pub(in crate::mir::builder) fn build_plan( pub(in crate::mir::builder) fn build_plan_from_facts( facts: CanonicalLoopFacts, -) -> Result, Freeze> { +) -> Result, Freeze> { // Phase 29ai P3: CandidateSet-based boundary (SSOT) // // P3 note: Facts are currently `Ok(None)` (P0 skeleton), so this function is @@ -37,30 +36,24 @@ pub(in crate::mir::builder) fn build_plan_from_facts( let mut candidates = CandidateSet::new(); - if matches_canonical_scan_with_init(&facts)? { + if let Some(scan) = &facts.facts.scan_with_init { candidates.push(PlanCandidate { - kind: PlanKind::ScanWithInit, + plan: DomainPlan::ScanWithInit(ScanWithInitPlan { + loop_var: scan.loop_var.clone(), + haystack: scan.haystack.clone(), + needle: scan.needle.clone(), + step_lit: scan.step_lit, + early_return_expr: ASTNode::Variable { + name: scan.loop_var.clone(), + span: crate::ast::Span::unknown(), + }, + not_found_return_lit: -1, + scan_direction: ScanDirection::Forward, + dynamic_needle: false, + }), rule: "loop/scan_with_init", }); } candidates.finalize() } - -fn matches_canonical_scan_with_init(facts: &CanonicalLoopFacts) -> Result { - let (ConditionShape::VarLessVar { left, right: _ }, StepShape::AssignAddConst { var, k }) = ( - &facts.facts.condition_shape, - &facts.facts.step_shape, - ) else { - return Ok(false); - }; - - if var != left { - return Ok(false); - } - if *k != 1 { - return Ok(false); - } - - Ok(true) -} diff --git a/src/mir/builder/control_flow/plan/planner/candidates.rs b/src/mir/builder/control_flow/plan/planner/candidates.rs index 3f45446d..d2200b7e 100644 --- a/src/mir/builder/control_flow/plan/planner/candidates.rs +++ b/src/mir/builder/control_flow/plan/planner/candidates.rs @@ -4,11 +4,12 @@ #![allow(dead_code)] -use super::{Freeze, Plan, PlanKind}; +use super::Freeze; +use crate::mir::builder::control_flow::plan::DomainPlan; #[derive(Debug, Clone)] pub(in crate::mir::builder) struct PlanCandidate { - pub kind: PlanKind, + pub plan: DomainPlan, pub rule: &'static str, } @@ -28,12 +29,12 @@ impl CandidateSet { self.candidates.push(candidate); } - pub(in crate::mir::builder) fn finalize(self) -> Result, Freeze> { + pub(in crate::mir::builder) fn finalize(self) -> Result, Freeze> { match self.candidates.len() { 0 => Ok(None), 1 => { let c = self.candidates.into_iter().next().expect("len == 1"); - Ok(Some(Plan { kind: c.kind })) + Ok(Some(c.plan)) } n => { let rules = self diff --git a/src/mir/builder/control_flow/plan/planner/mod.rs b/src/mir/builder/control_flow/plan/planner/mod.rs index a96b81c3..05c9e645 100644 --- a/src/mir/builder/control_flow/plan/planner/mod.rs +++ b/src/mir/builder/control_flow/plan/planner/mod.rs @@ -1,4 +1,4 @@ -//! Phase 29ai P0: Single Planner skeleton (Facts → Plan) +//! Phase 29ai P7: Single Planner (Facts → DomainPlan) //! //! P0 goal: expose a single external-ish entrypoint (`build_plan`) and hide //! pattern-name branching behind internal enums. @@ -9,17 +9,5 @@ pub(in crate::mir::builder) mod build; pub(in crate::mir::builder) mod candidates; pub(in crate::mir::builder) mod freeze; -pub(in crate::mir::builder) use freeze::Freeze; - -#[derive(Debug, Clone)] -pub(in crate::mir::builder) struct Plan { - pub kind: PlanKind, -} - -#[derive(Debug, Clone)] -pub(in crate::mir::builder) enum PlanKind { - Placeholder, - ScanWithInit, -} - pub(in crate::mir::builder) use build::build_plan; +pub(in crate::mir::builder) use freeze::Freeze;