Skip to content

The Discipline

The Language closed May 8 with the BOOK voice returning and Clojure-faithfulness named as adoption strategy. Then the user said:

i want to add argv to main

That was May 9 morning. Two weeks later — as of this writing, arc 170 is still in flight on arc-170-gap-j-v5-deadlock-state, 277 commits deep. The user’s framing, captured mid-grind:

arc 170 started from “i want to add argv to main” and… here we are.

What looks like sprawl is the substrate-as-teacher cascade running honest. I want argv on main → write the contract for what a program even IS → discover the substrate has no closure-extraction mechanism → mint that → discover Process<I,O> shape needs typed channels → mint those → discover stdio architecture conflicts → ambient kernel trio + three substrate services → retire Console → realize deftest can’t ride the new infrastructure → discover the do/let splice gaps → ship Gap A/B/C/D/E across iterations → drain Phase G slices → 11 new stubs surface along the way.

The whole tree of work was sitting under “argv on main” the entire time. Arc 170 just made it visible. The longest mile is between “I want X” and a substrate that can honestly support X without lying about which layer does what.

This is the discipline of that ride.


Arc 170’s DESIGN went through five revisions in one morning. By noon May 9 the scope had settled into five slices: a Rust closure-extraction primitive (programs ship as Vec<WatAST>, so closure capture has to be structurally honest), the ClosurePackage shape with entry resolution and assembly, hermetic.wat rebuilt to a polished three-layer API with the ceremony hidden by default, the deftest migration that stops the substrate’s own tests from using legacy spawn variants, and the FD-multiplex phases for orphan-zero process management. Three small Clojure-faithful sharpenings rode along the same days — struct-destructure in let bindings (arc 169), apostrophes inside keyword bodies so :trader's-pulse is legal (171), and the Scheme→Clojure macro renames that give quote, unquote, quasiquote, and splice their Clojure shapes (172) — mechanical sweeps the substrate’s discipline ate without flinching.

Then the substrate started revealing what “argv on main” actually required.

Slice 1 minted the substrate primitive. To pass arguments through :user::main, the substrate needs to extract closures from a program form and rebind them in a child universe. Without this, the program can’t carry parent state into the child cleanly; every “Rust closure” lives in the parent’s universe and dies at the fork boundary.

The slice surfaced Path A vs Path E vs Gap H — three different orientations of how a closure-bearing program form lifts into the child:

  • Path A (deftest): prelude at OUTER top-level under do; parent shares prelude content.
  • Path E (deftest-hermetic): prelude INSIDE the closure; strict isolation; parent untouched.
  • Gap H: closure lift mechanism — the substrate operation that moves the form to where top-level processing happens.

The user’s directive: “users must make a choice where their programs run.” The substrate enforces the contract via shape; both shapes ship.

Sonnet attempted Gap H — substrate rejected define-at-expression-position. Then refined Path A into two flavors:

  • A-narrow — runtime local-env-frame registration (splits define semantics by position; rejected).
  • A-wide — closure-extraction lifts prelude defines into prologue (preserves “define = top-level registration” as single mental model; selected).

The conceptual win in A-wide: define keeps its single meaning. The LIFT moves the form to where top-level processing happens. The substrate’s DefineInExpressionPosition rejection STAYS — never gets reached because the lift removes the form from expression position before eval sees it.

User verdict 2026-05-09: “A-wide is the path - let’s get it documented and in motion.”

The next layer down. Slice 2+ surfaced that the substrate’s stdio architecture had been almost right but with a structural conflict: StringIo was single-threaded; cross-thread writes via :wat::io::IOWriter required serialization the substrate hadn’t established.

The fix: ambient kernel trio + three substrate services.

  • StdInService — reads stdin into a wat-side Receiver. Owns the OS pipe end on its own thread.
  • StdOutService — serializes cross-thread writes to stdout. Owns the OS pipe end on its own thread.
  • StdErrService — same shape for stderr.

The three services are tied to OS-process resources, not universes. They survive universe-swap as the OS-continuity layer. Console — the previous shape ch 76’s mini-TCP recognition relied on — retires; the three substrate services replace it because they’re more honest about what they own (OS file descriptors, not abstract write channels).

