phase29ai(p4): first LoopFacts + scan_with_init candidate

This commit is contained in:
2025-12-29 07:40:57 +09:00
parent 4fa425fac2
commit a17a7d7852
5 changed files with 203 additions and 14 deletions

View File

@ -11,6 +11,7 @@ use crate::ast::ASTNode;
use super::scan_shapes::{ConditionShape, StepShape};
use crate::mir::builder::control_flow::plan::planner::Freeze;
use crate::ast::{BinaryOperator, LiteralValue};
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct LoopFacts {
@ -19,8 +20,171 @@ pub(in crate::mir::builder) struct LoopFacts {
}
pub(in crate::mir::builder) fn try_build_loop_facts(
_condition: &ASTNode,
_body: &[ASTNode],
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<LoopFacts>, Freeze> {
Ok(None)
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);
};
// 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);
}
}
Ok(Some(LoopFacts {
condition_shape,
step_shape,
}))
}
fn try_extract_condition_shape(condition: &ASTNode) -> Result<Option<ConditionShape>, Freeze> {
let ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left,
right,
..
} = condition
else {
return Ok(None);
};
let ASTNode::Variable { name: left, .. } = left.as_ref() else {
return Ok(None);
};
let ASTNode::Variable { name: right, .. } = right.as_ref() else {
return Ok(None);
};
Ok(Some(ConditionShape::VarLessVar {
left: left.clone(),
right: right.clone(),
}))
}
fn try_extract_step_shape(body: &[ASTNode]) -> Result<Option<StepShape>, Freeze> {
let Some(last) = body.last() else {
return Ok(None);
};
let ASTNode::Assignment { target, value, .. } = last else {
return Ok(None);
};
let ASTNode::Variable { name: var, .. } = target.as_ref() else {
return Ok(None);
};
let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = value.as_ref()
else {
return Ok(None);
};
let ASTNode::Variable { name: lhs, .. } = left.as_ref() else {
return Ok(None);
};
if lhs != var {
return Ok(None);
}
let ASTNode::Literal { value, .. } = right.as_ref() else {
return Ok(None);
};
let LiteralValue::Integer(k) = value else {
return Ok(None);
};
if *k != 1 {
return Ok(None);
}
Ok(Some(StepShape::AssignAddConst {
var: var.clone(),
k: *k,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
fn v(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: Span::unknown(),
}
}
#[test]
fn loopfacts_ok_some_for_canonical_scan_with_init_minimal() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(v("i")),
right: Box::new(v("n")),
span: Span::unknown(),
};
let step = 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(),
};
let facts = try_build_loop_facts(&condition, &[step]).expect("Ok");
assert!(facts.is_some());
}
#[test]
fn loopfacts_ok_none_when_condition_not_supported() {
let condition = v("i"); // not `i < n`
let facts = try_build_loop_facts(&condition, &[]).expect("Ok");
assert!(facts.is_none());
}
#[test]
fn loopfacts_ok_none_when_step_var_differs_from_condition_var() {
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(v("i")),
right: Box::new(v("n")),
span: Span::unknown(),
};
let step = ASTNode::Assignment {
target: Box::new(v("j")),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(v("j")),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let facts = try_build_loop_facts(&condition, &[step]).expect("Ok");
assert!(facts.is_none());
}
}

View File

@ -10,3 +10,4 @@ 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,12 +2,14 @@
#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::mir::builder) enum StepShape {
AssignAddConst { var: String, k: i64 },
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::mir::builder) enum ConditionShape {
VarLessVar { left: String, right: String },
Unknown,
}

View File

@ -8,7 +8,9 @@ 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::{Freeze, Plan};
use super::candidates::PlanCandidate;
use super::{Freeze, Plan, PlanKind};
use crate::mir::builder::control_flow::plan::facts::{ConditionShape, StepShape};
/// Phase 29ai P0: External-ish SSOT entrypoint (skeleton)
///
@ -25,7 +27,7 @@ pub(in crate::mir::builder) fn build_plan(
}
pub(in crate::mir::builder) fn build_plan_from_facts(
_facts: CanonicalLoopFacts,
facts: CanonicalLoopFacts,
) -> Result<Option<Plan>, Freeze> {
// Phase 29ai P3: CandidateSet-based boundary (SSOT)
//
@ -33,13 +35,32 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
// unreachable in normal execution today. We still implement the SSOT
// boundary here so that future Facts work cannot drift.
let candidates = CandidateSet::new();
let mut candidates = CandidateSet::new();
// No candidates in P3 (unreachable today); boundary remains stable.
match candidates.finalize()? {
Some(plan) => Ok(Some(plan)),
None => Err(Freeze::unsupported(
"facts were provided but no planner rule produced a candidate (P3 placeholder)",
)),
if matches_canonical_scan_with_init(&facts)? {
candidates.push(PlanCandidate {
kind: PlanKind::ScanWithInit,
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

@ -19,6 +19,7 @@ pub(in crate::mir::builder) struct Plan {
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum PlanKind {
Placeholder,
ScanWithInit,
}
pub(in crate::mir::builder) use build::build_plan;