# `PtcRunner.Lisp.SourceAtoms`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/lisp/source_atoms.ex#L1)

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 env-dispatched builtin name from
     `PtcRunner.Lisp.BuiltinNames.env_names/0` — all builtin
     functions (`map`, `filter`, `+`, `str`, etc.). These equal the
     keys of `PtcRunner.Lisp.Env.initial/0` but are derived from the
     compile-time registry so `SourceAtoms` stays out of the Lisp
     runtime cycle (issue #1051).
  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).

# `intern`

```elixir
@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`

```elixir
@spec table() :: %{required(String.t()) =&gt; atom()}
```

Returns the full lookup table — binary names → atoms.

Cached in `:persistent_term` after first call.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