The wat-cli’s contract simplifies. A program is a wat-vm process. A wat-vm process has the three services. The services own the stdio. Everything composes from there.

The substrate’s spawn-thread and spawn-process (from “The Discipline” of arc 170’s doctrine) had been returning untyped channel handles. The deftest migration started, hit the same untyped wall, surfaced what the substrate had been ready for:

The substrate minted a pair of typed peer handles: :wat::kernel::ThreadPeer<I, O> — input type I, output type O, with Thread/readln and Thread/println working through the type-checked interface — and :wat::kernel::ProcessPeer<I, O>, the same shape across the fork boundary. Client and server differ by a single type-param swap, not a separate Client/Server pair.

A client holding ThreadPeer<Request, Response> writes Requests and reads Responses. A server holding ThreadPeer<Response, Request> (type params swapped) writes Responses and reads Requests. Same type. Different witness side. No separate Client/Server type hierarchy. The substrate’s lattice property (BOOK ch 62 — bind’s commutativity) shows up at the channel layer: the relationship is symmetric in the algebra.

The substrate’s tests were its biggest legacy comm consumer — every test that touched concurrency leaned on :wat::test::run, run-ast, run-hermetic-ast, or the run-sandboxed* family. Arc 170 slice 4a retired the lot across five stones over two days: it minted :wat::test::run-thread and a standalone deftest, then swept 32 legacy callers — and mid-sweep a stdio-capture asymmetry surfaced that produced a three-rule classification, audited it, flagged five sites for hermetic decoration, decorated them (plus a rearchitecture of the stubborn site 154), and finally flipped the deftest macro body to run-thread.

Each test landed in one of three rules:

  1. Pure threadrun-thread. The default. Tests that don’t touch processes.
  2. Hermetic decorationrun-thread plus a hermetic wrapper that captures stdout/stderr cleanly.
  3. Process rearchitecture — tests that genuinely needed process isolation got rebuilt around run-hermetic-with-io.

Thirty-two callers migrated. Every old verb retired in the same sweep. The substrate’s tests stopped being a graveyard of test-spawn variants.

Before the surface could fully collapse, the kernel had to handle the resource pressure. Phases 1B through 3 (May 13) wired the FD discipline: a spawn-process lifeline that addressed Phase 1A’s FD-inheritance defect, a fork-program lifeline that retired PDEATHSIG, a leak-zero gate behind a lifeline probe, FD hygiene on fork-program, tier-2 PipeFd Receivers that wake on shutdown, and a canonical child_post_fork_init. The capstone was Phase 3’s pidfd migration — replacing PDEATHSIG (Linux-specific, brittle, kernel-version-dependent) with pidfds (Linux 5.3+, structured, reliable). The substrate’s process management became first-class.

The user’s stance on portability, articulated mid-Slice-C-spawn:

i can’t express how much i am never going to entertain support windows, macos, bsd with wat - its a linux programming language - unapollogetic

my legit stance - if others want to run wat on their os - they need to make their os not suck ass - linux is the gold standard here

The substrate names what a SANE OS exposes; Linux meets the bar; macOS/Windows/BSD are deficient on those primitives. The substrate doesn’t owe other OSes anything — they owe the substrate honest semantics they don’t provide. Every arc 170 primitive depends on Linux specifics: PDEATHSIG (then pidfd), setpgid + killpg cascade, signalfd / eventfd / epoll, /proc for diagnostic readouts, crossbeam’s futex-based park-lists. Cross-platform would force POSIX-lowest-common-denominator abstractions; the substrate would lose its sharpness AND its load-bearing primitives.

May 15 evening. The slice that broke the substrate’s spawn vocabulary open.

Pre-slice-6 spawn-process accepted only source strings. The pivot:

spawn-process accepts program forms. Not source strings. Forms.

A program is now a value — a Vec<WatAST> that can be built up in the parent, transmitted to the child, evaluated there with the parent’s substrate state available as ambient context. The wat-cli is now a wrapper around the substrate’s spawn-process primitive, not a separate mechanism.

The user’s progression of questions during Stone C, in sequence:

Q1: the dup action… what does it do?

