use lemma::evaluation::OperationResult;
use lemma::parsing::ast::DateTimeValue;
use lemma::Engine;
use std::collections::HashMap;
fn rule_value(result: &lemma::evaluation::Response, rule_name: &str) -> String {
let rr = result
.results
.get(rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name));
match &rr.result {
OperationResult::Value(v) => v.to_string(),
OperationResult::Veto(v) => format!("VETO({})", v),
}
}
fn load_err_joined(engine_res: Result<(), lemma::Errors>) -> String {
let err = engine_res.expect_err("expected load to fail");
err.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn local_reference_to_nested_spec_data_copies_value() {
let code = r#"
spec law
data other: number -> default 42
spec license
with l: law
data license2: l.other
rule check: license2 > 10
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.unwrap();
let now = DateTimeValue::now();
let result = engine
.run("license", Some(&now), HashMap::new(), false)
.expect("should run");
assert_eq!(rule_value(&result, "check"), "true");
}
#[test]
fn binding_reference_copies_cross_spec_target_value() {
let code = r#"
spec law
data other: number -> default 99
spec inner
with l: law
data slot: number
spec top
with lic: inner
with lw: law
data lic.slot: lw.other
rule answer: lic.slot
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.unwrap();
let now = DateTimeValue::now();
let result = engine
.run("top", Some(&now), HashMap::new(), false)
.expect("should run");
assert_eq!(rule_value(&result, "answer"), "99");
}
#[test]
fn user_value_overrides_reference() {
let code = r#"
spec law
data other: number -> default 42
spec license
with l: law
data license2: l.other
rule check: license2
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.unwrap();
let mut data = HashMap::new();
data.insert("license2".to_string(), "777".to_string());
let now = DateTimeValue::now();
let result = engine
.run("license", Some(&now), data, false)
.expect("should run");
assert_eq!(rule_value(&result, "check"), "777");
}
#[test]
fn reference_chain_resolves_in_dependency_order() {
let code = r#"
spec base
data other: number -> default 5
spec mid
with b: base
data m2: b.other
spec top
with mm: mid
data t2: mm.m2
rule result: t2
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.unwrap();
let now = DateTimeValue::now();
let result = engine
.run("top", Some(&now), HashMap::new(), false)
.expect("should run");
assert_eq!(rule_value(&result, "result"), "5");
}
#[test]
fn closed_reference_cycle_is_rejected() {
let code = r#"
spec inner
data a: number
data b: number
spec outer
with i: inner
data i.a: i.b
data i.b: i.a
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("Circular data reference"),
"closed reference cycle must be reported as a circular data reference, got: {joined}"
);
}
#[test]
fn self_referential_reference_is_rejected() {
let code = r#"
spec inner
data x: number
spec outer
with i: inner
data i.x: i.x
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("Circular data reference"),
"self-referential reference must be reported as a circular data reference, got: {joined}"
);
}
#[test]
fn unknown_reference_target_is_rejected_with_exact_error() {
let code = r#"
spec test
data a: number -> default 1
data b: a.nonexistent
rule r: b
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("'a' is not a spec reference")
|| joined.contains("'nonexistent' not found")
|| joined.contains("target 'a.nonexistent' does not exist"),
"unknown reference target must identify the missing path, got: {joined}"
);
}
#[test]
fn reference_target_is_spec_reference_rejected() {
let code = r#"
spec inner
data x: number -> default 1
spec outer
with i: inner
data copy_of_i: i
rule r: copy_of_i
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("is a spec reference and cannot carry a value"),
"referencing a spec reference must be rejected with the exact error, got: {joined}"
);
}
#[test]
fn reference_target_is_ambiguous_data_and_rule() {
let code = r#"
spec inner
data conflict: number -> default 1
rule conflict: 2
spec outer
with i: inner
data c: i.conflict
rule r: c
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("is ambiguous"),
"duplicate data+rule name must be reported as ambiguous reference target, got: {joined}"
);
}
#[test]
fn binding_reference_target_type_incompatible_with_child_declared_type_is_rejected() {
let code = r#"
spec inner
data n: number
spec source_spec
data s: text -> default "hello"
spec outer
with i: inner
with src: source_spec
data i.n: src.s
rule r: i.n
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("type mismatch"),
"binding reference with target of a different base kind must be rejected with a \
type mismatch error, got: {joined}"
);
}
#[test]
fn rule_target_reference_copies_rule_value() {
let code = r#"
spec inner
rule my_r: 42
spec top
with i: inner
data x: i.my_r
rule out: x
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("rule-target reference must be accepted at plan time");
let now = DateTimeValue::now();
let result = engine
.run("top", Some(&now), HashMap::new(), false)
.expect("must evaluate without error");
assert_eq!(rule_value(&result, "out"), "42");
}
#[test]
fn rule_target_reference_propagates_veto() {
let code = r#"
spec inner
data denom: number -> default 0
rule divided: 10 / denom
spec top
with i: inner
data x: i.divided
rule out: x
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("rule-target reference must be accepted at plan time");
let now = DateTimeValue::now();
let result = engine
.run("top", Some(&now), HashMap::new(), false)
.expect("evaluator must run; veto is a domain result, not an error");
let rr = result
.results
.get("out")
.expect("rule 'out' must be present");
match &rr.result {
OperationResult::Veto(v) => {
let s = v.to_string();
assert!(
s.contains("Division by zero"),
"reference must propagate the target rule's division-by-zero veto reason, got: {s}"
);
}
OperationResult::Value(v) => {
panic!("expected propagated veto, got value: {v}");
}
}
}
#[test]
fn rule_target_reference_cycle_through_self_is_rejected() {
let code = r#"
spec inner
data slot: number
spec outer
with i: inner
data i.slot: r
rule r: i.slot
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.to_lowercase().contains("circular") || joined.to_lowercase().contains("cycle"),
"rule-target reference forming a cycle with its target rule must be rejected at plan \
time with a circular-dependency error, got: {joined}"
);
}
#[test]
fn rule_target_reference_lhs_type_mismatch_is_rejected() {
let code = r#"
spec inner
data v: number
spec source_spec
rule greeting: "hello"
spec outer
with i: inner
with src: source_spec
data i.v: src.greeting
rule r: i.v
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(code, lemma::SourceType::Labeled("reference.lemma")));
assert!(
joined.contains("type mismatch"),
"rule-target reference whose target rule's type kind differs from the \
child-declared LHS type must be rejected with a type mismatch error, \
got: {joined}"
);
}
#[test]
fn rule_target_reference_in_chain_resolves_value() {
let code = r#"
spec inner
rule my_r: 42
spec mid
with i: inner
data x: i.my_r
spec top
with m: mid
data y: m.x
rule out: y
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("rule-target reference chain must be accepted at plan time");
let now = DateTimeValue::now();
let result = engine
.run("top", Some(&now), HashMap::new(), false)
.expect("must evaluate without error");
assert_eq!(rule_value(&result, "out"), "42");
}
#[test]
fn rule_target_reference_user_override_wins_over_rule_value() {
let code = r#"
spec inner
rule my_r: 42
spec top
with i: inner
data x: i.my_r
rule out: x
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("rule-target reference must be accepted at plan time");
let now = DateTimeValue::now();
let mut overrides = HashMap::new();
overrides.insert("x".to_string(), "99".to_string());
let result = engine
.run("top", Some(&now), overrides, false)
.expect("must evaluate without error");
assert_eq!(
rule_value(&result, "out"),
"99",
"user-provided override at the reference path must win over the target rule's value"
);
}
#[test]
fn reference_value_violating_child_declared_max_is_rejected() {
let code = r#"
spec inner
data limited: number -> maximum 5
spec source_spec
data v: number -> default 10
spec outer
with i: inner
with src: source_spec
data i.limited: src.v
rule r: i.limited
"#;
let mut engine = Engine::new();
let load_result = engine.load(code, lemma::SourceType::Labeled("reference.lemma"));
match load_result {
Ok(()) => {
let now = DateTimeValue::now();
let run_result = engine.run("outer", Some(&now), HashMap::new(), false);
match run_result {
Ok(resp) => {
let rr = resp.results.get("r").expect("rule 'r'");
match &rr.result {
OperationResult::Veto(v) => {
let s = v.to_string();
assert!(
s.contains("maximum") || s.contains("exceeds"),
"expected max-constraint veto, got: {s}"
);
}
OperationResult::Value(v) => {
panic!(
"expected constraint-violation veto or error; engine silently \
accepted out-of-range referenced value {v} (planning landmine)"
);
}
}
}
Err(err) => {
let s = err.to_string();
assert!(
s.contains("maximum") || s.contains("exceeds") || s.contains("constraint"),
"expected constraint error at run time, got: {s}"
);
}
}
}
Err(errors) => {
let joined = errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n");
assert!(
joined.contains("maximum")
|| joined.contains("exceeds")
|| joined.contains("constraint"),
"expected constraint error at load time, got: {joined}"
);
}
}
}
#[test]
fn reference_local_default_supplies_value_when_target_missing() {
let code = r#"
spec inner
data maybe: number
spec outer
with i: inner
data here: i.maybe -> default 77
rule r: here
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("must load");
let now = DateTimeValue::now();
let result = engine
.run("outer", Some(&now), HashMap::new(), false)
.expect("must evaluate");
assert_eq!(
rule_value(&result, "r"),
"77",
"reference-local default must fill in when target is missing"
);
}
#[test]
fn local_non_dotted_rhs_stays_type_declaration() {
let code = r#"
spec s
data age: number -> default 30
data person: age
rule r: person
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("loads: `data person: age` is a typedef reference, not a value-copy reference");
let now = DateTimeValue::now();
let result = engine
.run("s", Some(&now), HashMap::new(), false)
.expect("evaluates; `person` is typed 'age' and inherits its default");
assert_eq!(
rule_value(&result, "r"),
"30",
"typedef inheritance must propagate default; if this becomes a value-copy reference \
instead, the parser silently changed shape"
);
}
#[test]
fn binding_non_dotted_rhs_resolves_in_enclosing_spec() {
let code = r#"
spec inner
data slot: number
spec outer
with i: inner
data src: number -> default 123
data i.slot: src
rule r: i.slot
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("non-dotted RHS in binding context must resolve in the enclosing spec");
let now = DateTimeValue::now();
let result = engine
.run("outer", Some(&now), HashMap::new(), false)
.expect("evaluates");
assert_eq!(
rule_value(&result, "r"),
"123",
"non-dotted RHS in binding context must resolve as reference and copy 'src' value"
);
}
#[test]
fn reference_local_default_appears_in_schema() {
let code = r#"
spec inner
data maybe: number
spec outer
with i: inner
data here: i.maybe -> default 77
rule r: here
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("reference.lemma"))
.expect("must load");
let now = DateTimeValue::now();
let schema = engine
.schema("outer", Some(&now))
.expect("schema must build");
let here_entry = schema
.data
.get("here")
.expect("schema must include 'here' data entry");
let default = here_entry
.default
.as_ref()
.expect("schema must surface the reference's `-> default 77` value");
let rendered = default.to_string();
assert!(
rendered.contains("77"),
"schema default must render as 77; got: {rendered}"
);
}
#[test]
fn binding_reference_scale_family_mismatch_is_rejected() {
let code = r#"
spec inner
data money: scale -> unit eur 1.00
data payment: money
spec source_spec
data temp_unit: scale -> unit celsius 1.0
data temperature: temp_unit
spec outer
with i: inner
with src: source_spec
data i.payment: src.temperature
rule r: i.payment
"#;
let mut engine = Engine::new();
let res = engine.load(code, lemma::SourceType::Labeled("reference.lemma"));
let joined = load_err_joined(res);
assert!(
joined.contains("scale family")
|| joined.contains("scale_family")
|| joined.contains("family")
|| joined.contains("type mismatch"),
"expected scale-family-mismatch error, got: {joined}"
);
}