use lemma::{DateTimeValue, Engine, SourceType};
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
fn src(name: &str) -> SourceType {
SourceType::Path(Arc::new(PathBuf::from(name)))
}
fn run_spec(engine: &Engine, spec: &str, data: &[(&str, &str)]) -> lemma::Response {
let now = DateTimeValue::now();
let data_map: HashMap<String, String> = data
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
engine
.run(None, spec, Some(&now), data_map, false, None)
.unwrap()
}
fn rule_display(response: &lemma::Response, rule_name: &str) -> String {
response
.results
.values()
.find(|r| r.rule.name == rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name))
.display
.clone()
.unwrap_or_else(|| panic!("rule '{}' has no display", rule_name))
}
fn rule_vetoed(response: &lemma::Response, rule_name: &str) -> bool {
response
.results
.values()
.find(|r| r.rule.name == rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name))
.vetoed
}
#[test]
fn edge_large_exponentiation() {
let mut engine = Engine::new();
engine
.load(
r#"
spec big_math
data base: number
data exponent: number
rule result: base ^ exponent
"#,
src("big.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "big_math", &[("base", "2"), ("exponent", "10")]);
let display = rule_display(&resp, "result");
assert_eq!(display, "1024", "2^10 = 1024");
let resp = run_spec(&engine, "big_math", &[("base", "10"), ("exponent", "0")]);
let display = rule_display(&resp, "result");
assert_eq!(display, "1", "10^0 = 1");
}
#[test]
fn edge_negative_arithmetic() {
let mut engine = Engine::new();
engine
.load(
r#"
spec negatives
data a: number
data b: number
rule sum: a + b
rule diff: a - b
rule product: a * b
rule negated: 0 - a
"#,
src("neg.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "negatives", &[("a", "-5"), ("b", "3")]);
let sum = rule_display(&resp, "sum");
assert_eq!(sum, "-2", "-5 + 3 = -2");
let diff = rule_display(&resp, "diff");
assert_eq!(diff, "-8", "-5 - 3 = -8");
let product = rule_display(&resp, "product");
assert_eq!(product, "-15", "-5 * 3 = -15");
let negated = rule_display(&resp, "negated");
assert_eq!(negated, "5", "0 - (-5) = 5");
}
#[test]
fn edge_three_level_spec_composition() {
let mut engine = Engine::new();
engine
.load(
r#"
spec base
data factor: 2
spec middle
uses b: base
rule doubled: b.factor * 3
spec top
uses m: middle
rule final: m.doubled + 1
"#,
src("nested.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "top", &[]);
let display = rule_display(&resp, "final");
assert_eq!(display, "7", "2 * 3 + 1 = 7");
}
#[test]
fn edge_chained_percentage_reduction() {
let mut engine = Engine::new();
engine
.load(
r#"
spec chain_pct
data amount: 1000
rule half: amount - 50% * amount
rule quarter: half - 50% * half
rule eighth: quarter - 50% * quarter
"#,
src("chain.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "chain_pct", &[]);
assert_eq!(rule_display(&resp, "half"), "500");
assert_eq!(rule_display(&resp, "quarter"), "250");
assert_eq!(rule_display(&resp, "eighth"), "125");
}
#[test]
fn edge_veto_overridden_by_later_clause() {
let mut engine = Engine::new();
engine
.load(
r#"
spec veto_override
data value: number
data override_flag: boolean
rule result: 0
unless value > 100 then veto "Too large"
unless override_flag then 999
"#,
src("veto_override.lemma"),
)
.unwrap();
let resp = run_spec(
&engine,
"veto_override",
&[("value", "200"), ("override_flag", "true")],
);
assert!(
!rule_vetoed(&resp, "result"),
"later clause should override veto"
);
assert_eq!(rule_display(&resp, "result"), "999");
let resp = run_spec(
&engine,
"veto_override",
&[("value", "200"), ("override_flag", "false")],
);
assert!(
rule_vetoed(&resp, "result"),
"veto should win when no later clause matches"
);
}
#[test]
fn edge_zero_multiplication() {
let mut engine = Engine::new();
engine
.load(
r#"
spec zero_mult
data quantity: number
data price: number
rule total: quantity * price
rule zero_check: 0 * price
"#,
src("zero.lemma"),
)
.unwrap();
let resp = run_spec(
&engine,
"zero_mult",
&[("quantity", "0"), ("price", "9999")],
);
assert_eq!(rule_display(&resp, "total"), "0", "0 * anything = 0");
assert_eq!(
rule_display(&resp, "zero_check"),
"0",
"literal 0 * price = 0"
);
}
#[test]
fn edge_text_comparison() {
let mut engine = Engine::new();
engine
.load(
r#"
spec text_cmp
data status: text
-> option "active"
-> option "inactive"
-> option "pending"
rule is_active: status is "active"
rule is_not_pending: status is not "pending"
"#,
src("text.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "text_cmp", &[("status", "active")]);
assert_eq!(rule_display(&resp, "is_active"), "true");
assert_eq!(rule_display(&resp, "is_not_pending"), "true");
let resp = run_spec(&engine, "text_cmp", &[("status", "pending")]);
assert_eq!(rule_display(&resp, "is_active"), "false");
assert_eq!(rule_display(&resp, "is_not_pending"), "false");
}
#[test]
fn edge_unless_complex_and() {
let mut engine = Engine::new();
engine
.load(
r#"
spec complex_and
data a: boolean
data b: boolean
data c: boolean
rule result: "none"
unless a and b then "a_and_b"
unless a and b and c then "all_three"
"#,
src("complex.lemma"),
)
.unwrap();
let resp = run_spec(
&engine,
"complex_and",
&[("a", "true"), ("b", "true"), ("c", "true")],
);
assert_eq!(rule_display(&resp, "result"), "all_three");
let resp = run_spec(
&engine,
"complex_and",
&[("a", "true"), ("b", "true"), ("c", "false")],
);
assert_eq!(rule_display(&resp, "result"), "a_and_b");
let resp = run_spec(
&engine,
"complex_and",
&[("a", "true"), ("b", "false"), ("c", "true")],
);
assert_eq!(rule_display(&resp, "result"), "none");
}
#[test]
fn edge_ratio_operations() {
let mut engine = Engine::new();
engine
.load(
r#"
spec ratio_math
data discount: ratio -> minimum 0% -> maximum 100%
data base: number
rule raw_mult: base * discount
"#,
src("ratio.lemma"),
)
.unwrap();
let resp = run_spec(
&engine,
"ratio_math",
&[("discount", "25%"), ("base", "400")],
);
assert_eq!(rule_display(&resp, "raw_mult"), "100", "400 * 25% = 100");
}
#[test]
fn edge_ratio_add_subtract_rejected() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec ratio_math
data discount: ratio
data base: number
rule bad: base - discount
"#,
src("ratio.lemma"),
);
assert!(result.is_err());
let msg = result
.unwrap_err()
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("; ");
assert!(msg.contains("scale explicitly"), "got: {}", msg);
}
#[test]
fn edge_temporal_before_any_version() {
let mut engine = Engine::new();
engine
.load(
r#"
spec rates 2024-01-01
data rate: 5%
spec rates 2025-01-01
data rate: 7%
spec calculator 2024-01-01
uses r: rates
data amount: number
rule result: amount * r.rate
"#,
src("temporal2.lemma"),
)
.unwrap();
let effective = DateTimeValue::from_str("2024-06-01").unwrap();
let data: HashMap<String, String> = [("amount".to_string(), "1000".to_string())].into();
let resp = engine
.run(None, "calculator", Some(&effective), data, false, None)
.unwrap();
let display = rule_display(&resp, "result");
assert_eq!(display, "50", "2024: 1000 * 5% = 50");
let effective = DateTimeValue::from_str("2025-06-01").unwrap();
let data: HashMap<String, String> = [("amount".to_string(), "1000".to_string())].into();
let resp = engine
.run(None, "calculator", Some(&effective), data, false, None)
.unwrap();
let display = rule_display(&resp, "result");
assert_eq!(display, "70", "2025: 1000 * 7% = 70");
}
#[test]
fn edge_sqrt_operations() {
let mut engine = Engine::new();
engine
.load(
r#"
spec sqrt_test
data value: number -> minimum 0
rule root: sqrt(value)
rule root_of_zero: sqrt(0)
rule root_of_one: sqrt(1)
"#,
src("sqrt.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "sqrt_test", &[("value", "144")]);
assert_eq!(rule_display(&resp, "root"), "12", "sqrt(144) = 12");
assert_eq!(rule_display(&resp, "root_of_zero"), "0", "sqrt(0) = 0");
assert_eq!(rule_display(&resp, "root_of_one"), "1", "sqrt(1) = 1");
}
#[test]
fn edge_non_terminating_division() {
let mut engine = Engine::new();
engine
.load(
r#"
spec repeating
data a: number
data b: number
rule third: a / b
"#,
src("repeating.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "repeating", &[("a", "1"), ("b", "3")]);
let display = rule_display(&resp, "third");
assert!(!display.is_empty(), "1/3 should have a display value");
}
#[test]
fn edge_no_unless_matches() {
let mut engine = Engine::new();
engine
.load(
r#"
spec fallthrough
data x: number
rule category: "default"
unless x > 100 then "high"
unless x < 0 then "negative"
"#,
src("fallthrough.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "fallthrough", &[("x", "50")]);
assert_eq!(rule_display(&resp, "category"), "default");
}
#[test]
fn edge_deep_rule_chain() {
let mut engine = Engine::new();
engine
.load(
r#"
spec chain
data input: number
rule step1: input + 1
rule step2: step1 * 2
rule step3: step2 - 3
rule step4: step3 / 2
rule step5: step4 + 10
"#,
src("chain.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "chain", &[("input", "5")]);
assert_eq!(
rule_display(&resp, "step5"),
"14.5",
"(((5+1)*2)-3)/2+10 = 14.5"
);
}
#[test]
fn edge_measure_to_number_conversion() {
let mut engine = Engine::new();
engine
.load(
r#"
spec unit_strip
uses lemma units
data weight: 5 kilogram
rule kg_value: weight as kilogram as number
rule gram_value: weight as gram as number
"#,
src("strip.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "unit_strip", &[]);
assert_eq!(
rule_display(&resp, "kg_value"),
"5",
"5kg as kg as number = 5"
);
assert_eq!(
rule_display(&resp, "gram_value"),
"5000",
"5kg as g as number = 5000"
);
}
#[test]
fn edge_not_operator() {
let mut engine = Engine::new();
engine
.load(
r#"
spec not_test
data active: boolean
data score: number
rule is_inactive: not active
rule below_threshold: not score >= 70
"#,
src("not.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "not_test", &[("active", "true"), ("score", "80")]);
assert_eq!(
rule_display(&resp, "is_inactive"),
"false",
"not true = false"
);
assert_eq!(
rule_display(&resp, "below_threshold"),
"false",
"not (80>=70) = false"
);
let resp = run_spec(&engine, "not_test", &[("active", "false"), ("score", "50")]);
assert_eq!(
rule_display(&resp, "is_inactive"),
"true",
"not false = true"
);
assert_eq!(
rule_display(&resp, "below_threshold"),
"true",
"not (50>=70) = true"
);
}
#[test]
fn edge_with_binding_override() {
let mut engine = Engine::new();
engine
.load(
r#"
spec config
data tax_rate: 10%
data base_price: 100
rule tax_amount: base_price * tax_rate
rule total: base_price + tax_amount
spec custom
uses c: config
with c.tax_rate: 25%
rule custom_total: c.total
"#,
src("with.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "custom", &[]);
assert_eq!(
rule_display(&resp, "custom_total"),
"125",
"with override: 100 + 100*25% = 125"
);
}
fn main() {}