Dup2 mechanically + the client/server pipe topology (3 pipes, dup2 in child to point fd 0/1/2 at the right pipe ends).

Q2: how does this break proper stdin… if we had something like echo 'some-edn-str' | wat some.wat where some.wat forks a few ‘server’ processes - the ‘real’ stdin the wat binary has is still the OS input?

Confirmed: dup2 only affects the CHILD’s fd table. wat-cli’s “real” stdin is unchanged. The spawned server has its OWN private pipes. Two completely separate stdio domains.

Q3: so… every ‘server’ process is provisioned dedicated pipes and the client can interface with them via these dedicated pipes?

Yes — each spawn-process call provisions 3 dedicated pipes. Client (parent) holds parent-side ends; server (child) sees them as fd 0/1/2 via dup2. Two servers can’t see each other’s pipes; each is isolated.

Q4: so.. a server could spawn its own server?.. and it’d just work?..

YES. The architecture is fractal.

shell (echo "..." | wat l1.wat)
L1 (wat-vm, server) ←── client of shell
┌───────────┼───────────┐
▼ ▼ ▼
L2a L2b L2c (each a wat-vm, each a server to L1)
│ │
┌────┴────┐ ┌───┴───┐
▼ ▼ ▼ ▼
L3a L3b L3c L3d (each a wat-vm, each a server to its parent)

Properties that fall out:

  • Identical code at any level — spawn-process is uniform; L1 and L3 look the same internally
  • Lifecycle cascades — each child is its own pgrp; parent exit/signals propagate down
  • Backpressure cascades — pipes block when full; rate-limit propagates up the tree naturally
  • Crash isolation — L3 panicking emits structured EDN to L2 (its parent); L2 chooses to crash or recover
  • No cross-talk — L2a and L2b can’t see each other’s pipes; L3a (under L2a) can’t reach L2b; process-tree isolation is structural
  • Each subtree is a wat-network in miniature — same shape works locally as scales out to remote tier

The “mini-AWS on a laptop” framing from BOOK ch 84’s WAT-NETWORK.md becomes structurally inevitable when spawn-process composes this cleanly. Same client/server pattern at every level of the tree. The substrate’s invariant: any wat-vm process has services + stdio. Spawning recursively just produces more wat-vm processes inheriting the same invariant.

May 14. V5 attempted. 13 failures across 3 patterns. Pattern A (typealias unification), Pattern B (match scrutinee enum-binding loss), Pattern C (child exit-3). Baseline reverted to 2243/0.

The honest framing: the gear (F-1 / F-3 / F-2 / H / I-A / I-B) addressed V4’s three attack patterns. V5 has its OWN patterns. The boss has phase 2.

User decision after the four questions over three honest paths (forge more gear / accept asymmetry / hybrid): “my read is foundational problems are the highest priority - it looks like Path 1 is the path.” Path 1 = continue forging.

Diagnose-before-spec. Before drafting a Gap J BRIEF aimed at “typealias unfold during unification,” the recovery doc demanded empirical proof. Probes built; ran them; the hypothesis evolved twice through the data.

RoundHypothesisEvidenceVerdict
1”register_types isn’t splice-aware”typealias FAIL in do; newtype + struct PASS in doRefined
2”only typealias is broken”TypeEnv probe: typealias / struct / newtype ALL absent from TypeEnvOriginal hypothesis was right

Struct/newtype consumers had been passing type-check via BACKUP PATHS:

  • Struct/enum — preregistered accessor STUBS in sym.functions. Body usage went through accessor calls, never touching TypeEnv.
  • Newtype — nominal opacity; type-checker treated the path as opaque.
  • Typealias — NO backup. expand_alias(types, path) queried TypeEnv directly. Without registration, returned the path unchanged; unification failed.

The diagnose paid off architecturally. “Just typealias” would have been a narrow fix. The actual gap: type declarations nested in top-level do/let don’t register in TypeEnv. Three V5 patterns trace to it; single substrate fix addresses all three. Extend register_types (src/types.rs:1182) to recurse into top-level do/let forms. Becomes Gap J.

