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
}
const PROG: &str = "\
color(red). color(blue).
sc1(X) :- X = a, fail ; X = b.
sc2(X, Y) :- X = a, Y = 1 ; X = b, Y = 2.
";
fn prog() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(PROG))
}
#[track_caller]
fn check(goal: &str, expected_out: &str, expected_code: i32) {
let (out, code) = prog().query(goal, &[]);
assert_eq!(
norm(&out),
norm(&format!("{expected_out}\n")),
"goal: {goal}"
);
assert_eq!(code, expected_code, "goal: {goal}");
}
#[track_caller]
fn succeeds_once(goal: &str) {
let (out, code) = prog().query(goal, &[]);
assert!(
out.contains("\"count\":1,\"exhausted\":true"),
"goal {goal}: {out}"
);
assert_eq!(code, 1, "goal: {goal}");
}
#[track_caller]
fn fails(goal: &str) {
let (out, code) = prog().query(goal, &[]);
assert_eq!(
out, "{\"count\":0,\"exhausted\":true,\"solutions\":[]}\n",
"goal: {goal}"
);
assert_eq!(code, 0, "goal: {goal}");
}
#[track_caller]
fn err_contains(goal: &str, needle: &str) {
let (out, code) = prog().query(goal, &[]);
assert!(out.contains(needle), "goal {goal}: {out}");
assert_eq!(code, 3, "goal: {goal}");
}
#[test]
fn throw_uncaught_surfaces_as_error() {
let (out, code) = prog().query("throw(my_error)", &[]);
assert_eq!(out, "{\"error\":\"Runtime error: my_error\"}\n");
assert_eq!(code, 3);
}
#[test]
fn catch_traps_and_binds() {
check(
"catch(throw(e), e, X = recovered)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"recovered\"}]}",
1,
);
check(
"catch(throw(foo(bar)), foo(X), true)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"bar\"}]}",
1,
);
check(
"catch(throw(payload(42)), payload(N), Y = N)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"N\":42,\"Y\":42}]}",
1,
);
}
#[test]
fn catch_passthrough_non_matching() {
let (out, code) = prog().query("catch(throw(a), b, true)", &[]);
assert_eq!(out, "{\"error\":\"Runtime error: a\"}\n");
assert_eq!(code, 3);
}
#[test]
fn catch_transparent_when_no_throw() {
check(
"catch(color(X), _, true)",
"{\"count\":2,\"exhausted\":true,\"solutions\":[{\"X\":\"red\"},{\"X\":\"blue\"}]}",
1,
);
}
#[test]
fn catch_traps_builtin_errors() {
check(
"catch(X is foo + 1, _, X = trapped)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"trapped\"}]}",
1,
);
check(
"catch(undefined_predicate(X), error(existence_error(procedure, _), _), Y = trapped)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"_V\",\"Y\":\"trapped\"}]}",
1,
);
check(
"catch(X is foo, error(type_error(evaluable, _), _), Y = trapped)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"_V\",\"Y\":\"trapped\"}]}",
1,
);
}
#[test]
fn catch_nested() {
check(
"catch(catch(throw(e), e, X = inner), e, X = outer)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"inner\"}]}",
1,
);
check(
"catch(catch(throw(b), a, X = inner), b, X = outer)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"outer\"}]}",
1,
);
}
#[test]
fn throw_unbound_is_instantiation_error() {
check(
"catch(throw(_), error(instantiation_error, _), X = trapped)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"trapped\"}]}",
1,
);
}
#[test]
fn catch_inside_naf() {
err_contains("\\+ throw(my_err)", "my_err");
check(
"\\+ catch(throw(e), e, fail)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{}]}",
1,
);
}
#[test]
fn existence_and_type_error_shapes() {
let (out, code) = prog().query("frobnicate(X, Y)", &[]);
assert!(out.contains("existence_error(procedure"), "{out}");
assert!(out.contains("frobnicate"), "{out}");
assert_eq!(code, 3);
}
#[test]
fn semicolon_comma_precedence() {
check(
"sc1(X)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"b\"}]}",
1,
);
check(
"sc2(X, Y)",
"{\"count\":2,\"exhausted\":true,\"solutions\":[{\"X\":\"a\",\"Y\":1},{\"X\":\"b\",\"Y\":2}]}",
1,
);
}
#[test]
fn unify_with_occurs_check() {
check(
"unify_with_occurs_check(X, a)",
"{\"count\":1,\"exhausted\":true,\"solutions\":[{\"X\":\"a\"}]}",
1,
);
fails("unify_with_occurs_check(X, f(X))");
}
#[test]
fn negation_with_member_and_naf_binding() {
succeeds_once("X = 1, Y = 2, X \\= Y");
succeeds_once("X = foo, Y = bar, X \\== Y");
succeeds_once("\\+ (1 = 2)");
}
#[test]
fn no_occurs_check_unify_v1_divergence() {
succeeds_once("X = f(X)");
}