phase29ai(p7): planner returns DomainPlan (no hardcoded names)

This commit is contained in:
2025-12-29 08:39:33 +09:00
parent f9817dda4c
commit c7697edde1
7 changed files with 243 additions and 64 deletions

View File

@ -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 | DomainPlanpattern固有の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 を入口で実行 |

View File

@ -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 {

View File

@ -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};

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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

View File

@ -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;