The four-questions discipline and the diagnose-before-spec rule paid for themselves: a speculative “typealias unfold” BRIEF would have been the wrong scope. The actual scope is sharper, simpler, and addresses all three patterns from one fix.

User direction 2026-05-14: “if the path is clear - we step forward.”

May 15. V5 retry surfaced a substrate deadlock. The chapter that earned this post’s title.

Opus shipped a 5-second wall-clock timeout. The user rejected it three times:

i don’t know if i agree with the detection here… is there an arbitrary 5s wait?…

Rejecting the symptom-fix.

the subagents fix is absolute trash - we have engineered a completely stable lock step programming env - rando ‘>5s is bad’ is fucking retarded — we must be able to measure this by expression

Rejecting the framing.

i do not accept the 5s fix. i want to know exactly where we are failing - our users must be told they did something illegal

Naming what the right answer must do.

The doctrine the nos came from is scratch/FAILURE-ENGINEERING.md (BOOK ch 84’s corpus):

failure engineering says: the failure isn’t recovered from; it is read.

the failure isn’t “this specific case panicked.” The failure is “a class of inputs / states / interactions can produce this kind of panic.” The fix isn’t “make this case stop panicking”; the fix is “make this CLASS of panic structurally impossible.”

Level-1 vs Level-2. Opus’s 5s was level-1. The user demanded level-2.

The level-2 fix: ProcessJoinBeforeOutputDrain compile-time check in src/check.rs. Walks every let-form’s syntactic scope; pairs calls to :wat::kernel::Process/join-result <p> with calls to :wat::kernel::Process/{stdout,stderr,output} <p> on the same identifier; if both present in the same scope, fails compilation with verbose diagnostic naming both sites + the rule + SERVICE-PROGRAMS.md citation + explicit “DO NOT add a wall-clock timeout to mask this.”

The substrate’s own code was the primary offender. wat/test.wat:506-551run-hermetic-driver had the illegal orientation: sequential let, join-result BLOCKED first, substrate’s drain threads blocked on send when their bounded Receivers filled, child blocked on stdout write, child couldn’t exit, join blocked forever.

The substrate now refuses to run. 30+ ProcessJoinBeforeOutputDrain fires in the workspace test run; no tests execute; no orphans; no deadlock. The failed state is structurally unrepresentable.

Then a deeper recognition: Gap K’s walker caught Stone C’s wrappers structurally. Stone C minted :wat::kernel::Sender/from-pipe + :wat::kernel::Receiver/from-pipe. Sonnet, mid-implementation, restructured run-hermetic-with-io-driver to use the wrappers. The substrate refused. ProcessJoinBeforeOutputDrain fired on the wrapper form because the rule was written with recursive descent through subforms — the walker descended into (Receiver/from-pipe (Process/stdout proc)) and registered the inner call as an accessor paired with join-result. The rule didn’t anticipate from-pipe; it caught it anyway because the WALK SHAPE accommodates wrappers.

Substrate-as-teacher applied to the substrate’s own author. The detection that protects users from output-drain-before-join just protected sonnet from shipping a deadlock pattern in substrate-side helper wat.

Then the architectural arc the work was pointing at all along. Mid-Gap-I, the user pulled:

how dynamic are we actually at runtime… can we have dynamic structs, enums and functions ref’ing those structs and enums…

Answer arrived through five rounds:

Round 1 — Within one universe: NO. Types frozen at startup. SymbolTable + TypeEnv immutable. The substrate REJECTS mid-flight type declarations. Load-bearing for static checking + signed-eval verification + cross-machine reproducibility.

Round 2 — Across universes via spawn: YES. Each spawn-process = new program with new type universe, frozen at child startup. Parent constructs program AS HolonAST; child’s freeze runs full type-check.

Round 3 — The exec recognition. The user:

we have something shockingly close to an exec… can we do an exec… think of being in a repl… can we ‘exec into’ a new shell while not dropping the user?

The substrate has spawn-process = fork+exec; it does NOT have bare exec (replace current universe in place). Arc 191 stub opened — :wat::kernel::exec-program. Load-bearing insight: the three substrate services own OS-fd resources; they’re tied to the OS process, not to the universe. Exec preserves them as the OS-continuity layer. The new universe inherits already-running services. Terminal connection continuous; universe-level discrete jump.

