patch-prolog-compiler 0.4.2

Standalone Prolog compiler (plgc) — compiles .pl to native binaries via LLVM
//! ISO 8.16.2 `atom_concat/3` — forward assembly AND the relational split
//! modes (issue #35).
//!
//! `atom_concat/3` is multi-mode, like `append/3` for lists:
//! - forward: A and B atoms → concatenate, unify with C;
//! - known prefix/suffix: one of A/B bound + C bound → the matching split;
//! - both unbound + C bound → enumerate every decomposition on backtracking;
//! - insufficiently instantiated (A or B var, C var) → `instantiation_error`
//!   (NOT `type_error`, which is reserved for a bound term of the wrong type).
//!
//! These pin ISO behaviour directly; the old forward-only `type_error`-on-split
//! contract was a bug. Output is the readable text format (default).

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, "false.\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)", "X = foobar");
    // Both bound: a verification, binding nothing.
    check("atom_concat(foo, bar, foobar)", "true.");
    fails("atom_concat(foo, bar, nope)");
}

#[test]
fn known_prefix_or_suffix_selects_the_split() {
    check("atom_concat(X, bar, foobar)", "X = foo");
    check("atom_concat(foo, Y, foobar)", "Y = bar");
    // A prefix/suffix that doesn't line up has no solution (it does not error).
    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)",
        "A = \nB = abc\nA = a\nB = bc\nA = ab\nB = c\nA = abc\nB =",
    );
    // The empty atom decomposes exactly one way: into two empty atoms.
    check("atom_concat(A, B, '')", "A = \nB =");
}

#[test]
fn shared_variable_keeps_only_equal_halves() {
    // atom_concat(X, X, C) succeeds only at the split whose halves match.
    check("atom_concat(X, X, abab)", "X = ab");
    fails("atom_concat(X, X, abc)");
}

#[test]
fn unicode_splits_on_char_boundaries() {
    // Multi-byte atoms must split between characters, never inside a code point.
    check(
        "atom_concat(A, B, héllo)",
        "A = \nB = héllo\nA = h\nB = éllo\nA = hé\nB = llo\nA = hél\nB = lo\nA = héll\nB = o\nA = héllo\nB =",
    );
}

#[test]
fn unbound_with_unbound_c_is_instantiation_error() {
    // ISO: instantiation_error, not type_error, when a needed argument is a var.
    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() {
    // Exercises the compiled predicate-dispatch path (clause.rs), distinct from
    // the top-level query/metacall path: a nondeterministic atom_concat mid-body
    // must enumerate and feed each solution to the continuation.
    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(), "P = \nP = a\nP = ab\nP = abc",);
}

#[test]
fn split_mode_reachable_via_metacall() {
    // call/1 routes through the runtime's query-side dispatch (try_builtin),
    // which must also enumerate splits.
    check(
        "call(atom_concat(A, B, ab))",
        "A = \nB = ab\nA = a\nB = b\nA = ab\nB =",
    );
}