PtcRunner.Lisp.SourceAtoms (PtcRunner v0.11.0)

Copy Markdown View Source

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

  1. Every key of PtcRunner.Lisp.Env.initial/0 — all builtin functions (map, filter, +, str, etc.).
  2. Analyzer special forms — let, fn, def, if, case, etc. Only forms that the analyzer currently dispatches on. No aspirational Clojure entries.
  3. Bounded keyword modifiers used by for/doseq/destructuring — :else, :keys, :as, :or, etc.
  4. Bounded namespaces — data, tool, budget, json, mcp, plus Clojure aliases (clojure.string), and fully-qualified Java namespaces from Env.clojure_namespaces (java.time.LocalDate, etc.).
  5. Qualified analyzer keys such as servers and JSON member names matched as atom literals in dispatch_list_form clauses.
  6. Short-fn param atoms :p1..:p20 synthesized by the short-fn analyzer.

What's NOT in the table

User-defined names: var bindings from let, fn params, def bindings, custom keywords like :my_kw, namespaced keys like data/foo_42. These stay as binaries in the AST.

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

intern(name)

@spec intern(String.t()) :: atom() | String.t()

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.

table()

@spec table() :: %{required(String.t()) => atom()}

Returns the full lookup table — binary names → atoms.

Cached in :persistent_term after first call.