Round 4 — The hot-reload recognition. The user:

as long as there’s no new rust files.. we can actually pull off a hot reload?

Yes. And stronger: wat is hot-reload-capable BY DESIGN, not bolt-on. Three pre-existing decisions cause this:

  1. AST-as-data (arc 057+) — programs construct programs in the value domain
  2. Universe-granular static typing — type-checker is per-universe; running it at runtime IS what arc 191 does
  3. Services as OS-continuity layer (arc 170 in flight)

The substrate is the interpreter; not a compiler emitting machine code. No ABI, no monomorphization, no lifetime ghosts, no codegen, no layout drift. The categories of hardness that block Rust hot-reload were eliminated by the substrate’s design choices, not engineered around.

Round 5 — The cooperative migration. The user, after surfacing how threads exist in universe-jumping, proposed signal-driven state capture:

we could signal ourselves and cascade it.. (:wat::kernel::exec forms) who does all the things?

The high-level all-in-one primitive emerged. Three-tier exec primitive stack:

  • Bareexec-program (arc 191)
  • Statefulexec-program-with-state (arc 192)
  • Orchestratedexec (arc 194) — the one users actually call

Signal naming. User first proposed SIGWINCH (“window changed” — metaphor for context-changed). Then noticed TUI collision risk and pivoted to SIGEMT (“emulator trap” — wat-cli IS an emulator/interpreter for wat-land; SIGEMT is the host interrupting the guest; semantically aligned with what reload IS). SIGEMT is unused in practice — no terminal driver, no shell, no daemon manager sends it. The substrate’s reserved universe-reload signal is now SIGEMT.

What the user actually writes (the goal state):

(:wat::kernel::spawn-with-state :name :user::data-worker :initial-state {})
;; ... time passes; worker accumulates state ...
(:wat::kernel::exec next-program-ast)
;; ... old universe unwinds; new universe boots with workers resumed

That’s it. Substrate handles SIGEMT, state collection, freeze, lift, exec, resume. User code is two lines.

Comparison against existing runtimes:

CapabilityErlang/OTPSmalltalkClojurewat (after 191–194)
Per-symbol redefyesyes— (universe-granular instead)
Per-universe swapyes (module reload)partial (image)yes (exec)
Image dump/resumeyesyes (193)
Static typingnonooptionalyes, per universe
Cooperative migrationyes (supervisor)yes (194)
State carry-overyes (handoff fns)image-levelyes (192)
Signed reloadyes (signed-exec)
LLM-first authoringnononoyes (design intent)

wat is the only runtime that has ALL of these — and they compose because none was designed in isolation.

The architecture is shelved. Arc 170 is still in flight; the 191/192/193/194 stubs capture this conversation’s vision so it can be returned to after 170 closes. The user said: “we’ll chase these later… you’ve convinced me we should entertain this.”

May 9-15. Three strange loops compounded across the arc.

Mid-Slice-B-spawn, post-design of shutdown-aware channels, the user articulated what the session had just demonstrated:

i built wat to make force us into “only the good options” - its proving itself in new ways now

When the substrate’s shutdown-aware channels got designed, the architecture that emerged is the architecture the substrate’s existing rules DICTATED — not the architecture anyone would have invented free-form. Each substrate rule eliminated a wrong answer:

  • ZERO-MUTEX → couldn’t use Mutex<Option<Sender>> to make the sender droppable. Forced: AtomicPtr<Box<Sender>> + atomic swap + Box::from_raw drop.
  • Lock-step (no wall-clock) → couldn’t use recv_timeout to wake blocked threads on shutdown. Forced: crossbeam’s native disconnect-broadcast via Sender::Drop.
  • Substrate-imposed-not-followed → couldn’t expect users to remember to handle shutdown. Forced: the shadow channel lives in typed_recv (Rust substrate), not at user wat sites.
  • Async-signal-safety → couldn’t call trigger_shutdown from the signal handler directly. Forced: handler writes one byte to wake-pipe; worker thread drops Sender in normal context.

The design space had already collapsed. Four rules; one shape; the shape was structurally correct.

