Bounded vocabulary — the set of names the parser is allowed to intern as atoms.
More precisely: this is the set of source-text names the analyzer/evaluator currently pattern-matches as atom literals. Builtin function names + special forms + bounded namespaces + destructuring modifiers + qualified analyzer keys + short-fn param atoms. Everything else stays as a binary in the AST so the global atom table never grows from user input (issue #953).
Why an explicit allowlist instead of String.to_existing_atom/1:
the global VM atom table is non-deterministic — unrelated modules
loading later can change how the same source parses. Codex's
pushback on the bug thread covers this in detail.
What's in the table
- Every env-dispatched builtin name from
PtcRunner.Lisp.BuiltinNames.env_names/0— all builtin functions (map,filter,+,str, etc.). These equal the keys ofPtcRunner.Lisp.Env.initial/0but are derived from the compile-time registry soSourceAtomsstays out of the Lisp runtime cycle (issue #1051). - Analyzer special forms —
let,fn,def,if,case, etc. Only forms that the analyzer currently dispatches on. No aspirational Clojure entries. - Bounded keyword modifiers used by
for/doseq/destructuring —:else,:keys,:as,:or, etc. - Bounded namespaces —
data,tool,json, plus Clojure aliases (clojure.string), and fully-qualified Java namespaces fromBuiltinNames.java_namespace_atoms/0(projected frompriv/java_interop.exs) (java.time.LocalDate, etc.). - Qualified analyzer keys such as JSON member names plus atom-named Java
namespace members projected from
priv/java_interop.exs. - Short-fn param atoms
:p1..:p20synthesized by the short-fn analyzer.
What's NOT in the table
Any spelling not listed above — which in practice means most user-chosen
names: let bindings, fn params, def names, custom keywords like
:my_kw, namespaced keys like data/foo_42. These stay as binaries in the
AST.
Membership is a property of the spelling, not of what the name is used
for. A user definition whose spelling happens to be in the table —
(defn- parse …), (let [text …] …) — interns as an atom just the same,
so a single form routinely mixes atoms and binaries. Anything comparing
definition names against reference names must normalise first; comparing
raw representations reported such definitions as undefined (#1166).
Cache
Table is built lazily on first call and cached in :persistent_term.
Read cost after first call is one :persistent_term.get/1 (no copy).
Summary
Functions
Returns the atom for name if it's in the bounded vocabulary,
otherwise returns the binary unchanged.
Returns the full lookup table — binary names → atoms.
Functions
Returns the atom for name if it's in the bounded vocabulary,
otherwise returns the binary unchanged.
This is the only function the parser should call to convert a source-text name into its AST representation.
Returns the full lookup table — binary names → atoms.
Cached in :persistent_term after first call.