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 PROG: &str = "\
score(alice, 85).
score(bob, 92).
score(carol, 78).
sorted_names(Sorted) :- findall(Name, score(Name, _), L), sort(L, Sorted).
my_list([1,2,3]).
";
fn prog() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(PROG))
}
#[test]
fn list_operations() {
check(
prog(),
"member(X, [a, b, c])",
"{\"count\":3,\"exhausted\":true,\"solutions\":[{\"X\":\"a\"},{\"X\":\"b\"},{\"X\":\"c\"}]}",
1,
);
check(
prog(),
"append([1, 2], [3, 4], X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[1,2,3,4]}]}",
1,
);
check(
prog(),
"length([a, b, c, d], N)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"N\":4}]}",
1,
);
}
#[test]
fn reverse_and_last() {
check(
prog(),
"reverse([1, 2, 3], X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[3,2,1]}]}",
1,
);
check(
prog(),
"last([1, 2, 3], X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":3}]}",
1,
);
}
#[test]
fn is_list_predicate() {
check(
prog(),
"X = [1,2,3], is_list(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[1,2,3]}]}",
1,
);
check(
prog(),
"my_list(X), is_list(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[1,2,3]}]}",
1,
);
}
#[test]
fn copy_term_ground_and_lists() {
check(
prog(),
"copy_term(hello, Copy)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Copy\":\"hello\"}]}",
1,
);
check(
prog(),
"copy_term(42, Copy)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Copy\":42}]}",
1,
);
check(
prog(),
"copy_term([a, b, c], X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[\"a\",\"b\",\"c\"]}]}",
1,
);
check(
prog(),
"copy_term([1,2,3,4,5,6,7,8,9,10], X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[1,2,3,4,5,6,7,8,9,10]}]}",
1,
);
}
#[test]
fn copy_term_fresh_vars_and_aliasing() {
check(
prog(),
"copy_term(f(X, Y), Copy)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Copy\":{\"args\":[\"_V\",\"_V\"],\"functor\":\"f\"},\"X\":\"_V\",\"Y\":\"_V\"}]}",
1,
);
check(
prog(),
"copy_term(f(X, X), f(A, B)), A = hello",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"A\":\"hello\",\"B\":\"hello\",\"X\":\"_V\"}]}",
1,
);
check(
prog(),
"copy_term([A, B, C], [1, 2, 3])",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"A\":\"_V\",\"B\":\"_V\",\"C\":\"_V\"}]}",
1,
);
}
const BETWEEN: &str = "digit(D) :- between(0, 9, D).\n";
fn between() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(BETWEEN))
}
#[test]
fn between_basic() {
check(
between(),
"between(1, 5, X)",
"{\"count\":5,\"exhausted\":true,\"solutions\":[{\"X\":1},{\"X\":2},{\"X\":3},{\"X\":4},{\"X\":5}]}",
1,
);
check(
between(),
"between(3, 3, X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":3}]}",
1,
);
check(
between(),
"between(5, 3, X)",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
}
#[test]
fn between_in_rule_and_findall() {
check(
between(),
"digit(D)",
"{\"count\":10,\"exhausted\":true,\"solutions\":[{\"D\":0},{\"D\":1},{\"D\":2},{\"D\":3},{\"D\":4},{\"D\":5},{\"D\":6},{\"D\":7},{\"D\":8},{\"D\":9}]}",
1,
);
check(
between(),
"findall(X, between(1, 5, X), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":[1,2,3,4,5],\"X\":\"_V\"}]}",
1,
);
}
#[test]
fn between_bound_x_fast_paths() {
check(
between(),
"between(1, 1000000, 5)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{}]}",
1,
);
check(
between(),
"between(1, 10, 11)",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
check(
between(),
"between(5, 10, 3)",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
check(
between(),
"findall(X, (X = 42, between(1, 1000000, X)), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":[42],\"X\":\"_V\"}]}",
1,
);
check(
between(),
"X = 50, \\+ between(1, 1000000, X)",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
}
#[test]
fn between_overflow_at_max_v1_divergence() {
let g = format!("between({}, {}, X)", i64::MAX, i64::MAX);
check(
between(),
&g,
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":9223372036854775807}]}",
1,
);
}
#[test]
fn succ_and_plus() {
check(
prog(),
"succ(3, X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":4}]}",
1,
);
check(
prog(),
"succ(X, 5)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":4}]}",
1,
);
check(
prog(),
"succ(X, 0)",
"{\"count\":0,\"exhausted\":true,\"solutions\":[]}",
0,
);
check(
prog(),
"plus(3, 4, X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":7}]}",
1,
);
check(
prog(),
"plus(3, Y, 10)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"Y\":7}]}",
1,
);
check(
prog(),
"plus(X, 4, 10)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":6}]}",
1,
);
}
#[test]
fn findall_filter_and_empty() {
let c = compile(
"score(alice, 85).\nscore(bob, 92).\nscore(carol, 78).\nscore(dave, 95).\n\
missing(_) :- fail.\n",
);
check(
&c,
"findall(Name, (score(Name, S), S > 90), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":[\"bob\",\"dave\"],\"Name\":\"_V\",\"S\":\"_V\"}]}",
1,
);
check(
&c,
"findall(X, missing(X), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":\"[]\",\"X\":\"_V\"}]}",
1,
);
}
#[test]
fn sort_in_rule() {
check(
prog(),
"sorted_names(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":[\"alice\",\"bob\",\"carol\"]}]}",
1,
);
}
#[test]
fn findall_once_collects_one() {
let c = compile("color(red). color(green). color(blue).\n");
check(
&c,
"findall(X, once(color(X)), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":[\"red\"],\"X\":\"_V\"}]}",
1,
);
check(
&c,
"findall(X, once(member(X, [a,b,c])), L)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"L\":[\"a\"],\"X\":\"_V\"}]}",
1,
);
}
#[test]
fn cut_in_findall_stops_clause_iteration() {
let c = compile("p(1). p(2). p(3).\n");
check(
&c,
"findall(X, (p(X), !), Xs)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"_V\",\"Xs\":[1]}]}",
1,
);
check(
&c,
"findall(X, p(X), Xs)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"_V\",\"Xs\":[1,2,3]}]}",
1,
);
}