mod harness;
use harness::{Compiled, compile};
use std::sync::OnceLock;
fn norm(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'_' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
out.push_str("_V");
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
#[track_caller]
fn check(c: &Compiled, goal: &str, expected_out: &str, expected_code: i32) {
let (out, code) = c.query(goal, &[]);
assert_eq!(
norm(&out),
norm(&format!("{expected_out}\n")),
"goal: {goal}"
);
assert_eq!(code, expected_code, "goal: {goal}");
}
const FAMILY: &str = "\
parent(tom, mary).
parent(tom, james).
parent(mary, ann).
parent(mary, bob).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
sibling(X, Y) :- parent(P, X), parent(P, Y), X \\= Y.
";
fn family() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(FAMILY))
}
#[test]
fn family_relationships() {
check(
family(),
"grandparent(tom, X)",
"{\"count\":2,\"exhausted\":true,\"solutions\":[{\"X\":\"ann\"},{\"X\":\"bob\"}]}",
1,
);
check(
family(),
"sibling(ann, X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"bob\"}]}",
1,
);
}
const ANCESTOR: &str = "\
parent(tom, mary).
parent(mary, ann).
parent(ann, alice).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
";
fn ancestor() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(ANCESTOR))
}
#[test]
fn complex_recursive_query() {
check(
ancestor(),
"ancestor(tom, X)",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"mary\"},{\"X\":\"ann\"},{\"X\":\"alice\"}]}",
1,
);
}
const FACTORIAL: &str = "\
factorial(0, 1).
factorial(N, F) :- N > 0, N1 is N - 1, factorial(N1, F1), F is N * F1.
";
#[test]
fn arithmetic_pipeline() {
let c = compile(FACTORIAL);
check(
&c,
"factorial(5, X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":120}]}",
1,
);
}
const TYPECLASS: &str = "\
classify(X, integer) :- integer(X).
classify(X, float) :- float(X).
classify(X, atom) :- atom(X).
";
#[test]
fn type_checking_in_rules() {
let c = compile(TYPECLASS);
check(
&c,
"classify(42, T)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"T\":\"integer\"}]}",
1,
);
check(
&c,
"classify(3.14, T)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"T\":\"float\"}]}",
1,
);
check(
&c,
"classify(hello, T)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"T\":\"atom\"}]}",
1,
);
}
const CONTROL: &str = "\
absval(X, Y) :- (X < 0 -> Y is 0 - X ; Y = X).
primary_color(X) :- (X = red ; X = green ; X = blue).
classify2(X, R) :- (X > 0 -> R = positive ; R = non_positive).
test_match(X, R) :- (X = hello -> R = matched ; R = no_match).
";
fn control() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(CONTROL))
}
#[test]
fn if_then_else_in_rule() {
check(
control(),
"absval(-5, Y)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Y\":5}]}",
1,
);
check(
control(),
"absval(3, Y)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Y\":3}]}",
1,
);
}
#[test]
fn disjunction_in_rule() {
check(
control(),
"primary_color(X)",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"},{\"X\":\"green\"},{\"X\":\"blue\"}]}",
1,
);
}
#[test]
fn if_then_else_keeps_bindings() {
check(
control(),
"classify2(5, R)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"R\":\"positive\"}]}",
1,
);
check(
control(),
"classify2(-1, R)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"R\":\"non_positive\"}]}",
1,
);
check(
control(),
"test_match(hello, R)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"R\":\"matched\"}]}",
1,
);
check(
control(),
"test_match(world, R)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"R\":\"no_match\"}]}",
1,
);
}
const CUT: &str = "\
classify(X, positive) :- X > 0, !.
classify(0, zero) :- !.
classify(_, negative).
foo(a). foo(b). foo(c).
q(a). q(b). q(c).
bird(tweety).
bird(penguin).
can_fly(X) :- bird(X), \\+ penguin_species(X).
penguin_species(penguin).
";
fn cut() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(CUT))
}
#[test]
fn cut_prevents_backtracking() {
check(
cut(),
"classify(5, C)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"C\":\"positive\"}]}",
1,
);
check(
cut(),
"classify(0, C)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"C\":\"zero\"}]}",
1,
);
check(
cut(),
"classify(-3, C)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"C\":\"negative\"}]}",
1,
);
}
#[test]
fn cut_prevents_all_alternatives() {
check(
cut(),
"foo(X), !",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"a\"}]}",
1,
);
check(
cut(),
"foo(X), !, X = b",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
}
#[test]
fn cut_in_once() {
check(
cut(),
"once((foo(X), !, X = b))",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
}
#[test]
fn cut_in_try_solve_no_leak_after_once() {
check(
cut(),
"once(!), q(X)",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"a\"},{\"X\":\"b\"},{\"X\":\"c\"}]}",
1,
);
}
#[test]
fn negation_as_failure_pipeline() {
check(
cut(),
"can_fly(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"tweety\"}]}",
1,
);
}
const META: &str = "\
color(red). color(green). color(blue).
n(1). n(2). n(3).
first_n(X) :- once(n(X)).
applyg(Goal) :- call(Goal).
apply1(F, X) :- call(F, X).
foo(a, 1, 2). foo(a, 3, 4).
ok. bar :- ok.
weight(apple, 150). weight(banana, 120). weight(cherry, 8).
";
fn meta() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(META))
}
#[test]
fn once_limits_to_first_solution() {
check(
meta(),
"once(color(X))",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"}]}",
1,
);
check(
meta(),
"first_n(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":1}]}",
1,
);
}
#[test]
fn call_meta_predicate() {
check(
meta(),
"applyg(color(X))",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"},{\"X\":\"green\"},{\"X\":\"blue\"}]}",
1,
);
check(
meta(),
"call(color, X)",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"},{\"X\":\"green\"},{\"X\":\"blue\"}]}",
1,
);
check(
meta(),
"call(foo(a), X, Y)",
"{\"count\":2,\"exhausted\":true,\"solutions\":[{\"X\":1,\"Y\":2},{\"X\":3,\"Y\":4}]}",
1,
);
check(
meta(),
"apply1(color, X)",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"},{\"X\":\"green\"},{\"X\":\"blue\"}]}",
1,
);
check(
meta(),
"call(bar)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{}]}",
1,
);
}
#[test]
fn call_n_with_stdlib_member_and_nesting() {
check(
meta(),
"call(member, X, [1, 2, 3])",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":1},{\"X\":2},{\"X\":3}]}",
1,
);
check(
meta(),
"call(call(member, X), [a, b, c])",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"a\"},{\"X\":\"b\"},{\"X\":\"c\"}]}",
1,
);
}
#[test]
fn call_n_operator_atom_and_findall_inner_goal() {
check(
meta(),
"call('=', X, foo)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"foo\"}]}",
1,
);
check(
meta(),
"findall(W, call(weight, _, W), Ws), Ws = [150, 120, 8]",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"W\":\"_V\",\"Ws\":[150,120,8]}]}",
1,
);
}
#[test]
fn call_n_errors() {
let (out, code) = meta().query("call(G, X)", &[]);
assert!(out.contains("instantiation_error"), "{out}");
assert_eq!(code, 3);
let (out, code) = meta().query("call(5, X)", &[]);
assert!(out.contains("type_error(callable"), "{out}");
assert!(out.contains('5'), "{out}");
assert_eq!(code, 3);
}
#[test]
fn solution_limit_respected() {
let c = compile("n(1). n(2). n(3). n(4). n(5). n(6). n(7). n(8). n(9). n(10).\n");
let (out, _) = c.query("n(X)", &["--limit", "3"]);
assert!(out.contains("\"count\":3,\"exhausted\":false"), "{out}");
let (out, _) = c.query("n(X)", &["--limit", "100"]);
assert!(out.contains("\"count\":10,\"exhausted\":true"), "{out}");
}
#[test]
fn cut_in_negation_succeeds_with_unbound_var() {
check(
cut(),
"\\+((foo(X), !, X = b))",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"_V\"}]}",
1,
);
}