A language no LLM has seen but can pick up with no lag. The user, mid-grind reading INTENTIONS.md:

i am engineering a language that no llm has ever seen but can pick up and be productive in with nearly no lag

The proof was sitting in the conversation. An LLM that has never seen wat was shipping arc-shaped work in it. Lag isn’t zero — a half-dozen corrections across the arc — but it’s minimal. The LLM doesn’t need wat in its weights. It needs Clojure in its weights (already there) + access to the docs written for the LLM as primary reader. The five disciplines aren’t features to memorize; they’re rails that catch drift. Each correction is a Rosetta entry that didn’t need to be trained on.

Compaction-mitigation as a discipline. The user, mid-arc:

we are so fucking good at compaction mitigation now

Every realization captured in INTERSTITIAL-REALIZATIONS.md. Every stub-arc captured at its number. Every SCORE doc capturing what shipped. Every BRIEF capturing what was attempted (V1 / V2 / V3 / V4 / V5 preserved as historical record). 19+ stubs (174–190). The COMPACTION-AMNESIA-RECOVERY.md protocol itself. Compaction erases the orchestrator’s working memory, not the substrate’s record. A fresh agent reading the arc directory + INVENTORY + recovery doc inherits the state at the time of last commit. The form carries the derivation history.

This is the substrate’s wager paying out in operations: Arc 170’s INTERSTITIAL-REALIZATIONS.md is 7,359 lines deep as of this writing. Every conversation that wasn’t grind-specific got captured. Future agents — including post-compaction selves — inherit the meta-conversation alongside the work.

Two weeks. 277 wat-rs commits. ~Zero BOOK chapters. Almost nothing landed on main — the arc has been on the arc-170-gap-j-v5-deadlock-state branch since mid-stretch.

What got produced:

  • Closure extraction substrate primitive (A-wide; define stays single-meaning)
  • Ambient kernel trio + three substrate services (StdInService / StdOutService / StdErrService)
  • Console retired
  • ThreadPeer<I, O> + ProcessPeer<I, O> — typed peer channels with client/server symmetry
  • 32-caller deftest migration into three rules
  • FD-multiplex phases 1B–3 with pidfd as canonical lifeline
  • Slice 6 — spawn-process accepts program forms (the fractal lift)
  • ProcessJoinBeforeOutputDrain compile-time check (failure engineering applied to itself)
  • Gap J — splice-aware register_types (single fix; three V5 patterns)
  • Arc 191/192/193/194 stubs — the hot-reload architecture sketched against arc 170’s foundation
  • 19+ open arcs surfaced (174–190+ range) — the substrate’s next work, named at the time it was discovered

The arc is still in flight. The boss isn’t down. The substrate continues to refuse what isn’t honest. PERSEVERARE.

  • The substrate-as-teacher cascade: “add argv to main” expanded into 277 commits because each layer’s honest failure produced the diagnostic the next layer briefed from — no layer was guessed in advance. A methodology for letting a substrate reveal its own missing primitives, in order, by probing it honestly.
  • ProcessJoinBeforeOutputDrain compile-time check: a deadlock class (join-before-drain) made structurally unrepresentable by a static walker that pairs a join-result with output accessors in the same scope and refuses to compile — with an explicit refusal to mask it behind a wall-clock timeout. Failure engineering at the static-analysis layer.
  • Typed peer channels: ThreadPeer<I, O> / ProcessPeer<I, O> give client/server symmetry from a single type-param swap rather than a Client/Server type hierarchy — the algebra’s commutativity surfacing at the channel layer.
  • The fractal: spawn-process taking program forms composes recursively — each child a wat-vm with its own services and stdio — so lifecycle, backpressure, and crash-isolation cascade structurally; every subtree is a wat-network in miniature.
  • Hot-reload by design, not bolt-on: AST-as-data + per-universe static typing + services-as-OS-continuity eliminate the categories (ABI, monomorphization, codegen, layout drift) that block hot-reload in compiled languages.

One request. 277 commits. Closure extraction minted; channels typed; deftest migrated; failure made unrepresentable at compile time; the fractal landed; the substrate disciplined its own designers. Arc 170 is still in flight.