use lemma::parsing::ast::DateTimeValue;
use lemma::Engine;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
fn source() -> lemma::SourceType {
lemma::SourceType::Path(Arc::new(PathBuf::from("test.lemma")))
}
fn eval_rule(code: &str, spec_name: &str, rule_name: &str) -> String {
let mut engine = Engine::new();
engine.load(code, source()).expect("Should parse and plan");
let now = DateTimeValue::now();
let response = engine
.run(
None,
spec_name,
Some(&now),
HashMap::new(),
false,
lemma::EvaluationRequest::default(),
)
.expect("Should evaluate");
let result = response
.results
.get(rule_name)
.unwrap_or_else(|| panic!("Rule '{}' not found in response", rule_name));
result
.result
.value()
.unwrap_or_else(|| {
panic!(
"Rule '{}' returned non-value: {:?}",
rule_name, result.result
)
})
.to_string()
}
fn expect_plan_error(code: &str, expected_fragment: &str) {
let mut engine = Engine::new();
let result = engine.load(code, source());
assert!(
result.is_err(),
"Expected planning error containing '{}', but loading succeeded",
expected_fragment
);
if !expected_fragment.is_empty() {
let errors = result.unwrap_err();
let combined = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
assert!(
combined.contains(expected_fragment),
"Expected error containing '{}', got: {}",
expected_fragment,
combined
);
}
}
#[test]
fn d1_base_quantity_decomposition_self_cancels() {
let code = r#"spec d1
data money: quantity -> unit eur 1.00 -> unit cent 0.01
data price1: 10 eur
data price2: 5 eur
rule ratio: price1 / price2"#;
let val = eval_rule(code, "d1", "ratio");
assert!(val.contains('2'), "Expected 2 (10/5), got: {val}");
assert!(
!val.to_lowercase().contains("eur"),
"Same-family quantity/quantity should cancel to number, got: {val}"
);
}
#[test]
fn d1_cross_unit_same_family_cancels() {
let code = r#"spec d1b
data money: quantity -> unit eur 1.00 -> unit cent 0.01
data price_cents: 100 cent
data price_eur: 1 eur
rule ratio: price_cents / price_eur"#;
let val = eval_rule(code, "d1b", "ratio");
assert!(
val.contains('1'),
"Expected 1.00 (100 cent == 1 eur), got: {val}"
);
}
#[test]
fn d2_velocity_compound_unit_decomposition() {
let code = r#"spec d2
uses lemma si
data length: quantity -> unit meter 1 -> unit kilometer 1000
data velocity: quantity -> unit mps meter/second -> unit kmh kilometer/hour
data dist: 100 meter
data secs: 20 seconds
rule speed: (dist / secs) as mps"#;
let val = eval_rule(code, "d2", "speed");
assert!(val.contains('5'), "Expected 5 mps, got: {val}");
assert!(
val.to_lowercase().contains("mps"),
"Result should be in mps, got: {val}"
);
}
#[test]
fn d2_velocity_in_kmh_conversion() {
let code = r#"spec d2b
uses lemma si
data length: quantity -> unit meter 1 -> unit kilometer 1000
data velocity: quantity -> unit mps meter/second -> unit kmh kilometer/hour
data dist: 100 meter
data secs: 20 seconds
rule speed_kmh: (dist / secs) as kmh"#;
let val = eval_rule(code, "d2b", "speed_kmh");
assert!(
val.contains("17") || val.contains("18"),
"Expected ~18 kmh, got: {val}"
);
assert!(
val.to_lowercase().contains("kmh"),
"Result should be in kmh, got: {val}"
);
}
#[test]
fn d3_inconsistent_unit_decompositions_rejected() {
let code = r#"spec d3
uses lemma si
data length: quantity -> unit meter 1
data velocity: quantity -> unit mps meter/second -> unit just_meters meter"#;
expect_plan_error(code, "inconsistent");
}
#[test]
fn d4_same_quantity_self_reference_rejected() {
let code = r#"spec d4
uses lemma si
data velocity: quantity -> unit mps velocity/second"#;
expect_plan_error(code, "");
}
#[test]
fn d5_uses_does_not_import_quantity_types_for_compound_units() {
let code = r#"spec spec_b
data length: quantity -> unit meter 1
spec spec_a
uses lb: spec_b
data velocity: quantity -> unit mps meter/second"#;
expect_plan_error(code, "");
}
#[test]
fn d6_uses_and_qualified_parent_makes_type_available_for_compound_units() {
let code = r#"spec spec_b
data length: quantity -> unit meter 1
spec spec_a
uses lemma si
uses spec_b
data length: spec_b.length
data velocity: quantity -> unit mps meter/second
data dist: 100 meter
data secs: 20 seconds
rule speed: (dist / secs) as mps"#;
let val = eval_rule(code, "spec_a", "speed");
assert!(val.contains('5'), "Expected 5 mps, got: {val}");
}
#[test]
fn d7_cross_library_same_named_quantity_resolves_speed_literal() {
let code = r#"spec spec_b
uses lemma si
data length: quantity -> unit meter 1
data velocity: quantity -> unit mps meter/second
spec spec_a
uses lemma si
data length: quantity -> unit meter 1
uses spec_b_ref: spec_b
data velocity: spec_b_ref.velocity
data dist: 100 meter
data secs: 20 seconds
rule speed: (dist / secs) as mps"#;
let val = eval_rule(code, "spec_a", "speed");
assert!(
val.contains('5'),
"Expected speed near 5 mps for 100 meter / 20 seconds, got: {val}"
);
assert!(
val.to_lowercase().contains("mps"),
"Result should be in mps, got: {val}"
);
}
#[test]
fn d8_compound_unit_with_numeric_prefix() {
let code = r#"spec d8
uses lemma si
data money: quantity -> unit eur 1.00
data wage_rate: quantity
-> unit eur_per_second eur/second
-> unit standard 28.50 eur/hour
data hours_worked: 40 hours
data rate: 1 standard
rule total: (rate * hours_worked) as eur"#;
let val = eval_rule(code, "d8", "total");
assert!(val.contains("1140"), "Expected 1140 eur, got: {val}");
}
#[test]
fn d9_quantity_with_no_canonical_unit_rejected() {
let code = r#"spec d9
data length: quantity -> unit kilometer 1000 -> unit mile 1609"#;
expect_plan_error(code, "conversion factor 1");
}
#[test]
fn d10_calendar_unit_cross_axis_arithmetic_at_rule_boundary() {
let code = r#"spec d10
data money: quantity -> unit eur 1.00
data sales: 1200 eur
rule rate: sales / 1 month"#;
expect_plan_error(code, "anonymous intermediate");
}
#[test]
fn d10_calendar_unit_in_derived_quantity_definition_allowed() {
let code = r#"spec d10b
data money: quantity -> unit eur 1.00
data monthly_rate: quantity
-> unit eur_per_month eur/month
data sales: 1200 eur
data months: 1 month
rule rate: (sales / months) as eur_per_month"#;
let val = eval_rule(code, "d10b", "rate");
assert!(
val.contains("1200") && val.to_lowercase().contains("eur_per_month"),
"Expected 1200 eur_per_month, got: {val}"
);
}
#[test]
fn d10_exact_duration_compound_cast_allowed() {
let code = r#"spec d10c
uses lemma si
data money: quantity -> unit eur 1.00
data per_second_rate: quantity
-> unit eur_per_second eur/second
data sales: 1200 eur
data seconds: 1 second
rule rate: (sales / seconds) as eur_per_second"#;
let val = eval_rule(code, "d10c", "rate");
assert!(
val.contains("1200"),
"Expected 1200 eur_per_second, got: {val}"
);
}
#[test]
fn d11_calendar_duration_in_date_arithmetic() {
let code = r#"spec d11
data d1: 3 months
data d2: 9 months
rule total: d1 + d2"#;
let val = eval_rule(code, "d11", "total");
assert!(val.contains("12"), "Expected 12 months, got: {val}");
}
#[test]
fn d12_duration_keyword_in_compound_unit() {
let code = r#"spec d12
uses lemma si
data length: quantity -> unit meter 1
data velocity: quantity -> unit mps meter/second
data dist: 200 meter
data secs: 40 seconds
rule speed: (dist / secs) as mps"#;
let val = eval_rule(code, "d12", "speed");
assert!(val.contains('5'), "Expected 5 mps, got: {val}");
}
#[test]
fn integration_velocity_basic() {
let code = r#"spec phys
uses lemma si
data length: quantity -> unit meter 1 -> unit kilometer 1000
data velocity: quantity -> unit mps meter/second -> unit kmh kilometer/hour
data dist: 1000 meter
data time: 200 seconds
rule speed_mps: (dist / time) as mps
rule speed_kmh: (dist / time) as kmh"#;
let mps = eval_rule(code, "phys", "speed_mps");
assert!(mps.contains('5'), "Expected 5 mps, got: {mps}");
let kmh = eval_rule(code, "phys", "speed_kmh");
assert!(
kmh.contains("17") || kmh.contains("18"),
"Expected ~18 kmh, got: {kmh}"
);
}
#[test]
fn integration_wage_rate() {
let code = r#"spec wage
uses lemma si
data money: quantity -> unit eur 1.00 -> unit cent 0.01
data wage_rate: quantity
-> unit eur_per_second eur/second
-> unit eur_per_hour eur/hour
data hours: 8 hours
data rate: 85 eur_per_hour
rule total: (rate * hours) as eur"#;
let val = eval_rule(code, "wage", "total");
assert!(val.contains("680"), "Expected 680 eur, got: {val}");
}
#[test]
fn integration_anonymous_at_rule_boundary_rejected() {
let code = r#"spec phys
uses lemma si
data length: quantity -> unit meter 1
data dist: 100 meter
data time: 20 seconds
rule speed: dist / time"#;
expect_plan_error(code, "anonymous intermediate");
}
#[test]
fn integration_as_number_on_anonymous_rejected() {
let code = r#"spec phys
uses lemma si
data length: quantity -> unit meter 1
data dist: 100 meter
data time: 20 seconds
rule speed: (dist / time) as number"#;
expect_plan_error(code, "anonymous intermediate");
}
#[test]
fn integration_same_family_quantity_multiply_rejected() {
let code = r#"spec t
data money: quantity -> unit eur 1.00
data a: 10 eur
data b: 5 eur
rule product: a * b"#;
expect_plan_error(code, "");
}
#[test]
fn integration_same_family_quantity_multiply_via_as_number() {
let code = r#"spec t
data money: quantity -> unit eur 1.00
data a: 10 eur
data b: 5 eur
rule product: (a as number) * (b as number)"#;
let val = eval_rule(code, "t", "product");
assert!(val.contains("50"), "Expected 50, got: {val}");
}
#[test]
fn integration_quantity_divide_quantity_same_family() {
let code = r#"spec t
data money: quantity -> unit eur 1.00 -> unit cent 0.01
data a: 100 eur
data b: 25 eur
rule ratio: a / b"#;
let val = eval_rule(code, "t", "ratio");
assert!(val.contains('4'), "Expected 4, got: {val}");
assert!(
!val.to_lowercase().contains("eur"),
"Result should be dimensionless number, got: {val}"
);
}
#[test]
fn integration_typedef_cast_dimension_mismatch_rejected() {
let code = r#"spec phys
uses lemma si
data length: quantity -> unit meter 1
data money: quantity -> unit eur 1.00
data dist: 100 meter
data time: 20 seconds
rule speed_as_eur: (dist / time) as eur"#;
expect_plan_error(code, "do not match target dimensions");
}
#[test]
fn integration_quantity_power_integer_literal() {
let code = r#"spec t
data money: quantity -> unit eur 1.00
data a: 3 eur
rule cube: a ^ 3"#;
let val = eval_rule(code, "t", "cube");
assert!(val.contains("27"), "Expected 27 eur, got: {val}");
}
#[test]
fn integration_quantity_power_fractional_rejected() {
let code = r#"spec t
data money: quantity -> unit eur 1.00
data a: 4 eur
rule frac_pow: a ^ 0.5"#;
expect_plan_error(code, "fractional");
}
#[test]
fn integration_quantity_power_variable_rejected() {
let code = r#"spec t
data money: quantity -> unit eur 1.00
data a: 4 eur
data exponent: 2
rule powered: a ^ exponent"#;
expect_plan_error(code, "integer literal");
}