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, "writeln", 1, "Write a term to stdout followed by a newline."),
92 b(Det, "nl", 0, "Write a newline to stdout."),
93 b(Inline, "=", 2, "Unification: `X = Y` succeeds if X and Y can be made identical."),
95 b(Inline, "\\=", 2, "Not-unifiable: succeeds when `=` would fail."),
96 b(Inline, "is", 2, "Arithmetic evaluation: `X is Expr` binds X to the value of Expr."),
97 b(Inline, "compare", 3, "`compare(Order, T1, T2)` — bind Order to <, =, or > per standard term ordering."),
98 b(Inline, "==", 2, "Term identity: structural equality without unification."),
99 b(Inline, "\\==", 2, "Term non-identity."),
100 b(Inline, "@<", 2, "Standard term ordering: less."),
101 b(Inline, "@>", 2, "Standard term ordering: greater."),
102 b(Inline, "@=<", 2, "Standard term ordering: less-or-equal."),
103 b(Inline, "@>=", 2, "Standard term ordering: greater-or-equal."),
104 b(Inline, "<", 2, "Arithmetic less-than."),
105 b(Inline, ">", 2, "Arithmetic greater-than."),
106 b(Inline, "=<", 2, "Arithmetic less-or-equal (note: `=<`, not `<=`)."),
107 b(Inline, ">=", 2, "Arithmetic greater-or-equal."),
108 b(Inline, "=:=", 2, "Arithmetic equality."),
109 b(Inline, "=\\=", 2, "Arithmetic inequality."),
110 b(Control, ",", 2, "`(A, B)` — conjunction: prove A, then B."),
112 b(Control, ";", 2, "`(A ; B)` — disjunction: prove A, or B on backtracking. `(C -> T ; E)` reads as if-then-else."),
113 b(Control, "->", 2, "`(C -> T)` — if-then: if C succeeds (committing to its first solution), prove T; otherwise fail."),
114 b(Control, "\\+", 1, "Negation as failure: succeeds when its argument fails."),
115 b(Control, "once", 1, "`once(Goal)` — succeed at most once for Goal."),
116 b(Control, "catch", 3, "`catch(Goal, Catcher, Recovery)` — run Goal; on thrown error matching Catcher, run Recovery."),
117 b(Control, "throw", 1, "Raise an error term that propagates to the nearest matching `catch/3`."),
118 b(Control, "findall", 3, "`findall(Template, Goal, List)` — collect all solutions of Goal."),
119 b(Control, "call", 1, "Meta-call: execute its argument as a goal. Variadic — extra args are appended."),
120 b(Control, "between", 3, "`between(Low, High, X)` — enumerate or test integers in [Low, High]."),
121 b(Atom, "true", 0, "Always succeeds."),
123 b(Atom, "fail", 0, "Always fails."),
124 b(Atom, "false", 0, "Always fails (alias for `fail`)."),
125 b(Atom, "!", 0, "Cut: commit to current choices; remove choice points back to the parent clause."),
126];
127
128const fn b(kind: BuiltinKind, name: &'static str, arity: u32, doc: &'static str) -> BuiltinSpec {
130 BuiltinSpec {
131 name,
132 arity,
133 kind,
134 doc,
135 }
136}
137
138pub fn lookup(name: &str, arity: u32) -> Option<&'static BuiltinSpec> {
140 BUILTINS.iter().find(|s| s.name == name && s.arity == arity)
141}
142
143pub fn doc(name: &str) -> Option<&'static str> {
146 BUILTINS.iter().find(|s| s.name == name).map(|s| s.doc)
147}
148
149pub fn atom_names() -> impl Iterator<Item = &'static str> {
151 BUILTINS
152 .iter()
153 .filter(|s| s.arity == 0 && s.completable())
154 .map(|s| s.name)
155}
156
157pub fn functor_names() -> impl Iterator<Item = (&'static str, u32)> {
159 BUILTINS
160 .iter()
161 .filter(|s| s.arity > 0 && s.completable())
162 .map(|s| (s.name, s.arity))
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn roster_size_and_partition() {
171 assert_eq!(BUILTINS.len(), 55, "roster size changed — update the doc");
172 let count = |k: BuiltinKind| BUILTINS.iter().filter(|s| s.kind == k).count();
173 assert_eq!(count(Det), 25);
174 assert_eq!(count(Inline), 16);
175 assert_eq!(count(Control), 10);
176 assert_eq!(count(Atom), 4);
177 }
178
179 #[test]
180 fn no_duplicate_name_arity() {
181 for (i, s) in BUILTINS.iter().enumerate() {
182 for t in &BUILTINS[i + 1..] {
183 assert!(
184 !(s.name == t.name && s.arity == t.arity),
185 "duplicate {}/{}",
186 s.name,
187 s.arity
188 );
189 }
190 }
191 }
192
193 #[test]
194 fn every_row_has_a_doc() {
195 for s in BUILTINS {
196 assert!(!s.doc.is_empty(), "{}/{} has no doc", s.name, s.arity);
197 }
198 }
199
200 #[test]
203 fn reference_doc_covers_every_builtin() {
204 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
205 .join("../../docs/builtin-reference.md");
206 let doc = std::fs::read_to_string(&path).expect("docs/builtin-reference.md must exist");
207 for s in BUILTINS {
208 let entry = format!("{}/{}", s.name, s.arity);
209 assert!(
210 doc.contains(&entry),
211 "{entry} is missing from {}",
212 path.display()
213 );
214 }
215 }
216
217 #[test]
218 fn completable_tracks_identifier_not_kind() {
219 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()); }
231
232 #[test]
233 fn accessors_respect_completable() {
234 let atoms: Vec<_> = atom_names().collect();
235 assert!(atoms.contains(&"true") && atoms.contains(&"nl"));
236 assert!(!atoms.contains(&"!"));
237 let fns: Vec<_> = functor_names().collect();
238 assert!(fns.contains(&("once", 1)) && fns.contains(&("catch", 3)));
239 assert!(!fns.contains(&(",", 2)) && !fns.contains(&("=..", 2)));
240 }
241}