use crate::engine::Context;
use crate::limits::ResourceLimits;
use crate::parsing::parse;
use crate::planning::execution_plan::ExecutionPlan;
use crate::planning::normalize::{LeafKind, NormalForm, NormalFormId, NormalFormKind};
use crate::planning::plan;
use crate::planning::semantics::{MathematicalComputation, ValueKind};
use rust_decimal::Decimal;
use std::sync::Arc;
fn plan_from_code(code: &str) -> ExecutionPlan {
let specs: Vec<_> = parse(
code,
crate::parsing::source::SourceType::Volatile,
&ResourceLimits::default(),
)
.expect("parse")
.into_flattened_specs();
let mut ctx = Context::new();
let repository = ctx.workspace();
for spec in &specs {
ctx.insert_spec(Arc::clone(&repository), spec.clone())
.expect("insert spec");
}
let result = plan(&ctx, &ResourceLimits::default());
assert!(
result.errors.is_empty(),
"planning errors: {:?}",
result.errors
);
let spec_name = specs.last().expect("spec").name.clone();
let plans = result
.plans
.get_plans(repository.name.as_deref(), &spec_name)
.expect("spec result");
assert_eq!(plans.len(), 1, "expected one plan");
plans.values().next().expect("plan").clone()
}
fn rule_root<'a>(plan: &'a ExecutionPlan, rule_name: &str) -> &'a NormalForm {
let id = plan.get_rule(rule_name).expect("rule").normal_form;
plan.normal_form(id)
}
#[test]
fn shipped_normal_forms_are_exactly_reachable_closure() {
let code = r#"
spec t
data input: 5
rule step1: input + 1
rule step2: step1 * 2
rule step3: step2 - 3
"#;
let plan = plan_from_code(code);
let mut reachable: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut worklist: Vec<NormalFormId> = plan.rules.values().map(|r| r.normal_form).collect();
while let Some(id) = worklist.pop() {
if !reachable.insert(id.index() as u32) {
continue;
}
match &plan.normal_form(id).kind {
NormalFormKind::Leaf(_) | NormalFormKind::Veto(_) | NormalFormKind::Now => {}
NormalFormKind::Sum(children)
| NormalFormKind::Product(children)
| NormalFormKind::And(children) => {
worklist.extend(children.iter().copied());
}
NormalFormKind::Subtract(a, b)
| NormalFormKind::Divide(a, b)
| NormalFormKind::Power(a, b)
| NormalFormKind::Modulo(a, b)
| NormalFormKind::Comparison(a, _, b)
| NormalFormKind::RangeLiteral(a, b)
| NormalFormKind::RangeContainment(a, b) => {
worklist.push(*a);
worklist.push(*b);
}
NormalFormKind::Negate(x)
| NormalFormKind::Reciprocal(x)
| NormalFormKind::Not(x)
| NormalFormKind::MathOp(_, x)
| NormalFormKind::UnitConversion(x, _)
| NormalFormKind::DateRelative(_, x)
| NormalFormKind::DateCalendar(_, _, x)
| NormalFormKind::PastFutureRange(_, x)
| NormalFormKind::ResultIsVeto(x) => worklist.push(*x),
NormalFormKind::Piecewise(arms) => {
for (c, r) in arms {
worklist.push(*c);
worklist.push(*r);
}
}
}
if let Some(origin) = plan.normal_form(id).origin {
worklist.push(origin);
}
}
assert_eq!(
reachable.len(),
plan.normal_forms.len(),
"shipped table must equal reachable closure from rule roots"
);
for id in 0..plan.normal_forms.len() {
assert!(
reachable.contains(&(id as u32)),
"shipped cell {id} not reachable from any rule root"
);
}
}
#[test]
fn unless_chain_embeds_dependency_as_piecewise() {
let code = r#"
spec t
data flag: boolean
data base: 10
rule doubled: base * 2
rule scaled: doubled
unless flag then doubled * 3
"#;
let plan = plan_from_code(code);
let scaled = rule_root(&plan, "scaled");
assert!(
matches!(scaled.kind, NormalFormKind::Piecewise(_)),
"unless rule must normalize to a Piecewise, got {:?}",
scaled.kind
);
}
#[test]
fn doubling_chain_graph_stays_linear() {
let mut code = String::from("spec doubling\ndata input: number\nrule r0: input > 0\n");
let n = 40;
for index in 1..=n {
code.push_str(&format!(
"rule r{index}: r{previous} and not r{previous}\n",
previous = index - 1
));
}
let plan = plan_from_code(&code);
let cells = plan.normal_forms.len();
assert!(
cells <= 5 * n,
"expected linear shared graph (<=5n), got {cells} for n={n}"
);
}
#[test]
fn sqrt_product_folds_to_literal_two_in_plan() {
let code = r#"
spec test
rule sqrt_two: sqrt 2
rule sqrt_product: sqrt_two * sqrt_two
"#;
let plan = plan_from_code(code);
let body = rule_root(&plan, "sqrt_product");
match &body.kind {
NormalFormKind::Leaf(LeafKind::Literal(literal)) => match &literal.value {
ValueKind::Number(rational) => {
let decimal = ValueKind::Number(rational.clone())
.as_decimal_magnitude()
.expect("numeric literal");
assert_eq!(decimal, Decimal::from(2));
}
other => panic!("expected numeric literal, got {other:?}"),
},
other => panic!("sqrt_product must fold to a literal leaf, got {other:?}"),
}
}
#[test]
fn log_exp_chain_folds_to_literal_one_in_plan() {
let code = r#"
spec test
rule exp_one: exp 1
rule log_one: log exp_one
"#;
let plan = plan_from_code(code);
let body = rule_root(&plan, "log_one");
match &body.kind {
NormalFormKind::Leaf(LeafKind::Literal(literal)) => match &literal.value {
ValueKind::Number(rational) => {
let decimal = ValueKind::Number(rational.clone())
.as_decimal_magnitude()
.expect("numeric literal");
assert_eq!(decimal, Decimal::ONE);
}
other => panic!("expected numeric literal, got {other:?}"),
},
other => panic!("log_one must fold to a literal leaf, got {other:?}"),
}
}
#[test]
fn bare_use_site_exposes_exp_kind_with_rule_embed() {
let code = r#"
spec test
rule exp_one: exp 1
rule just: exp_one
"#;
let plan = plan_from_code(code);
let just = rule_root(&plan, "just");
assert!(
matches!(
just.kind,
NormalFormKind::MathOp(MathematicalComputation::Exp, _)
),
"bare use-site must share body Exp Kind, got {:?}",
just.kind
);
let embed = just
.rule_embed
.as_ref()
.expect("bare use-site must keep rule_embed");
assert_eq!(embed.rule, "exp_one");
}
#[test]
fn identity_elim_keeps_rule_embed_on_survivor() {
let code = r#"
spec test
rule exp_one: exp 1
rule r: exp_one + 0
"#;
let plan = plan_from_code(code);
let r = rule_root(&plan, "r");
assert!(
matches!(
r.kind,
NormalFormKind::MathOp(MathematicalComputation::Exp, _)
),
"identity-elim survivor must keep Exp Kind, got {:?}",
r.kind
);
let embed = r
.rule_embed
.as_ref()
.expect("identity-elim must copy rule_embed onto survivor");
assert_eq!(embed.rule, "exp_one");
}