phase29ai(p7): planner returns DomainPlan (no hardcoded names)
This commit is contained in:
@ -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 を入口で実行 |
|
||||
|
||||
|
||||
@ -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<ScanWithInitFacts>,
|
||||
}
|
||||
|
||||
#[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<Option<LoopFacts>, 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<Option<ConditionSh
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let ASTNode::Variable { name: left, .. } = left.as_ref() else {
|
||||
let ASTNode::Variable { name: idx_var, .. } = left.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let ASTNode::Variable { name: right, .. } = right.as_ref() else {
|
||||
let ASTNode::MethodCall {
|
||||
object,
|
||||
method,
|
||||
arguments,
|
||||
..
|
||||
} = right.as_ref()
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !arguments.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let method = match method.as_str() {
|
||||
"length" => 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<Option<StepShape>, Freeze>
|
||||
}))
|
||||
}
|
||||
|
||||
fn try_extract_scan_with_init_facts(
|
||||
body: &[ASTNode],
|
||||
condition_shape: &ConditionShape,
|
||||
step_shape: &StepShape,
|
||||
) -> Result<Option<ScanWithInitFacts>, 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 {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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<Option<Plan>, Freeze> {
|
||||
) -> Result<Option<DomainPlan>, 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<Option<Plan>, Freeze> {
|
||||
) -> Result<Option<DomainPlan>, 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<bool, Freeze> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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<Option<Plan>, Freeze> {
|
||||
pub(in crate::mir::builder) fn finalize(self) -> Result<Option<DomainPlan>, 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
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user