mod harness;
use harness::{Compiled, compile};
use std::sync::OnceLock;
fn empty() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile("dummy_fact.\n"))
}
#[track_caller]
fn ok(goal: &str, expected_x: &str) {
let (out, code) = empty().query(goal, &[]);
assert_eq!(
out,
format!("{{\"count\":1,\"exhausted\":true,\"solutions\":[{{\"X\":{expected_x}}}]}}\n"),
"goal: {goal}"
);
assert_eq!(code, 1, "goal: {goal}");
}
#[track_caller]
fn solves(goal: &str) {
let (out, code) = empty().query(goal, &[]);
assert_eq!(
out, "{\"count\":1,\"exhausted\":true,\"solutions\":[{}]}\n",
"goal: {goal}"
);
assert_eq!(code, 1, "goal: {goal}");
}
#[track_caller]
fn succeeds_once(goal: &str) {
let (out, code) = empty().query(goal, &[]);
assert!(
out.contains("\"count\":1,\"exhausted\":true"),
"goal {goal}: {out}"
);
assert_eq!(code, 1, "goal: {goal}");
}
#[track_caller]
fn err_contains(goal: &str, needle: &str) {
let (out, code) = empty().query(goal, &[]);
assert!(out.contains(needle), "goal {goal}: {out}");
assert_eq!(code, 3, "goal: {goal}");
}
#[test]
fn arithmetic_functions() {
ok("X is abs(-42)", "42");
ok("X is abs(42)", "42");
ok("X is max(10, 20)", "20");
ok("X is min(10, 20)", "10");
ok("X is sign(42)", "1");
ok("X is sign(0)", "0");
ok("X is sign(-7)", "-1");
ok("X is abs(min(3, -5))", "5");
}
#[test]
fn extended_operators() {
ok("X is 2 ^ 10", "1024");
ok("X is 2 ^ 3 ^ 2", "512"); ok("X is 1 << 4", "16");
ok("X is 32 >> 2", "8");
ok("X is 6 /\\ 3", "2");
ok("X is 5 \\/ 2", "7");
ok("X is 6 xor 3", "5");
}
#[test]
fn pow_is_always_float() {
ok("X is 2 ** 3", "8.0");
succeeds_once("X is 2 ** 3, float(X)");
succeeds_once("X is 2 ** 3, X =:= 8");
succeeds_once("X is 2 ^ 10, integer(X)");
}
#[test]
fn operator_precedence() {
succeeds_once("X is 2 * 3 ** 2, X =:= 18");
ok("X is 1 + 2 << 1", "5");
ok("X is 6 /\\ 3 + 1", "3");
}
#[test]
fn div_floor_semantics() {
ok("X is -7 div 2", "-4");
ok("X is 7 div -2", "-4");
ok("X is 7 div 2", "3");
}
#[test]
fn integer_division_and_rem() {
ok("X is 7 // 2", "3");
ok("X is -7 // 2", "-3");
ok("X is 7 rem 3", "1");
ok("X is -7 rem 2", "-1");
}
#[test]
fn mod_floored_semantics() {
ok("X is -7 mod 3", "2");
ok("X is 7 mod -3", "-2");
ok("X is 5 mod -3", "-1");
}
#[test]
fn mod_vs_rem_difference() {
let (out, code) = empty().query("X is -7 mod 2, Y is -7 rem 2", &[]);
assert_eq!(
out,
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":1,\"Y\":-1}]}\n"
);
assert_eq!(code, 1);
}
#[test]
fn iso_div_yields_float() {
let (out, _) = empty().query("X is 10 / 3", &[]);
assert!(out.contains("3.333"), "{out}");
let (out, _) = empty().query("X is -10 / 3", &[]);
assert!(out.contains("-3.333"), "{out}");
ok("X is 6 / 2", "3.0");
succeeds_once("X is 6 / 2, float(X)");
let (out, code) = empty().query("X is 6 / 2, integer(X)", &[]);
assert_eq!(out, "{\"count\":0,\"exhausted\":true,\"solutions\":[]}\n");
assert_eq!(code, 0);
ok("X is 10 // 3", "3");
}
#[test]
fn double_minus_and_infix() {
ok("X is - - 3", "3");
ok("X is -3 + 5", "2");
ok("X is 1 + 2", "3");
}
#[test]
fn arithmetic_error_terms() {
err_contains("X is 10 / 0", "zero");
err_contains("X is 1.0 / 0", "Division by zero");
err_contains("X is 1 / 0", "zero");
err_contains("X is 5 div 0", "zero");
err_contains("X is Y + 1", "instantiation");
err_contains(&format!("X is {} + 1", i64::MAX), "overflow");
err_contains("X is 1 << -1", "Shift");
err_contains("X is foo + 1", "type_error(evaluable");
}
#[test]
fn succ_plus_overflow() {
err_contains(&format!("succ({}, X)", i64::MAX), "overflow");
err_contains(&format!("plus({}, 1, X)", i64::MAX), "overflow");
}
#[test]
fn naf_precedence() {
solves("\\+ 1 =:= 2");
let (out, code) = empty().query("X = hello, \\+ X = goodbye", &[]);
assert_eq!(
out,
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"hello\"}]}\n"
);
assert_eq!(code, 1);
}