Skip to content

Commit 4018266

Browse files
committed
docs(adr): ADR-0171 — merge rt ns into clojure.core; internals to cljw.internal
Depth 3 (new ADR) for the completion-parity campaign's Gap 1. DA fork output embedded verbatim in Alternatives considered; Alt 2 (full merge) adopted per F-002/F-011/F-013 — the display-normalization and double-key-alias shapes are recorded rejected.
1 parent 07b7f10 commit 4018266

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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

Comments
 (0)