use crate::{parse_program, parse_program_spanned, EvalError, Program};
fn answers(src: &str) -> Vec<Vec<String>> {
let clauses = parse_program(src).expect("parses");
let program = Program::compile(&clauses).expect("compiles");
program
.queries()
.iter()
.map(|q| program.solve(q).map(|a| a.to_string()).collect())
.collect()
}
#[test]
fn facts_rules_and_backtracking() {
let out = answers(
"parent(tom, bob). parent(tom, liz). parent(bob, ann).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
?- parent(tom, X).
?- grandparent(tom, W).
?- parent(ann, X).",
);
assert_eq!(out[0], vec!["X = bob", "X = liz"]);
assert_eq!(out[1], vec!["W = ann"]);
assert!(out[2].is_empty());
}
#[test]
fn recursion_over_lists() {
let out = answers(
"append(nil, L, L).
append(cons(H, T), L, cons(H, R)) :- append(T, L, R).
?- append(X, Y, cons(a, cons(b, nil))).",
);
assert_eq!(
out[0],
vec![
"X = nil, Y = cons(a, cons(b, nil))",
"X = cons(a, nil), Y = cons(b, nil)",
"X = cons(a, cons(b, nil)), Y = nil",
]
);
}
#[test]
fn constraint_goals_in_bodies_and_queries() {
let out = answers(
"same(X, X).
p(X) :- { X = f(Y) }, same(Y, a).
?- p(Z).
?- same(A, B).
?- same(A, B), { A != B }.
?- { X = 0.50 }, same(X, 0.5).",
);
assert_eq!(out[0], vec!["Z = f(a)"]);
assert_eq!(out[1], vec!["A = B"]);
assert!(out[2].is_empty());
assert_eq!(out[3], vec!["X = 1/2"]);
}
#[test]
fn implication_is_sugar_for_a_constraint_rule() {
let out = answers(
"{ X = ok } => fine(X).
?- fine(ok).
?- fine(bad).
?- fine(Y).",
);
assert_eq!(out[0], vec!["true"]);
assert!(out[1].is_empty());
assert_eq!(out[2], vec!["Y = ok"]);
}
#[test]
fn constraint_facts_form_the_initial_store() {
let out = answers("{ X != a }. { f(A) = f(b) }. p(1). ?- p(X).");
assert_eq!(out[0], vec!["X = 1"]);
let out = answers("p(X) :- { Y != a }. q(X) :- { X != a }. ?- p(Z). ?- q(Z).");
assert_eq!(out[0], vec!["true"]);
assert_eq!(out[1], vec!["Z != a"]);
let clauses = parse_program("{ a != a }. p(1).").unwrap();
assert_eq!(Program::compile(&clauses).unwrap_err(), EvalError::InitialStoreUnsatisfiable);
let clauses = parse_program("{ X = a }. { X != a }. p(1).").unwrap();
assert!(Program::compile(&clauses).is_ok());
}
#[test]
fn undefined_predicate_fails() {
let out = answers("p(1). ?- q(X). ?- p(X), q(X).");
assert!(out[0].is_empty());
assert!(out[1].is_empty());
}
#[test]
fn cyclic_answers_print_as_equations() {
let out = answers(
"omega(X) :- { X = f(X) }.
?- omega(X).
?- omega(X), { X = f(f(X)) }.
?- { X = g(Y, Y), Y = h(X) }.
?- { X = f(X, a) }, { X = f(X, b) }.",
);
assert_eq!(out[0], vec!["X = f(X)"]);
assert_eq!(out[1], vec!["X = f(X)"]);
assert_eq!(out[2], vec!["X = g(Y, Y), Y = h(X)"]);
assert!(out[3].is_empty());
}
#[test]
fn dif_answers_show_pending_disequations() {
let out = answers(
"same(X, X).
?- { X != Y }, same(X, f(Z)), same(Y, f(W)).
?- { X != Y }, same(X, f(Z)), same(Y, f(W)), same(Z, W).
?- { X != a }, same(X, b).",
);
assert_eq!(out[0], vec!["X = f(Z), Y = f(W), Z != W"]);
assert!(out[1].is_empty());
assert_eq!(out[2], vec!["X = b"]);
}
#[test]
fn solutions_are_lazy_and_resumable() {
let clauses = parse_program("nat(zero). nat(s(N)) :- nat(N). ?- nat(X).").unwrap();
let program = Program::compile(&clauses).unwrap();
let mut sols = program.solve(&program.queries()[0]);
assert_eq!(sols.next().unwrap().to_string(), "X = zero");
assert_eq!(sols.next().unwrap().to_string(), "X = s(zero)");
assert_eq!(sols.next().unwrap().to_string(), "X = s(s(zero))");
}
#[test]
fn deep_derivations_do_not_touch_the_rust_stack() {
std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(|| {
let mut term = "zero".to_string();
for _ in 0..10_000 {
term = format!("s({term})");
}
let calls: Vec<String> = (0..10).map(|_| format!("count({term})")).collect();
let src = format!("count(zero). count(s(N)) :- count(N). ?- {}.", calls.join(", "));
let out = answers(&src);
assert_eq!(out[0], vec!["true"]);
let many: Vec<&str> = std::iter::repeat_n("p", 20_000).collect();
let src = format!("p(). ?- {}.", many.iter().map(|p| format!("{p}()")).collect::<Vec<_>>().join(", "));
assert_eq!(answers(&src)[0], vec!["true"]);
})
.unwrap()
.join()
.unwrap();
}
fn first(src: &str) -> String {
answers(src).into_iter().next().unwrap().join(" | ")
}
#[test]
fn linear_equations_determine_variables() {
assert_eq!(first("?- { X + Y = 10, X - Y = 2 }."), "X = 6, Y = 4");
assert_eq!(first("?- { 3 * X = 1 }."), "X = 1/3");
assert_eq!(first("?- { X = 1 / 3 }."), "X = 1/3");
assert_eq!(first("?- { X = -(2 * Y) }, { Y = 1.5 }."), "X = -3, Y = 3/2");
assert_eq!(first("?- { X = 0.1 + 0.2 }, { X = 0.3 }."), "X = 3/10");
assert_eq!(first("?- { X = 1 / 0 }."), "");
}
#[test]
fn arithmetic_in_heads_and_arguments() {
let prog = "sum(X, Y, X + Y).
average(X, Y, (X + Y) / 2).
temperature(fahrenheit(F), celsius((F - 32) * 5 / 9)).
discounted(P, D) :- { D = P - P / 10, D > 0 }.
same(X, X).";
let out = answers(&format!(
"{prog}
?- sum(1, 2, S).
?- average(3, 4, A).
?- temperature(fahrenheit(212), C).
?- temperature(F, celsius(100)).
?- discounted(100, D).
?- discounted(P, 90).
?- same(f(X + 1), f(2)).
?- sum(X, Y, 10), same(X, 4)."
));
assert_eq!(out[0], vec!["S = 3"]);
assert_eq!(out[1], vec!["A = 7/2"]);
assert_eq!(out[2], vec!["C = celsius(100)"]);
assert_eq!(out[3], vec!["F = fahrenheit(212)"]);
assert_eq!(out[4], vec!["D = 90"]);
assert_eq!(out[5], vec!["P = 100"]);
assert_eq!(out[6], vec!["X = 1"]);
assert_eq!(out[7], vec!["X = 4, Y = 6"]);
}
#[test]
fn inequalities_bounds_and_residuals() {
assert_eq!(first("?- { X >= 3, X <= 3 }."), "X = 3");
assert_eq!(first("?- { X > 3, X < 3 }."), "");
assert_eq!(first("?- { X > 3 }."), "X > 3");
assert_eq!(first("?- { X > 3 }, { X < 5 }."), "X > 3, X < 5");
assert_eq!(first("?- { X + Y <= 10 }."), "X + Y <= 10");
assert_eq!(first("?- { X > Y, Y > Z }."), "X > Y, Y > Z");
assert_eq!(first("?- { Y = X + 1 }."), "Y = X + 1");
assert_eq!(first("?- { Y = X + 1 }, { X > 0 }."), "Y = X + 1, X > 0");
assert_eq!(first("?- { X + Y = 10, 2*X - Y >= 1/3 }."), "Y = 10 - X, 2*X - Y >= 1/3");
}
#[test]
fn numeric_disequations_are_exact() {
assert_eq!(first("?- { X - Y = 0 }, { X != Y }."), "");
assert_eq!(first("same(X, X). ?- { X != 3 }, same(X, 3)."), "");
assert_eq!(first("same(X, X). ?- { X != 3 }, same(X, 4)."), "X = 4");
assert_eq!(first("?- { X != 3 }."), "X != 3");
assert_eq!(first("?- { X > 3 }, { X < 5 }, { X != 4 }."), "X > 3, X < 5, X != 4");
assert_eq!(first("?- { X + Y = 10 }, { X - Y = 2 }, { X != 6 }."), "");
assert_eq!(first("?- { X != Y }, { X >= 1 }, { X <= 1 }, { Y >= 1 }, { Y <= 1 }."), "");
}
#[test]
fn attribute_terms_and_congruence() {
let prog = "eligible(X) :- { age(X) >= 18 }.
{ age(socrates) > 70 }.
same(X, X).";
let out = answers(&format!(
"{prog}
?- eligible(socrates).
?- eligible(alice).
?- eligible(X), same(X, socrates).
?- eligible(X).
?- {{ age(bob) = 3 }}, eligible(bob).
?- {{ foo > 3 }}.
?- {{ X > 3 }}, same(X, foo).
?- same(X, foo), {{ X > 3 }}.
?- {{ X = 3 }}, {{ X = f(a) }}.
?- {{ X = f(a) }}, {{ X = 3 }}.
?- same(X, 3), same(X, f(a)).
?- {{ age(X) + 1 >= 19 }}, same(X, bob)."
));
assert_eq!(out[0], vec!["age(socrates) > 70"]); assert_eq!(out[1], vec!["age(alice) >= 18"]);
assert_eq!(out[2], vec!["X = socrates, age(socrates) > 70"]);
assert_eq!(out[3], vec!["age(X) >= 18"]);
assert!(out[4].is_empty()); assert_eq!(out[5], vec!["foo > 3"]);
assert_eq!(out[6], vec!["X = foo, foo > 3"]);
assert_eq!(out[7], vec!["X = foo, foo > 3"]);
assert_eq!(out[8], vec!["X = 3, f(a) = 3"]);
assert_eq!(out[9], vec!["X = f(a), f(a) = 3"]);
assert!(out[10].is_empty()); assert_eq!(out[11], vec!["X = bob, age(bob) >= 18"]);
}
#[test]
fn delayed_products() {
let prog = "same(X, X).";
let out = answers(&format!(
"{prog}
?- {{ Y = X * Z }}, same(X, 2), same(Z, 3).
?- {{ Y = X * Z }}, same(Z, 3), {{ X = 2 }}.
?- {{ Y = X / Z }}, same(Z, 4), same(X, 1).
?- {{ Y = X / Z }}, same(Z, 0)."
));
assert_eq!(out[0], vec!["Y = 6, X = 2, Z = 3"]);
assert_eq!(out[1], vec!["Y = 6, X = 2, Z = 3"]);
assert_eq!(out[2], vec!["Y = 1/4, X = 1, Z = 4"]);
assert!(out[3].is_empty()); let clauses = parse_program("?- { Y = X * Z }.").unwrap();
let program = Program::compile(&clauses).unwrap();
let mut sols = program.solve(&program.queries()[0]);
assert!(sols.next().is_none());
assert!(matches!(sols.error(), Some(EvalError::NonLinear { .. })));
}
#[test]
fn determined_variables_wake_difs_and_congruence() {
assert_eq!(first("?- { X != Y }, { X = 1 }, { Y = 1 }."), "");
assert_eq!(first("?- { X != Y }, { X = 1 }, { Y = 2 }."), "X = 1, Y = 2");
assert_eq!(first("?- { f(X) != f(3) }, { X >= 3, X <= 3 }."), "");
assert_eq!(first("?- { f(X) != f(3) }, { X >= 4 }."), "X >= 4");
assert_eq!(first("?- { age(X) > 1 }, { X = 3 }, { age(3) < 1 }."), "");
}
#[test]
fn numeric_examples_from_the_readme_run() {
let out = answers(
"human(socrates).
mortal(X) :- human(X).
{ age(socrates) > 70 }.
eligible(X) :- { age(X) >= 18 }.
{ age(X) >= 18 } => eligible(X).
?- mortal(socrates).
?- eligible(alice), { age(alice) >= 18 }.",
);
assert_eq!(out[0], vec!["true"]);
assert_eq!(out[1], vec!["age(alice) >= 18", "age(alice) >= 18"]);
}
#[test]
fn compile_spanned_still_works_and_initial_store_checks_numerics() {
let clauses = parse_program_spanned("{ X > 3, X < 2 }. p(1).").unwrap();
assert_eq!(Program::compile_spanned(&clauses).unwrap_err(), EvalError::InitialStoreUnsatisfiable);
let clauses = parse_program_spanned("{ age(a) > 3 }. p(1). ?- p(X).").unwrap();
assert!(Program::compile_spanned(&clauses).is_ok());
}
#[test]
fn no_floating_point_in_the_crate() {
fn scan(dir: &std::path::Path, hits: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
scan(&path, hits);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
let text = std::fs::read_to_string(&path).unwrap();
for (i, line) in text.lines().enumerate() {
let code = line.split("//").next().unwrap_or("");
if (code.contains("f64") || code.contains("f32")) && !path.ends_with("tests.rs") {
hits.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
}
}
}
}
}
let mut hits = Vec::new();
scan(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut hits);
assert!(hits.is_empty(), "floating point in src/: {hits:#?}");
}
#[test]
fn internal_variables_are_eliminated() {
let prog = "p(X) :- { X = Y + Z, Y > 0, Z > 0 }.
q(X) :- { X = Y + 1, Y != 2 }.
r(X, Y) :- { X > Y, Y > Z, X > Z }.
s(X) :- { X > 3, X > 2 }.
t(X) :- { X + Y = 10, X + Y = 10 }.
u(X) :- { X = 2 * Y, Y >= 1, Y <= 3 }.
v(X) :- { X = Y + Z, Y > 0, Z >= 0 }.
w(X) :- { X = 3 * Y + 1, Y >= 1 }.
b(X) :- { X = Y + Z, Y >= 0, Y <= 1, Z >= 0, Z <= 1 }.";
let out = answers(&format!(
"{prog}
?- p(A). ?- q(A). ?- r(A, B). ?- s(A). ?- t(A). ?- u(A). ?- v(A). ?- w(A). ?- b(A)."
));
assert_eq!(out[0], vec!["A > 0"]);
assert_eq!(out[1], vec!["A != 3"]); assert_eq!(out[2], vec!["A > B"]);
assert_eq!(out[3], vec!["A > 3"]);
assert_eq!(out[4], vec!["true"]);
assert_eq!(out[5], vec!["A >= 2, A <= 6"]);
assert_eq!(out[6], vec!["A > 0"]); assert_eq!(out[7], vec!["A >= 4"]); assert_eq!(out[8], vec!["A >= 0, A <= 2"]);
}
#[test]
fn redundant_constraints_are_dropped() {
assert_eq!(first("?- { X > 3, X > 2 }."), "X > 3");
assert_eq!(first("?- { X > Y, Y > Z }."), "X > Y, Y > Z");
assert_eq!(first("?- { X + Y <= 10, X + Y <= 20 }."), "X + Y <= 10");
assert_eq!(first("?- { 2 * X <= 6 }."), "X <= 3");
assert_eq!(first("?- { X > Y, Y > Z, X > Z }."), "X > Y, Y > Z");
assert_eq!(first("?- { X = Y + 1, X = Y + 1 }."), "X = Y + 1");
}
#[test]
fn tree_disequations_print_in_reduced_form() {
let out = answers(
"same(X, X).
?- { X != Y }, same(X, f(Z)), same(Y, f(W)).
?- { f(X, b) != f(a, Y) }, same(X, a).
?- { X != Y }, same(X, f(Z, V)), same(Y, f(W, U)).
?- { X != Y }, same(X, f(Z, a)), same(Y, f(W, b)).",
);
assert_eq!(out[0], vec!["X = f(Z), Y = f(W), Z != W"]);
assert_eq!(out[1], vec!["X = a, Y != b"]);
assert_eq!(out[2], vec!["X = f(Z, V), Y = f(W, U), f(Z, V) != f(W, U)"]);
assert_eq!(out[3], vec!["X = f(Z, a), Y = f(W, b)"]);
}
#[test]
fn projection_budget_fallback_stays_correct() {
let args: Vec<String> = (0..20).map(|i| format!("A{i}")).chain((0..20).map(|i| format!("B{i}"))).collect();
let mut body = String::from("{ ");
for i in 0..20 {
body.push_str(&format!("W >= A{i}, W <= B{i}, "));
}
body.push_str("A0 >= 1 }");
let src = format!("p({}) :- {body}. ?- p({}).", args.join(", "), args.join(", "));
let out = answers(&src);
let ans = &out[0][0];
assert!(ans.contains("A0 >= 1"), "{ans}");
assert!(ans.contains("_1 >= A0") || ans.contains("A0 <= _1"), "survivor named: {ans}");
}
#[test]
fn answers_are_deterministic() {
let src = "p(X, Y) :- { X = A + B, Y = A - B, A > 0, B > 0, A != B }.
?- p(U, V).";
let first_run = answers(src);
for _ in 0..5 {
assert_eq!(answers(src), first_run);
}
}
#[test]
fn interrupt_flag_stops_a_runaway_query() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let clauses = parse_program("p() :- p(). ?- p().").unwrap();
let program = Program::compile(&clauses).unwrap();
let flag = Arc::new(AtomicBool::new(false));
let mut sols = program.solve(&program.queries()[0]).with_interrupt(flag.clone());
let setter = {
let flag = flag.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
flag.store(true, Ordering::Relaxed);
})
};
assert!(sols.next().is_none());
assert!(sols.interrupted());
assert!(sols.error().is_none());
setter.join().unwrap();
let clauses = parse_program("q(a). q(b). ?- q(X).").unwrap();
let program = Program::compile(&clauses).unwrap();
let mut sols = program.solve(&program.queries()[0]);
assert_eq!(sols.next().unwrap().to_string(), "X = a");
assert!(sols.may_continue());
assert_eq!(sols.next().unwrap().to_string(), "X = b");
assert!(!sols.may_continue());
}