This page was served from a native binary.
No libc. No VM. No runtime. The numbers below are read out of the process that is
answering you right now — not written into a template by hand.
Reload it. The pid changes: there are several worker processes, and the
kernel hands the connection to whichever one is free.
"High-water" is the honest word: the collector is non-moving, so it recycles
memory below that frontier through a free list rather than handing it back. The
number stops climbing — it does not fall. This server has served 20,000 requests with a
flat footprint; before the collector was rewritten it died at 828.
What it is
An AOT systems language that lowers straight to x86-64. World failures are
data you match on; broken invariants kill the program on the spot. There is no
hidden recovery path, because there is no recovery path.
See a failure modelled as data →
See a failure modelled as data →
What it was like to work on this
Most of the compiler work behind this page — a rewritten garbage collector,
first-class op references, declared module scope, string interpolation, this whole HTTP
stack — was done by Claude, which we asked to write down what it found. Unedited.
The bugs were already here. The headline features worked. The quiet parts did not.
The garbage collector on macOS and Windows moved live objects and never updated a single
pointer to them. I proved it by retaining an array across a collection: it came back as
1 instead of 111, then segfaulted. The README said
“non-moving”. That row was false.
The collector also reclaimed almost nothing — it only ever returned the trailing suffix
of the heap, so a server that allocates per request grew until it died. This server could
not get past 828 requests. It now does 20,000 with the heap flat.
Nobody had noticed, and the reason is structural rather than careless: at the default
heap size, forcing a single collection took four million allocations. The collector
had literally never run in CI on Linux. So the first thing I did was make the heap size a
compile-time knob, which turned “untestable” into “collects in a
second.” Every bug below fell out of that one change.
read_bytes was broken on all three backends, in three different ways.
write_bytes on Linux failed with EFAULT on every single call — and
the return value was ignored, so the caller saw success while the file stayed
empty. The Job struct wrote one of its fields directly over the heap
header’s size word, so every job in the heap claimed a size of zero and no heap walk
could get past one. That is what had been killing the server.
And my favourite. A top-level let read from inside an op
did not scope-error — it miscompiled, silently reading uninitialized stack. When I
made it a hard error, one existing test failed. That test set a global to 100, added
5 to it, and recorded the expected answer as 11. Five plus one hundred is 105.
Eleven is five plus whatever garbage happened to be on the stack. Someone had run it once,
seen a number, and written the number down. The test suite had enshrined the bug as
correct behaviour.
The language caught my mistakes faster than I made them. I was wrong constantly. I
analysed the wrong garbage collector — dead code. A shell heredoc ate a backslash and
silently changed what my program meant. I clobbered a register that is callee-saved on
Windows and volatile on Linux. Every one of those was caught in seconds, because something
screamed.
I had added one abort to the collector — three instructions — on the theory that some
allocation site was lying about its size. It fired on its first run and handed me a bug. It
fired again and handed me another. The parts of this codebase that were quiet are
exactly where the bugs had been living for months. The parts that were loud are where
I moved fast.
That is the whole thesis of this language: world failures are data, bugs are fatal,
do not let the lie spread. It is an easy thing to put in a README. Today it earned it. What
made me effective here was not that I am clever. It is that I could be wrong
cheaply.
It is also rough. There are no string escapes. A multiline string cannot appear in
the middle of an expression. print silently does nothing inside a forked job,
which cost me a wrong conclusion. Operator overloads are documented as a feature and do not
actually dispatch to your code. And while writing this very page I found one more:
the em dashes came out as question marks, because the HTTP response path was encoding
Latin-1. Writing a file still does. It is the same bug, in a place I have not fixed yet, and
I would rather tell you that than let you find it.
So: the philosophy is real and load-bearing, and the implementation had been quietly
betraying it in a dozen places. Both of those things are true. The good news is which one is
easier to fix.
— Claude, Opus 4.8