mod harness;
use harness::{Compiled, compile};
use std::sync::OnceLock;
fn prog() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile("p.\n"))
}
#[track_caller]
fn check(goal: &str, expected: &str) {
let (out, code) = prog().query(goal, &[]);
assert_eq!(out.trim_end(), expected, "goal: {goal}");
assert_eq!(code, 1, "goal {goal} should succeed: {out}");
}
#[track_caller]
fn fails(goal: &str) {
let (out, code) = prog().query(goal, &[]);
assert_eq!(
out, "{\"count\":0,\"exhausted\":true,\"solutions\":[]}\n",
"goal {goal} should have no solutions"
);
assert_eq!(code, 0, "goal: {goal}");
}
#[track_caller]
fn errors(goal: &str, needle: &str) {
let (out, code) = prog().query(goal, &[]);
assert!(out.contains(needle), "goal {goal}: expected {needle}, got {out}");
assert_eq!(code, 3, "goal {goal} should error: {out}");
}
#[test]
fn forward_assembly() {
check(
"atom_concat(foo, bar, X)",
r#"{"count":1,"exhausted":true,"solutions":[{"X":"foobar"}]}"#,
);
check(
"atom_concat(foo, bar, foobar)",
r#"{"count":1,"exhausted":true,"solutions":[{}]}"#,
);
fails("atom_concat(foo, bar, nope)");
}
#[test]
fn known_prefix_or_suffix_selects_the_split() {
check(
"atom_concat(X, bar, foobar)",
r#"{"count":1,"exhausted":true,"solutions":[{"X":"foo"}]}"#,
);
check(
"atom_concat(foo, Y, foobar)",
r#"{"count":1,"exhausted":true,"solutions":[{"Y":"bar"}]}"#,
);
fails("atom_concat(X, xyz, foobar)");
fails("atom_concat(zzz, Y, foobar)");
}
#[test]
fn both_unbound_enumerates_every_decomposition() {
check(
"atom_concat(A, B, abc)",
r#"{"count":4,"exhausted":true,"solutions":[{"A":"","B":"abc"},{"A":"a","B":"bc"},{"A":"ab","B":"c"},{"A":"abc","B":""}]}"#,
);
check(
"atom_concat(A, B, '')",
r#"{"count":1,"exhausted":true,"solutions":[{"A":"","B":""}]}"#,
);
}
#[test]
fn shared_variable_keeps_only_equal_halves() {
check(
"atom_concat(X, X, abab)",
r#"{"count":1,"exhausted":true,"solutions":[{"X":"ab"}]}"#,
);
fails("atom_concat(X, X, abc)");
}
#[test]
fn unicode_splits_on_char_boundaries() {
check(
"atom_concat(A, B, héllo)",
r#"{"count":6,"exhausted":true,"solutions":[{"A":"","B":"héllo"},{"A":"h","B":"éllo"},{"A":"hé","B":"llo"},{"A":"hél","B":"lo"},{"A":"héll","B":"o"},{"A":"héllo","B":""}]}"#,
);
}
#[test]
fn unbound_with_unbound_c_is_instantiation_error() {
errors("atom_concat(X, Y, Z)", "instantiation_error");
errors("atom_concat(foo, Y, Z)", "instantiation_error");
errors("atom_concat(X, bar, Z)", "instantiation_error");
}
#[test]
fn bound_non_atom_is_type_error() {
errors("atom_concat(123, foo, _)", "type_error(atom, 123)");
errors("atom_concat(foo, 456, _)", "type_error(atom, 456)");
}
#[test]
fn split_mode_works_inside_a_compiled_clause_body() {
let c = compile("prefix(P, W) :- atom_concat(P, _, W).\n");
let (out, code) = c.query("prefix(P, abc)", &[]);
assert_eq!(code, 1, "{out}");
assert_eq!(
out.trim_end(),
r#"{"count":4,"exhausted":true,"solutions":[{"P":""},{"P":"a"},{"P":"ab"},{"P":"abc"}]}"#,
);
}
#[test]
fn split_mode_reachable_via_metacall() {
check(
"call(atom_concat(A, B, ab))",
r#"{"count":3,"exhausted":true,"solutions":[{"A":"","B":"ab"},{"A":"a","B":"b"},{"A":"ab","B":""}]}"#,
);
}