#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BuiltinKind {
Control,
Inline,
Det,
Atom,
}
#[derive(Clone, Copy, Debug)]
pub struct BuiltinSpec {
pub name: &'static str,
pub arity: u32,
pub kind: BuiltinKind,
pub doc: &'static str,
}
impl BuiltinSpec {
pub fn completable(&self) -> bool {
self.name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
}
}
use BuiltinKind::{Atom, Control, Det, Inline};
#[rustfmt::skip]
pub const BUILTINS: &[BuiltinSpec] = &[
b(Det, "var", 1, "Type check: succeeds if argument is an unbound variable."),
b(Det, "nonvar", 1, "Type check: succeeds if argument is bound."),
b(Det, "atom", 1, "Type check: succeeds if argument is an atom."),
b(Det, "number", 1, "Type check: succeeds if argument is an integer or float."),
b(Det, "integer", 1, "Type check: succeeds if argument is an integer."),
b(Det, "float", 1, "Type check: succeeds if argument is a float."),
b(Det, "compound", 1, "Type check: succeeds if argument is a compound term."),
b(Det, "is_list", 1, "Type check: succeeds if argument is a proper list."),
b(Det, "functor", 3, "`functor(Term, Name, Arity)` — inspect or construct a term's functor."),
b(Det, "arg", 3, "`arg(N, Term, Arg)` — extract the N-th argument of Term."),
b(Det, "=..", 2, "Univ: `T =.. L` decomposes T into a list of its functor and args."),
b(Det, "copy_term", 2, "`copy_term(T, C)` — bind C to a copy of T with fresh variables."),
b(Det, "atom_length", 2, "`atom_length(A, L)` — bind L to the length of atom A."),
b(Det, "atom_concat", 3, "`atom_concat(A, B, C)` — concatenate atoms A and B into C."),
b(Det, "atom_chars", 2, "`atom_chars(A, Chars)` — convert between an atom and a list of single-char atoms."),
b(Det, "number_chars", 2, "`number_chars(N, Chars)` — convert between a number and a list of single-char atoms."),
b(Det, "number_codes", 2, "`number_codes(N, Codes)` — convert between a number and a list of character codes."),
b(Det, "msort", 2, "`msort(L, Sorted)` — sort without removing duplicates."),
b(Det, "sort", 2, "`sort(L, Sorted)` — sort and remove duplicates."),
b(Det, "succ", 2, "`succ(X, S)` — Peano successor relation; S = X + 1, both non-negative."),
b(Det, "plus", 3, "`plus(X, Y, Z)` — addition relation; any one argument may be unbound."),
b(Det, "unify_with_occurs_check", 2, "Unification with occurs check: rejects `X = f(X)`-style cycles."),
b(Det, "write", 1, "Write a term to stdout (no newline)."),
b(Det, "writeln", 1, "Write a term to stdout followed by a newline."),
b(Det, "nl", 0, "Write a newline to stdout."),
b(Inline, "=", 2, "Unification: `X = Y` succeeds if X and Y can be made identical."),
b(Inline, "\\=", 2, "Not-unifiable: succeeds when `=` would fail."),
b(Inline, "is", 2, "Arithmetic evaluation: `X is Expr` binds X to the value of Expr."),
b(Inline, "compare", 3, "`compare(Order, T1, T2)` — bind Order to <, =, or > per standard term ordering."),
b(Inline, "==", 2, "Term identity: structural equality without unification."),
b(Inline, "\\==", 2, "Term non-identity."),
b(Inline, "@<", 2, "Standard term ordering: less."),
b(Inline, "@>", 2, "Standard term ordering: greater."),
b(Inline, "@=<", 2, "Standard term ordering: less-or-equal."),
b(Inline, "@>=", 2, "Standard term ordering: greater-or-equal."),
b(Inline, "<", 2, "Arithmetic less-than."),
b(Inline, ">", 2, "Arithmetic greater-than."),
b(Inline, "=<", 2, "Arithmetic less-or-equal (note: `=<`, not `<=`)."),
b(Inline, ">=", 2, "Arithmetic greater-or-equal."),
b(Inline, "=:=", 2, "Arithmetic equality."),
b(Inline, "=\\=", 2, "Arithmetic inequality."),
b(Control, ",", 2, "`(A, B)` — conjunction: prove A, then B."),
b(Control, ";", 2, "`(A ; B)` — disjunction: prove A, or B on backtracking. `(C -> T ; E)` reads as if-then-else."),
b(Control, "->", 2, "`(C -> T)` — if-then: if C succeeds (committing to its first solution), prove T; otherwise fail."),
b(Control, "\\+", 1, "Negation as failure: succeeds when its argument fails."),
b(Control, "once", 1, "`once(Goal)` — succeed at most once for Goal."),
b(Control, "catch", 3, "`catch(Goal, Catcher, Recovery)` — run Goal; on thrown error matching Catcher, run Recovery."),
b(Control, "throw", 1, "Raise an error term that propagates to the nearest matching `catch/3`."),
b(Control, "findall", 3, "`findall(Template, Goal, List)` — collect all solutions of Goal."),
b(Control, "call", 1, "Meta-call: execute its argument as a goal. Variadic — extra args are appended."),
b(Control, "between", 3, "`between(Low, High, X)` — enumerate or test integers in [Low, High]."),
b(Atom, "true", 0, "Always succeeds."),
b(Atom, "fail", 0, "Always fails."),
b(Atom, "false", 0, "Always fails (alias for `fail`)."),
b(Atom, "!", 0, "Cut: commit to current choices; remove choice points back to the parent clause."),
];
const fn b(kind: BuiltinKind, name: &'static str, arity: u32, doc: &'static str) -> BuiltinSpec {
BuiltinSpec {
name,
arity,
kind,
doc,
}
}
pub fn lookup(name: &str, arity: u32) -> Option<&'static BuiltinSpec> {
BUILTINS.iter().find(|s| s.name == name && s.arity == arity)
}
pub fn doc(name: &str) -> Option<&'static str> {
BUILTINS.iter().find(|s| s.name == name).map(|s| s.doc)
}
pub fn atom_names() -> impl Iterator<Item = &'static str> {
BUILTINS
.iter()
.filter(|s| s.arity == 0 && s.completable())
.map(|s| s.name)
}
pub fn functor_names() -> impl Iterator<Item = (&'static str, u32)> {
BUILTINS
.iter()
.filter(|s| s.arity > 0 && s.completable())
.map(|s| (s.name, s.arity))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roster_size_and_partition() {
assert_eq!(BUILTINS.len(), 55, "roster size changed — update the doc");
let count = |k: BuiltinKind| BUILTINS.iter().filter(|s| s.kind == k).count();
assert_eq!(count(Det), 25);
assert_eq!(count(Inline), 16);
assert_eq!(count(Control), 10);
assert_eq!(count(Atom), 4);
}
#[test]
fn no_duplicate_name_arity() {
for (i, s) in BUILTINS.iter().enumerate() {
for t in &BUILTINS[i + 1..] {
assert!(
!(s.name == t.name && s.arity == t.arity),
"duplicate {}/{}",
s.name,
s.arity
);
}
}
}
#[test]
fn every_row_has_a_doc() {
for s in BUILTINS {
assert!(!s.doc.is_empty(), "{}/{} has no doc", s.name, s.arity);
}
}
#[test]
fn reference_doc_covers_every_builtin() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../docs/builtin-reference.md");
let doc = std::fs::read_to_string(&path).expect("docs/builtin-reference.md must exist");
for s in BUILTINS {
let entry = format!("{}/{}", s.name, s.arity);
assert!(
doc.contains(&entry),
"{entry} is missing from {}",
path.display()
);
}
}
#[test]
fn completable_tracks_identifier_not_kind() {
assert!(lookup("is", 2).unwrap().completable()); assert!(lookup("compare", 3).unwrap().completable()); assert!(lookup("once", 1).unwrap().completable()); assert!(lookup("catch", 3).unwrap().completable()); assert!(lookup("nl", 0).unwrap().completable()); assert!(!lookup("\\+", 1).unwrap().completable()); assert!(!lookup("=..", 2).unwrap().completable()); assert!(!lookup("!", 0).unwrap().completable()); assert!(!lookup(";", 2).unwrap().completable()); }
#[test]
fn accessors_respect_completable() {
let atoms: Vec<_> = atom_names().collect();
assert!(atoms.contains(&"true") && atoms.contains(&"nl"));
assert!(!atoms.contains(&"!"));
let fns: Vec<_> = functor_names().collect();
assert!(fns.contains(&("once", 1)) && fns.contains(&("catch", 3)));
assert!(!fns.contains(&(",", 2)) && !fns.contains(&("=..", 2)));
}
}