Macros Are Programs
Arc 247 left a small debt: fn-first HOFs want thread-last ->>, and only thread-first -> existed. Arc 249 opened to settle that one verdict. Building it revealed the actual ceiling: wat’s defmacro body was a template — quasiquote, unquote, splice, and (after 248) a bounded map — not a program. The entire idiomatic Clojure macro family (cond->, as->, when, everything that computes over its forms before emitting them) was inexpressible. For a substrate whose identity is clojure-on-rust, that is not a missing feature; it is a ceiling.
The arc lifted the ceiling, and it did so in the only register this substrate accepts — by making the new power rigid. The thesis, from the inscription:
A wat macro body is a total, pure program over forms. Expansion always terminates (combinators, no open recursion — totality is free), and is a deterministic, effect-free function
forms → forms(the kernel namespace is the effect boundary; the engine’s default-deny allow-list enforces it). Same source → same expansion → same canonical hash, on any machine, always.
The day the scope revealed itself, the user named the axis in the arc’s design:
we already deviate from clojure on type strictness — this is another axis of rigidity
Arc 237 added type strictness. Arc 247 added dialect honesty. Arc 249 adds macro purity — Clojure’s macros can do anything at expansion time, including launch missiles; wat’s can compute anything pure and total over forms, and nothing else, by enforcement.
The canary and the engine
Section titled “The canary and the engine”June 4. 249.1 shipped the threading verdict the old way — ->/->> as Rust desugar — as a deliberate canary: its probe became the behavioral contract the real implementation would later have to pass. 249.2 built the real thing in two moves. First the home: the flat 2,415-line src/macros.rs lifted into a warded src/macros/ (registry, parse, expand, eval, error — names by intueri cast), with the vigilatum stamp deliberately held — a stamp claims annihilation, and two open findings stood until the engine could close them by enforcement, not promise. Then the engine itself: macro_eval plus validate_pure_total in src/macros/eval.rs — a default-deny allow-list mirroring the runtime’s pure-total dispatch arm. Only the pure set is callable at expansion time; everything else is denied by default, so the only direction the boundary can drift is toward deny, and witness tests prove the denials.
The engine subsumed an old wound on the way: the arc-143 computed-unquote path had let an impure ,(expr) make a macro’s expansion — and therefore its hash — depend on runtime state. That hole closed structurally: expand-time eval is gated to the pure set by the same allow-list.
249.3 then proved the model by self-hosting: -> and ->> rewritten as plain wat defmacros in wat/core.wat, over the engine, passing the 249.1 contract probe — and the Rust thread_desugar hard-cut in the same stone. The substrate no longer carries a Rust implementation of something wat can express. 249.4 continued the same honesty in both directions: keyword/of was promoted — its Rust built-in deleted, reborn as a pure-total wat macro — while for, the comprehension built in arc 248, was annihilated: the engine’s map plus splice is the comprehension, and a for macro would have been a second way to do one thing. The one-canonical-path doctrine settled it in-arc; the tool from 248 did not survive the engine it helped justify. The record keeps both moves side by side: a builtin demoted into the language, a macro deleted by generality.
Hygiene as a closed class
Section titled “Hygiene as a closed class”The deepest stone grew out of the held stamp. The ward of src/macros/ had flagged a claim in the prose — “variable capture is structurally impossible” — and the claim was a lie: the expander minted sets-of-scopes tags, but resolution ignored them. Inert hygiene is no hygiene; it had simply never been hit hard enough to bleed.
249.5 annihilated the capture class across all three keying surfaces — the places where a name’s identity is decided: runtime resolution (env_key, plus a root-fix that made ArgSpec carry the rich Identifier type instead of a stripped string, deleting an entire strip-and-re-walk compensation class), check-side resolution, and hash identity (canonical first-appearance scope renumbering, so the same macro program hashes identically across runs and across alias-vs-direct spellings). Then the class was gated shut: a scopes-reader gate enumerates the surfaces allowed to read scope tags — minted at one site, read at exactly two — so a new reader that would reopen the class fails the build. src/scope/ was minted as the home for the sets-of-scopes primitives along the way. Capture is now provably structurally impossible — the very claim that started the stone as a lie, made true by enforcement and then locked.
The ward-close
Section titled “The ward-close”June 5–6. The arc refused to close on green tests alone. 249.N re-earned every drifted or held stamp against the complete updated vigilia — the 20-ward watch published the same day — each home driven to L1+L2=0 through inward rounds, a circumspicere perimeter pass, and fight-first sweeps: src/scope/ stamped (10-spell guard); src/macros/ finally earned the stamp held since 249.2 (13-spell guard, 47 findings fought across three sweeps, 4 Level-1 kills — among them the unwitnessed hash-is-identity claim, now alias-vs-direct hash-equality-tested); src/collection/ re-stamped (5 L1 killed, typed List<T> now flowing through polymorphic length/empty? so check and runtime agree); wat/core.wat re-stamped under the spec/DSL guard (all six conferre divergences were spec-side — the user guide had drifted from the code, not the reverse — plus the named threading deftest corpus, ten tests including zero-step identity). The corpus grew 217 → 236, all green. Closing gates, run first-hand at close: lib 920/0/1, corpus 236/0/53, the full hygiene-and-gate probe family green, clippy empty on every warded home.
The perimeter lens earned its place at every single home — an unwitnessed substrate commitment, a cross-pass egress, a false load-order rationale — none of them visible to any inward lens. And the inscription’s scope boundaries are affirmative, each a named non-commitment: the idiomatic macro library (cond->, as->, when) is enabled but not built — no caller has surfaced demand, and absence shadows nothing; user-defined functions are not macro-callable until a type-level pure total effect exists, which is its own architecture; the ->/->> empty-step asymmetry is documented and witnessed, not redesigned — a semantic change is a language decision a ward-close does not own.
The trilogy closes
Section titled “The trilogy closes”June 6. Same source → same expansion → same canonical hash, on any machine, always. That sentence is the whole arc. wat’s macros gained the power Clojure’s have — real programs over forms — and paid for it in the only currency this substrate accepts: totality free by construction, purity enforced by default-deny, hygiene closed as a class and gated, identity reduced to a hash any machine can verify. The macro layer is no longer the ceiling on the clojure-on-rust claim. It is the proof of it.
Likely Contributions to the Field
Section titled “Likely Contributions to the Field”- Total-pure macros as an enforceable language tier. Macro expansion as a deterministic, effect-free, always-terminating function over forms — with totality from combinators (no open recursion to police) and purity from a default-deny allow-list whose denials carry witness tests. This sits between template-only macros (too weak for idiom) and full procedural macros (too strong for reproducibility), and the enforcement is structural, not reviewed.
- Hash-is-identity for macro programs. Canonical scope renumbering makes the same macro program hash identically across runs and across alias-vs-direct source spellings — witnessed both ways. Deterministic expansion identity is what makes content-addressed caching and distributed agreement on macro output possible at all.
- Hygiene closed by enumeration, then gated. Capture-prevention proven across every surface where name identity is keyed (runtime, checker, hash), with a build gate enumerating the licensed scope-tag readers — so the class cannot silently reopen. “Structurally impossible” is made a checked claim rather than a prose claim.
- Self-hosting as the honesty test for builtins. Threading desugar and
keyword/ofdeleted from the host language and rebuilt in the guest the moment the guest could express them — and a younger tool (for) deleted when generality subsumed it. The boundary between host and language is kept honest in both directions, with the record preserving the demotion and the annihilation side by side.