Skip to main content

patch_prolog_shared/
builtins.rs

1//! The builtin / control-construct vocabulary: the single source of
2//! truth for *which* names the language knows, their arities, and a
3//! one-line doc each. Reconciled in `docs/design/BUILTIN_VOCAB.md`.
4//!
5//! This is **data only** — no symbol mapping (that stays compiler-side
6//! in `codegen/lower.rs::DET_BUILTINS`) and no dispatch (the runtime
7//! keeps its own `match`). Codegen and the runtime are *checked against*
8//! this table; the LSP (completion + hover) *reads* it directly.
9//!
10//! Zero-dependency, like the rest of `plg-shared`. IMPORTANT: the
11//! runtime must NOT reference `BUILTINS` outside `#[cfg(test)]` — the
12//! `doc` strings would otherwise land in every compiled program binary
13//! (see the doc's "doc strings must never reach a compiled program"
14//! constraint).
15
16/// Where a name is handled, used only to partition the validation that
17/// codegen/runtime stay in sync with this table. Not a dispatch hint.
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
19pub enum BuiltinKind {
20    /// Control construct — handled structurally in codegen/runtime
21    /// (`,` `;` `->` `\+` `once` `catch` `throw` `findall` `call`
22    /// `between`). Also covers nondeterministic builtins dispatched as
23    /// predicates (`between`, `atom_concat`): not control *flow*, but they
24    /// share the same predicate-call lowering rather than the `Det` path.
25    Control,
26    /// Inline goal — its own `LGoal` variant or an op-code table
27    /// (`=` `\=` `is` `compare`, arithmetic and term-order comparisons).
28    Inline,
29    /// Deterministic builtin dispatched to a `plg_rt_b_*` symbol
30    /// (the `DET_BUILTINS` set).
31    Det,
32    /// Reserved arity-0 atom goal (`true` `fail` `false` `!`).
33    Atom,
34}
35
36/// One vocabulary entry. `arity` is the canonical arity; `call/N` is
37/// listed once at its minimum arity (1) and noted variadic in `doc`.
38#[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    /// Completion-eligible iff the name is a typeable identifier (first
48    /// char ASCII-alphabetic). Offers `findall`/`once`/`is`/`compare`/
49    /// `nl`, suppresses operators and `!`. Deliberately NOT derived from
50    /// `kind`: `is`/`compare` (Inline) complete, `\+` (Control) does not.
51    /// If a real counter-example appears, promote this to an explicit
52    /// `complete: bool` column on `BuiltinSpec` and overrule per row.
53    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/// The full vocabulary (56 rows). Docs for everything except `,` `;`
64/// `->` port verbatim from v1's `BUILTIN_DOCS`; those three are new.
65/// `rustfmt::skip` keeps it as a one-row-per-line table — the doc
66/// strings would otherwise wrap to five lines each.
67#[rustfmt::skip]
68pub const BUILTINS: &[BuiltinSpec] = &[
69    // --- Det: deterministic builtins (DET_BUILTINS mirror) ---
70    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    // --- Inline: own LGoal variant / op-code table ---
96    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    // --- Control: structural constructs ---
113    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    // --- Atom: reserved arity-0 goals ---
125    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
131/// Const ctor so the table above reads as one row per line.
132const 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
141/// Exact lookup by name and arity (`call/N` matches only at arity 1).
142pub fn lookup(name: &str, arity: u32) -> Option<&'static BuiltinSpec> {
143    BUILTINS.iter().find(|s| s.name == name && s.arity == arity)
144}
145
146/// First doc for `name` regardless of arity — hover is arity-insensitive
147/// (the cursor is on a name, not a resolved call).
148pub fn doc(name: &str) -> Option<&'static str> {
149    BUILTINS.iter().find(|s| s.name == name).map(|s| s.doc)
150}
151
152/// Completion: arity-0 names worth offering.
153pub 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
160/// Completion: (name, arity) for arity->0 names worth offering.
161pub 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    /// The user-facing reference must enumerate every builtin — drift guard
204    /// so `docs/builtin-reference.md` can't silently fall behind the table.
205    #[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        // alphabetic-leading names complete, regardless of kind...
223        assert!(lookup("is", 2).unwrap().completable()); // Inline, yes
224        assert!(lookup("compare", 3).unwrap().completable()); // Inline, yes
225        assert!(lookup("once", 1).unwrap().completable()); // Control, yes
226        assert!(lookup("catch", 3).unwrap().completable()); // Control, yes (v1 omitted)
227        assert!(lookup("nl", 0).unwrap().completable()); // Det atom-arity, yes
228        // ...operators and `!` do not.
229        assert!(!lookup("\\+", 1).unwrap().completable()); // Control, no
230        assert!(!lookup("=..", 2).unwrap().completable()); // Det operator, no
231        assert!(!lookup("!", 0).unwrap().completable()); // Atom, no
232        assert!(!lookup(";", 2).unwrap().completable()); // Control, no
233    }
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}