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