11# 0020 — Generated namespaces: the ergonomics layer, as a file
22
3- ** Status:** proposed · 2026-07-30 · the shape ` 0015 ` accepted for later, picked
3+ ** Status:** accepted · 2026-07-30 · the shape ` 0015 ` accepted for later, picked
44up now that its two blockers — marshalling and host imports — are gone.
5+ Rewritten after an adversarial review: D inverted (the throwing variant lost),
6+ C gained the per-interface exit its collision rule needed, and the sha moved
7+ from the component bytes to the API. This layer ** opens S2** — its option keys
8+ are ` cljwit.edn ` 's keys, so the file becomes the edn's implementation rather
9+ than its rival — and S1 closes without it.
510
611## The question
712
@@ -29,9 +34,20 @@ file looks like.
2934is a dev-time act that reads a component's type, and requiring it in
3035production buys nothing. The generated file is ordinary checked-in source —
3136reviewed in diffs, seen by clj-kondo, clojure-lsp, and CIDER exactly because
32- it is not clever. Its header names the source component, its sha-256, and the
33- form that regenerates it, so drift is visible where drift is looked for: in
34- version control.
37+ it is not clever. Its header names the source component, a sha-256 ** of the
38+ ` describe ` output** — the API, not the bytes, so a toolchain bump that
39+ changes the ` @producers ` section without changing a signature does not churn
40+ the header — and the form that regenerates it.
41+
42+ ** What the file cannot do is notice it is stale.** Nothing reads the header
43+ at run time; a ` .wasm ` swapped under an unregenerated file produces no diff
44+ anywhere. The * loud* half is already covered — marshalling is rebuilt from
45+ the live component at instantiate, so a renamed export fails
46+ ` :no-such-export ` with a did-you-mean and a changed argument list fails
47+ ` :wrong-arity ` . The silent half is exactly the regenerate-and-diff check
48+ that ` cljwit.edn ` (S2) owes its components; the API hash makes it a string
49+ compare. Until S2 exists, staleness detection is the reviewer's, and this
50+ paragraph is the warning.
3551
3652### B. Every var takes the instance first
3753
@@ -44,13 +60,20 @@ version control.
4460
4561` 0015 ` measured why ambience loses: ` binding ` conveys to exactly the paths
4662that race (` future ` , ` pmap ` ) and a lazy seq realised under a second instance
47- runs against the second silently. The explicit argument is not a compromise —
48- it is what makes handing an instance to a ` future ` a visible act. Arity and
49- names now exist statically: ` (thumbnail i img) ` is a clj-kondo warning and an
63+ runs against the second silently. The explicit argument is what makes handing
64+ an instance to a ` future ` a visible act. Arity and names now exist
65+ statically: ` (thumbnail i img) ` is a clj-kondo warning and an
5066` ArityException ` naming ` acme.resize/thumbnail ` , not a gensym (` 0015 ` 's
5167macro-arity finding was about interned gensyms; a named ` defn ` does not have
5268that problem).
5369
70+ Two warts, named rather than hidden. A resource method takes ** two**
71+ contexts — ` (counter-bump i c 5) ` — and the signature permits a handle from
72+ instance A with instance B. The runtime catches that mismatch when lowering
73+ the handle (` :wrong-instance ` , since a ` Handle ` carries its context), which
74+ is a call-time error, not the visible act B advertises for instances; the
75+ docstring of every method var says which instance the handle must come from.
76+
5477### C. Names: the last WIT segment, and a collision is an error
5578
5679The exact WIT string stays the runtime key inside the generated body — ` 0014 `
@@ -65,38 +88,72 @@ WIT's grammar demands it:
6588| ` …#[method]counter.bump ` | ` counter-bump ` |
6689| ` …#[static]counter.reset ` | ` counter-reset ` |
6790
68- Two exports that munge to one name — two interfaces exporting ` run ` , say —
69- ** fail generation** with both WIT names in the message and a ` :rename ` option
70- (` {:rename {"pkg:a/x@1.0.0#run" 'x-run}} ` ) as the exit. Silent suffixing
71- (` run-2 ` ) would bake an iteration order into an API; a namespace per
72- interface multiplies files for a collision that ` zoo.wit ` — this repo's
73- worst-case shape — does not have. Deterministic failure with a named exit
74- follows ` 0014 ` 's nil-on-typo rule.
91+ The stripping ** collides by construction** — the same finding that made
92+ ` 0014 ` B keep exact WIT strings at the key level. Both shapes were built and
93+ verified against the pinned wasm-tools during this note's adversarial
94+ review: a world exporting ** two versions of one interface**
95+ (` acme:api/svc@1.0.0#run ` / ` …@2.0.0#run ` — every function collides
96+ pairwise), and a single legal interface where ` [method]counter.bump `
97+ strips to the same name as a plain export ` counter-bump ` .
98+
99+ So a collision ** fails generation** with the colliding WIT names in the
100+ message, and there are two exits at two granularities:
101+
102+ - ** ` :interface ` ** — generate one namespace per interface:
103+ ` (gen/write-ns! wasm {:ns 'acme.api.v1 :interface "acme:api/svc@1.0.0"}) ` .
104+ One option per interface, stable across regenerations. This is the exit
105+ for the two-version world, where per-function renames would cost
106+ O(functions) and have to be re-supplied verbatim forever.
107+ - ** ` :rename ` ** — ` {:rename {"t:c/i#counter-bump" 'counter-bump2}} ` , for a
108+ leaf collision inside one interface.
109+
110+ Silent suffixing (` run-2 ` ) stays rejected: an API whose names depend on
111+ reflection order breaks on the component author's next reorder.
112+ Deterministic failure with a named exit follows ` 0014 ` 's nil-on-typo rule.
113+
114+ A generated name that shadows ` clojure.core ` — ` count ` , ` map ` , ` get ` are
115+ all legal WIT identifiers — gets ` (:refer-clojure :exclude [...]) ` computed
116+ into the ` ns ` form, because the measured alternative is a load-time warning
117+ per consumer and clj-kondo exiting 2, which takes this repo's own gate red.
118+
119+ ### D. One var per export, tagged — and ` unwrap ` lives in the library
120+
121+ ** Amended in review: the first draft generated a throwing ` (f …) ` plus a
122+ tagged ` (f* …) ` , and that inverted ` 0012 ` .** ` 0012 ` 's accepted decision is
123+ that the tagged value * is* the mapping and throwing is opt-in sugar; giving
124+ the throwing form the short, completion-first name is opt-out, not opt-in —
125+ and it hands ` (map #(f i %) coll) ` exactly the laziness hazard ` 0012 `
126+ documents. It also encoded a type-level fact in the file: a stale throwing
127+ wrapper over an export whose return stopped being a ` result ` fails in
128+ undefined ways, silently.
129+
130+ So: ** one var per export, returning the value as ` 0012 ` maps it** — tagged,
131+ for a ` result ` return — and one function in the runtime library:
75132
76- ### D. ` result ` in return position gets ` 0012 ` 's sugar here
133+ ``` clojure
134+ (host/unwrap (add i 2 3 )) ; ok payload, or ex-info with :wit/error
135+ ```
77136
78- This is the layer ` 0012 ` deferred to: ` (f i args) ` returns the ok payload and
79- throws ` ex-info ` with the lifted error under ` :wit/error ` (the shrunk
80- contract — no type name); ` (f* i args) ` returns ` [:ok v] ` / ` [:err e] `
81- untouched. Two vars, generated together, only for exports whose * return* is a
82- ` result ` . Everywhere else one var, no throwing — ` 0012 ` 's laziness argument
83- stands.
137+ Opt-in at the call site, zero generated duplication, and the sugar cannot go
138+ stale because it is not generated.
84139
85140### E. Reflection moves to the artifact, and becomes public
86141
87- Generation needs exports and signatures without instantiating, and users have
88- asked the same question of instances (` host/exports ` , ` host/signature ` ). The
89- walk that ` instantiate ` does over the component type is extracted and exposed:
142+ Generation needs exports and signatures without instantiating. The walk that
143+ ` instantiate ` does over the component type is extracted and exposed:
90144
91145``` clojure
92146(host/describe artifact)
93147; ; => {"thumbnail" {:params [["img" {:kind :list :element :u8}] …]
94148; ; :result {:kind :list :element :u8}}, …}
95149```
96150
97- ` describe ` is useful on its own — a REPL user's first question about an alien
98- ` .wasm ` — and it is the single source the generator consumes, so the
99- generated file can never disagree with what ` instantiate ` will find.
151+ ` describe ` is the single source the generator consumes, so the generated
152+ file agrees with what ` instantiate ` finds ** at generation time** — staleness
153+ after that is A's problem, not E's. Docstrings render these trees in WIT
154+ syntax, and they are ** structural** : reflection has no declared type names
155+ (` 0012 ` 's amendment), so ` take-flags: func(p: perms) -> perms ` documents as
156+ ` flags{read, write, exec} ` , never as ` perms ` .
100157
101158## Why
102159
@@ -118,10 +175,15 @@ generated file can never disagree with what `instantiate` will find.
118175- ** Interning vars at instantiate** (` intern ` from a plain function). Vars
119176 appear at run time, so clj-kondo and lsp are exactly as blind as with the
120177 macro, minus even the macro's syntactic marker.
121- - ** A namespace per WIT interface.** Buys collision-freedom ` zoo.wit ` does
122- not need, at the cost of a file explosion and an import dance for the
123- common one-interface world. Reopen if a real component's interface count
124- makes the flat namespace unreadable.
178+ - ** A namespace per interface as the only mode.** The common one-interface
179+ world would pay a file explosion for nothing. It survives as the
180+ ` :interface ` * exit* instead — the first draft rejected it outright, on the
181+ strength of ` zoo.wit ` not colliding, which is this project's recorded
182+ failure shape: checking the artifact in the repo, not the artifact the
183+ design is for.
184+ - ** Two generated vars per ` result ` export** (the first draft's D). Inverted
185+ ` 0012 ` , doubled the surface, and created the one staleness mode the
186+ runtime cannot catch.
125187- ** Silent collision suffixes.** An API whose names depend on reflection
126188 order breaks on the component author's next reorder.
127189- ** Emitting ` defprotocol ` /` definterface ` for call-site speed.** ` 0013 ` :
@@ -132,9 +194,11 @@ generated file can never disagree with what `instantiate` will find.
132194
133195- A WIT export name whose last segment is not a readable Clojure symbol
134196 after the munge table above — the generator refuses it today; a real
135- component hitting that refusal reopens C.
136- - A component whose regenerated file churns on every toolchain bump (sha in
137- the header making diffs noisy) — would argue for dropping the sha line.
138- - clj-kondo flagging the generated shape itself — would demand a header
139- directive, and is checkable in this repo's own gate by generating against
140- a fixture and linting the output.
197+ component hitting that refusal reopens C. (Names that * shadow* — ` count ` ,
198+ ` map ` — are already handled by the computed ` :refer-clojure :exclude ` .)
199+ - A real component whose interface count makes even ` :interface ` -split
200+ namespaces unreadable — reopens C's granularity.
201+ - clj-kondo flagging the generated shape itself — the gate generates
202+ against a fixture whose exports include a ` clojure.core ` shadow and a
203+ ` result ` return, and the review measured arity warnings working through
204+ exactly this shape.
0 commit comments