|
| 1 | +# ADR-0171: Merge the `rt` kernel namespace into `clojure.core`; internals move to `cljw.internal` |
| 2 | + |
| 3 | +- **Status**: Accepted (2026-07-15) |
| 4 | +- **Driver**: CIDER completion-parity campaign (user-directed 2026-07-15); |
| 5 | + survey `private/notes/gapT-completion-parity-survey.md`; oracle baseline |
| 6 | + 5/18 probes matching (`scripts/completion_oracle.py --diff`). |
| 7 | + |
| 8 | +## Context |
| 9 | + |
| 10 | +Zig-implemented builtin vars intern into an internal `rt` namespace and are |
| 11 | +referred (Var-sharing) into `user` / every entered ns; bootstrap Zig macros |
| 12 | +home there too. The home ns stays `rt`, which leaks into every observable |
| 13 | +introspection surface, diverging from mainline Clojure (F-011 violation): |
| 14 | + |
| 15 | +- `(pr-str (resolve '+))` → `#'rt/+` (mainline: `#'clojure.core/+`); same |
| 16 | + for `doc`/`meta` `:ns`, eldoc/lookup/completion `ns` fields, callable |
| 17 | + printing, and error messages. |
| 18 | +- nREPL completion candidates annotate `(rt)` where mainline shows |
| 19 | + `(clojure.core)` — the user-visible trigger for this campaign. |
| 20 | +- `(ns-publics 'clojure.core)` is missing every Zig builtin (they are |
| 21 | + refers, not mappings). |
| 22 | +- `(find-ns 'rt)` / `(all-ns)` expose an undocumented ns; `(in-ns 'rt)` |
| 23 | + can enter and clobber the kernel. |
| 24 | +- Structural scar tissue exists only because of the split: |
| 25 | + `Namespace.resolveQualified`'s hard-coded clojure.core refers-exception |
| 26 | + (env.zig), `referAllOverriding`, the per-ns rt refer in both backends, |
| 27 | + and the per-site display hack `syntax_quote.zig` carries (rt → |
| 28 | + clojure.core rename during syntax-quote resolution). |
| 29 | + |
| 30 | +## Decision |
| 31 | + |
| 32 | +**Full merge** (the Devil's-advocate fork's Alt 2 — chosen over the |
| 33 | +lighter "re-home publics but keep `rt` alive" shape): |
| 34 | + |
| 35 | +1. Public Zig builtins and bootstrap Zig macros intern with **home ns = |
| 36 | + `clojure.core`** at registration. |
| 37 | +2. `__`-prefixed (and `-`-prefixed) internal vars move to a new, documented |
| 38 | + **`cljw.internal`** namespace (consistent with the owned `cljw.*` |
| 39 | + surface; its presence in `(all-ns)` is a recorded, documented fact |
| 40 | + rather than an undocumented leak). |
| 41 | +3. The `rt` namespace is **deleted**: `Env.init` no longer creates it, the |
| 42 | + boot referAll and the per-ns rt refer in both backends go away, and the |
| 43 | + `resolveQualified` clojure.core exception + `referAllOverriding`'s |
| 44 | + rt-collision purpose dissolve. |
| 45 | +4. The bundled `.clj` `rt/…` qualified references (~48 code sites) migrate |
| 46 | + to `cljw.internal/…` (internals) or bare/`clojure.core/…` (publics); |
| 47 | + the 12 Zig-side `rt/`-qualified symbol emissions (macro hygiene, |
| 48 | + `@deref` lowering, analyzer fallback) emit `clojure.core/…` — exactly |
| 49 | + what mainline's expansions emit. |
| 50 | +5. Rebind-recursion hazards (a core.clj wrapper that redefines a name |
| 51 | + while calling the rt builtin qualified, e.g. `re-find`) are enumerated |
| 52 | + mechanically (rt mappings ∩ core.clj re-defs) and those builtins are |
| 53 | + renamed to `cljw.internal/__<name>` so the wrapper calls the internal, |
| 54 | + not itself. |
| 55 | +6. The embedded AOT bootstrap cache regenerates at build (self-consistent). |
| 56 | + Externally persisted `cljw build` archives from before this change bake |
| 57 | + `"rt"` into var_ref constants and cannot resolve it once `rt` is gone: |
| 58 | + the serializer format version is bumped so old archives fail with the |
| 59 | + explicit version message, not a resolution error. |
| 60 | + |
| 61 | +## Consequences |
| 62 | + |
| 63 | +- `#'clojure.core/+`, `(:ns (meta #'+))`, `ns-publics`, completion/lookup/ |
| 64 | + eldoc ns fields, callable printing all match mainline **at the data |
| 65 | + layer** — no per-surface normalization, no whack-a-mole (F-013). |
| 66 | +- syntax_quote's per-site rt→clojure.core rename is deleted. |
| 67 | +- Future introspection surfaces (`cljw --list-vars`, new nREPL ops) are |
| 68 | + correct by construction. |
| 69 | +- `cljw.internal` is user-visible in `(all-ns)`; completion hides its |
| 70 | + `__` vars (the campaign's hidden-filter); an AD row records the |
| 71 | + divergence "mainline has no cljw.internal". |
| 72 | +- One-time migration cost: ~60 sites + 7 test files + serializer version |
| 73 | + bump. Accepted per F-002 (finished-form wins; diff size is not a |
| 74 | + constraint). |
| 75 | + |
| 76 | +## Alternatives considered (Devil's-advocate fork output, verbatim) |
| 77 | + |
| 78 | +### Alternative 1 — smallest-diff: shared display normalization |
| 79 | + |
| 80 | +One `introspect.publicVarNs(v)` (rt→"clojure.core") consulted by |
| 81 | +completion/eldoc/lookup (introspect.zig:68; ops.zig:347,363), `printVarRef` |
| 82 | +(print.zig:1318), the callable printer (print.zig:609/1039 via |
| 83 | +`indexBuiltinNames`, primitive.zig:175), and syntax-quote (deleting the |
| 84 | +syntax_quote.zig:95 per-site hack); plus a `__`-prefix filter in |
| 85 | +`emitVarMap`. |
| 86 | + |
| 87 | +- Better than the full merge: zero bootstrap-order risk; zero `.clj` |
| 88 | + churn; no AOT interaction; no rebind hazards; lands in one cycle. |
| 89 | +- Breaks / fails to fix: it is display-layer lying — the data still says |
| 90 | + `rt`. `(ns-name (:ns (meta (resolve '+))))` → `rt` unless var-meta is |
| 91 | + faked too (another site); `(ns-publics 'clojure.core)` still missing |
| 92 | + builtins; `(find-ns 'rt)`, `(all-ns)`, `(ns-resolve 'rt '+)`, |
| 93 | + `(in-ns 'rt)` clobber hazard all remain. Every future introspection |
| 94 | + surface must remember to call the normalizer — the exact per-site |
| 95 | + whack-a-mole F-013 forbids, and it does not reach the same finished |
| 96 | + form, so F-002's smallest-diff tiebreaker does not apply. Rejected as |
| 97 | + terminal shape. |
| 98 | + |
| 99 | +### Alternative 2 — finished-form-clean: FULL MERGE, `rt` eliminated (ADOPTED) |
| 100 | + |
| 101 | +Intern public Zig builtins + bootstrap macros with home = `clojure.core` |
| 102 | +at registration (primitive.zig:71-111 target swap; macro_transforms.zig:48); |
| 103 | +move `__`/`-`-prefixed internals to a real, documented `cljw.internal` ns. |
| 104 | +Delete `rt` entirely: Env.init (env.zig:390), the boot referAll |
| 105 | +(primitive.zig:169), the per-ns rt refer in both backends (vm.zig:1217,1247; |
| 106 | +tree_walk.zig:578), and the `resolveQualified` clojure.core exception |
| 107 | +(env.zig:222-228) — it dissolves, as does `referAllOverriding`'s |
| 108 | +rt-collision raison d'être and `cacheArithIntrinsics`' rt fallback |
| 109 | +(bootstrap.zig:445-451). Migrate the ~48 `.clj` sites and 12 Zig emission |
| 110 | +sites (`rt/or` → `clojure.core/or`, `rt/deref` → `clojure.core/deref` — |
| 111 | +exactly what clj's own expansions emit). |
| 112 | + |
| 113 | +- Better than keep-rt-alive (b): (b) keeps `(find-ns 'rt)`, `(all-ns)`, |
| 114 | + `(ns-resolve 'rt '+)`, `(in-ns 'rt)` as observable divergences — the |
| 115 | + F-011 violation is only ~90% closed, and the env.zig:222 special case |
| 116 | + must stay. Full merge closes the class, matches clj's actual shape, |
| 117 | + fixes `ns-publics` of clojure.core as a free side effect, and deletes |
| 118 | + ~4 pieces of structural scar tissue instead of preserving them. |
| 119 | +- Breaks (concrete): (i) rebind-recursion hazard — core.clj wrappers that |
| 120 | + redefine a name while calling the rt builtin qualified (`re-find`, |
| 121 | + core.clj:2145) would rebind the same Var and recurse; enumerate |
| 122 | + mechanically (diff rt mappings vs post-bootstrap clojure.core mappings) |
| 123 | + and rename those to `cljw.internal/__…`. (ii) Tests: env.zig:919-968, |
| 124 | + primitive.zig:183-200, protocol.zig:957, core.zig:2033, math.zig:1391, |
| 125 | + macro_transforms.zig:4116, root_set.zig:891 — mechanical updates. |
| 126 | + (iii) Bootstrap order unchanged (registerAll already receives |
| 127 | + `clojure_core_ns`; clojure.core exists from Env.init). (iv) AOT: |
| 128 | + serialize.zig:295-300 bakes `var_ptr.ns.name` into var_ref constants; |
| 129 | + the embedded cache regenerates at build; external pre-change archives |
| 130 | + need a format-version bump (currently 3, serialize.zig:10). (v) |
| 131 | + placement.yaml Pattern B2 already interns private leaves directly into |
| 132 | + clojure.core — the merge generalizes the existing pattern. Diff is |
| 133 | + large (~60 sites + ledger/docs) — per F-002 not a counter-argument. |
| 134 | + |
| 135 | +### Alternative 3 — wildcard: Env-level double-key ns aliasing |
| 136 | + |
| 137 | +Register `"rt"` as a second `env.namespaces` key pointing at the same |
| 138 | +`*Namespace` as `"clojure.core"`, and swap the registration target. |
| 139 | + |
| 140 | +- Better: cheapest route to home-ns = "clojure.core" everywhere; all |
| 141 | + 48+12 qualified refs keep resolving. |
| 142 | +- Breaks: `Namespace.deinit` double-frees under two owning keys (needs an |
| 143 | + alias-aware ownership flag); `(find-ns 'rt)` returns clojure.core (a |
| 144 | + new divergence); `__` internals become clojure.core-homed → pollute |
| 145 | + `(ns-publics 'clojure.core)` and completion worse than today; the |
| 146 | + `re-find` recursion hazard fires immediately. A trap dressed as a |
| 147 | + shortcut — also the reason (b)'s "alias ns" sub-option is distrusted. |
| 148 | + |
| 149 | +## Affected files (planned) |
| 150 | + |
| 151 | +`src/runtime/env.zig`, `src/lang/primitive.zig`, |
| 152 | +`src/lang/macro_transforms.zig`, `src/lang/bootstrap.zig`, |
| 153 | +`src/eval/backend/vm.zig`, `src/eval/backend/tree_walk.zig`, |
| 154 | +`src/eval/analyzer/{analyzer,syntax_quote}.zig`, `src/eval/reader.zig`, |
| 155 | +`src/app/builder/serialize.zig` (version bump), |
| 156 | +`src/lang/clj/clojure/core.clj` + `java/io.clj` + `instant.clj` |
| 157 | +(rt/ → cljw.internal/ migration), affected unit tests, |
| 158 | +`data/placement.yaml` (Pattern B wording), `.dev/accepted_divergences.yaml` |
| 159 | +(cljw.internal AD row). |
| 160 | + |
| 161 | +## Revision history |
| 162 | + |
| 163 | +- 2026-07-15: Accepted (DA fork output embedded above). |
0 commit comments