Reading Its Own Output
The third tool read binary files, and the binary it was pointed at was the compiler's own WebAssembly output — the language reading the very bytes it emits. The gap this was meant to expose was binary handling, and measuring it precisely mattered: the buffer already held every byte safely, and only one length function, built on C's string length, truncated at the first embedded zero. The fix was a single new builtin, not a whole new type.
The third tool read binary files, and there was one binary it was especially fitting to
read: the compiler’s own WebAssembly output. Point the compiler at itself in one mode and
it emits a .wasm file; point this new tool — mwasm — at that file and it reports the
magic number, the version, and the list of sections inside. The language would be reading
the very bytes it emits. That closes a loop worth pausing on: a compiler that can read its
own output is a compiler that can, in principle, check its own work, and building the reader
is the first step toward that. The stated gap was binary handling — the string type had
always been built for text — and a format parser was the honest way to walk into it.
Measuring the gap before naming it
Here the discipline of measuring instead of guessing paid off most sharply, because the obvious guess was wrong. “The language can’t handle binary” would have suggested a large project: a new bytes type, new operators, a whole parallel set of facilities. But reading one of these files and probing what actually happened told a much narrower story. The interpreter was already completely binary-safe: it reported the file’s true length, and reading individual bytes past an embedded zero returned the right values. The native build was safe too, with a single exception — asking for the file’s length returned zero, because that one operation was built on C’s notion of string length, which stops counting at the first zero byte. The buffer held all the bytes; only the ruler was broken. So the gap was not “binary handling is missing” but the far smaller “one length function loses the length on binary data.” Naming it that precisely is what kept the fix small.
A single builtin instead of a new type
The tempting response would have been to build the big thing: a proper bytes type, distinct from strings, with its own operations. That is a real and honest edge, and it was deliberately deferred — left for the day a program actually needs it — because a much smaller fix cleared the path. Adding one builtin that asks the operating system for a file’s size on disk, sitting right next to the existing “when was this file modified”, gave the program a length it could trust. From there the rule was simple: carry the buffer and its true size together as a pair, and every byte walk works with the read-a-byte operation that was already binary-safe. No new type, no new operators, one builtin — and the whole tool was unblocked. The choice not to build the larger machine was as important as the small one that got built; the frontier is not just about adding what is missing but about resisting the urge to add more than the program in front of you actually requires.
Routing around a second missing feature
Reading the section sizes needed a variable-length integer format, the kind that stores a number seven bits at a time with a continuation flag on each byte. The textbook way to decode it uses bitwise operators — mask off the low seven bits, shift, repeat — and the language did not have bitwise operators either. Rather than stop and add them, the decoder was written in plain arithmetic: the low seven bits are the remainder after dividing by a hundred and twenty-eight, and the continuation flag is whether the byte is at least that value. The same number falls out, with operators the language already had. This was a deliberate choice to route around a gap rather than fill it, and it is the mirror image of the previous decisions: sometimes the right move at the frontier is to not grow a capability, because the one program in hand can reach its goal without it, and a missing feature nobody is blocked on is not yet worth building. The absence was recorded, not fixed.
What the tool proved
Once it had a trustworthy length, the rest arrived without new pain: walk the sections, decode their sizes, read the export table with its length-prefixed names and kinds. The output was checked against the standard WebAssembly inspection tool and matched it exactly — the strongest kind of confirmation, because it is an external oracle rather than the language agreeing with itself. And this tool learned from the two before it: its ignore-list for build artifacts was written into the very first commit, so unlike its predecessors there was no history to clean up before publishing. The real result was smaller and sharper than “the language can now handle binary”: the read-a-file and read-a-byte operations had been binary-safe all along, and a single missing length function was the entire gap. One builtin, one meta demo — the compiler reading its own emitted bytes — and a bytes type left honestly unbuilt for the day something needs it.
Three tools done, three capabilities forced into being, each in a different corner of the language. The fourth would leave the command line entirely: a game that runs in a browser, drawn and driven through the frontend bindings — and it would surface not a missing builtin but a bug that had been hiding in the compiler across two backends the whole time.