1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
19pub enum BuiltinKind {
20 Control,
26 Inline,
29 Det,
32 Atom,
34}
35
36#[derive(Clone, Copy, Debug)]
39pub struct BuiltinSpec {
40 pub name: &'static str,
41 pub arity: u32,
42 pub kind: BuiltinKind,
43 pub doc: &'static str,
44}
45
46impl BuiltinSpec {
47 pub fn completable(&self) -> bool {
54 self.name
55 .chars()
56 .next()
57 .is_some_and(|c| c.is_ascii_alphabetic())
58 }
59}
60
61use BuiltinKind::{Atom, Control, Det, Inline};
62
63#[rustfmt::skip]
68pub const BUILTINS: &[BuiltinSpec] = &[
69 b(Det, "var", 1, "Type check: succeeds if argument is an unbound variable."),
71 b(Det, "nonvar", 1, "Type check: succeeds if argument is bound."),
72 b(Det, "atom", 1, "Type check: succeeds if argument is an atom."),
73 b(Det, "number", 1, "Type check: succeeds if argument is an integer or float."),
74 b(Det, "integer", 1, "Type check: succeeds if argument is an integer."),
75 b(Det, "float", 1, "Type check: succeeds if argument is a float."),
76 b(Det, "compound", 1, "Type check: succeeds if argument is a compound term."),
77 b(Det, "is_list", 1, "Type check: succeeds if argument is a proper list."),
78 b(Det, "functor", 3, "`functor(Term, Name, Arity)` — inspect or construct a term's functor."),
79 b(Det, "arg", 3, "`arg(N, Term, Arg)` — extract the N-th argument of Term."),
80 b(Det, "=..", 2, "Univ: `T =.. L` decomposes T into a list of its functor and args."),
81 b(Det, "copy_term", 2, "`copy_term(T, C)` — bind C to a copy of T with fresh variables."),
82 b(Det, "atom_length", 2, "`atom_length(A, L)` — bind L to the length of atom A."),
83 b(Det, "atom_chars", 2, "`atom_chars(A, Chars)` — convert between an atom and a list of single-char atoms."),
84 b(Det, "number_chars", 2, "`number_chars(N, Chars)` — convert between a number and a list of single-char atoms."),
85 b(Det, "number_codes", 2, "`number_codes(N, Codes)` — convert between a number and a list of character codes."),
86 b(Det, "msort", 2, "`msort(L, Sorted)` — sort without removing duplicates."),
87 b(Det, "sort", 2, "`sort(L, Sorted)` — sort and remove duplicates."),
88 b(Det, "succ", 2, "`succ(X, S)` — Peano successor relation; S = X + 1, both non-negative."),
89 b(Det, "plus", 3, "`plus(X, Y, Z)` — addition relation; any one argument may be unbound."),
90 b(Det, "unify_with_occurs_check", 2, "Unification with occurs check: rejects `X = f(X)`-style cycles."),
91 b(Det, "write", 1, "Write a term to stdout (no newline)."),
92 b(Det, "writeq", 1, "Write a term to stdout, quoting atoms so it reads back (no newline)."),
93 b(Det, "writeln", 1, "Write a term to stdout followed by a newline."),
94 b(Det, "nl", 0, "Write a newline to stdout."),
95 b(Inline, "=", 2, "Unification: `X = Y` succeeds if X and Y can be made identical."),
97 b(Inline, "\\=", 2, "Not-unifiable: succeeds when `=` would fail."),
98 b(Inline, "is", 2, "Arithmetic evaluation: `X is Expr` binds X to the value of Expr."),
99 b(Inline, "compare", 3, "`compare(Order, T1, T2)` — bind Order to <, =, or > per standard term ordering."),
100 b(Inline, "==", 2, "Term identity: structural equality without unification."),
101 b(Inline, "\\==", 2, "Term non-identity."),
102 b(Inline, "@<", 2, "Standard term ordering: less."),
103 b(Inline, "@>", 2, "Standard term ordering: greater."),
104 b(Inline, "@=<", 2, "Standard term ordering: less-or-equal."),
105 b(Inline, "@>=", 2, "Standard term ordering: greater-or-equal."),
106 b(Inline, "<", 2, "Arithmetic less-than."),
107 b(Inline, ">", 2, "Arithmetic greater-than."),
108 b(Inline, "=<", 2, "Arithmetic less-or-equal (note: `=<`, not `<=`)."),
109 b(Inline, ">=", 2, "Arithmetic greater-or-equal."),
110 b(Inline, "=:=", 2, "Arithmetic equality."),
111 b(Inline, "=\\=", 2, "Arithmetic inequality."),
112 b(Control, ",", 2, "`(A, B)` — conjunction: prove A, then B."),
114 b(Control, ";", 2, "`(A ; B)` — disjunction: prove A, or B on backtracking. `(C -> T ; E)` reads as if-then-else."),
115 b(Control, "->", 2, "`(C -> T)` — if-then: if C succeeds (committing to its first solution), prove T; otherwise fail."),
116 b(Control, "\\+", 1, "Negation as failure: succeeds when its argument fails."),
117 b(Control, "once", 1, "`once(Goal)` — succeed at most once for Goal."),
118 b(Control, "catch", 3, "`catch(Goal, Catcher, Recovery)` — run Goal; on thrown error matching Catcher, run Recovery."),
119 b(Control, "throw", 1, "Raise an error term that propagates to the nearest matching `catch/3`."),
120 b(Control, "findall", 3, "`findall(Template, Goal, List)` — collect all solutions of Goal."),
121 b(Control, "call", 1, "Meta-call: execute its argument as a goal. Variadic — extra args are appended."),
122 b(Control, "between", 3, "`between(Low, High, X)` — enumerate or test integers in [Low, High]."),
123 b(Control, "atom_concat", 3, "`atom_concat(A, B, C)` — concatenate A and B into C, or nondeterministically split C."),
124 b(Atom, "true", 0, "Always succeeds."),
126 b(Atom, "fail", 0, "Always fails."),
127 b(Atom, "false", 0, "Always fails (alias for `fail`)."),
128 b(Atom, "!", 0, "Cut: commit to current choices; remove choice points back to the parent clause."),
129];
130
131const fn b(kind: BuiltinKind, name: &'static str, arity: u32, doc: &'static str) -> BuiltinSpec {
133 BuiltinSpec {
134 name,
135 arity,
136 kind,
137 doc,
138 }
139}
140
141pub fn lookup(name: &str, arity: u32) -> Option<&'static BuiltinSpec> {
143 BUILTINS.iter().find(|s| s.name == name && s.arity == arity)
144}
145
146pub fn doc(name: &str) -> Option<&'static str> {
149 BUILTINS.iter().find(|s| s.name == name).map(|s| s.doc)
150}
151
152pub fn atom_names() -> impl Iterator<Item = &'static str> {
154 BUILTINS
155 .iter()
156 .filter(|s| s.arity == 0 && s.completable())
157 .map(|s| s.name)
158}
159
160pub fn functor_names() -> impl Iterator<Item = (&'static str, u32)> {
162 BUILTINS
163 .iter()
164 .filter(|s| s.arity > 0 && s.completable())
165 .map(|s| (s.name, s.arity))
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn roster_size_and_partition() {
174 assert_eq!(BUILTINS.len(), 56, "roster size changed — update the doc");
175 let count = |k: BuiltinKind| BUILTINS.iter().filter(|s| s.kind == k).count();
176 assert_eq!(count(Det), 25);
177 assert_eq!(count(Inline), 16);
178 assert_eq!(count(Control), 11);
179 assert_eq!(count(Atom), 4);
180 }
181
182 #[test]
183 fn no_duplicate_name_arity() {
184 for (i, s) in BUILTINS.iter().enumerate() {
185 for t in &BUILTINS[i + 1..] {
186 assert!(
187 !(s.name == t.name && s.arity == t.arity),
188 "duplicate {}/{}",
189 s.name,
190 s.arity
191 );
192 }
193 }
194 }
195
196 #[test]
197 fn every_row_has_a_doc() {
198 for s in BUILTINS {
199 assert!(!s.doc.is_empty(), "{}/{} has no doc", s.name, s.arity);
200 }
201 }
202
203 #[test]
206 fn reference_doc_covers_every_builtin() {
207 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
208 .join("../../docs/builtin-reference.md");
209 let doc = std::fs::read_to_string(&path).expect("docs/builtin-reference.md must exist");
210 for s in BUILTINS {
211 let entry = format!("{}/{}", s.name, s.arity);
212 assert!(
213 doc.contains(&entry),
214 "{entry} is missing from {}",
215 path.display()
216 );
217 }
218 }
219
220 #[test]
221 fn completable_tracks_identifier_not_kind() {
222 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()); }
234
235 #[test]
236 fn accessors_respect_completable() {
237 let atoms: Vec<_> = atom_names().collect();
238 assert!(atoms.contains(&"true") && atoms.contains(&"nl"));
239 assert!(!atoms.contains(&"!"));
240 let fns: Vec<_> = functor_names().collect();
241 assert!(fns.contains(&("once", 1)) && fns.contains(&("catch", 3)));
242 assert!(!fns.contains(&(",", 2)) && !fns.contains(&("=..", 2)));
243 }
244}