A Second Dogfood, Chosen to Hurt Differently
The first application built in Mere was a web app on the Wasm-and-Node host, where every system call was supplied for it. The second was chosen deliberately to remove that comfort: a jq-like command-line tool compiled to a native binary, which forced the C backend to answer questions the web host had always answered on its behalf — how to read arguments, how to read standard input, how to set an exit code, and, most sharply, how to compose two libraries without their internals colliding. The tool was real, but the friction log was the point.
The way to find out what a language is missing is to build something with it and watch where it resists. The first real application built in Mere was a small collaborative notes server, running as WebAssembly under a Node host, and it exercised a great deal — but the host was generous. Every system call the program needed, the Node runtime supplied: files, sockets, timers, standard output. A whole class of questions never came up, because something else was answering them.
So the second application was chosen not for what it would produce but for where it
would hurt. A jq-like tool for querying JSON and CSV on the command line,
compiled to a native binary through the C backend, lands in exactly the territory
the web host had been hiding: no ambient runtime, a real process with arguments and
standard input and an exit code, and libraries that have to be compiled together
rather than loaded by a forgiving host. The tool itself is useful. But its friction
log — the running record of everywhere the language pushed back — was as much the
deliverable as the binary.
The wall at the edge of the process
The first resistance was immediate and total. The whole purpose of a command-line tool
is to read its arguments and its input, and in the interpreter all of this worked:
there were builtins to get the argument vector, to read a line from standard input, to
exit with a status code. Compiled to a native binary, none of them existed. The C
backend emitted code that referred to args and read_line as if they were
functions, but never defined them, and the C compiler stopped with an undeclared
identifier. A native Mere program, at that moment, could read one hard-coded file path
and nothing else — no arguments, no pipe, no exit status.
This is the exact shape of the gap that a generous host conceals. On the web host,
argument-passing and input were the host’s job; the language had never been asked to
lower them to a real operating-system process, so it never had. Closing the gap was
undramatic once it was seen: the argument vector arrived first, so main took the
process’s argc and argv and handed the rest to the program; then reading all of
standard input, so a tool could sit at the end of a pipe; then a real process exit,
lowered to the C library’s exit the same way an unreachable error already was. Each
landed in its own release. None was hard. The point is that none had a reason to exist
until a program tried to be an ordinary Unix citizen — and then all three were
prerequisites, not niceties.
The backend that forgot about shadowing
The next family of frictions all had one shape: the C backend treating a name as a
builtin without first checking whether the program had rebound it. The concurrency
work had added a join builtin — the one that waits for a spawned thread — and the
CSV library, entirely reasonably, had its own local join that glued strings
together. Compiled, the string joiner turned into a call to the thread primitive, and
the C compiler complained that a string had no thread handle to wait on. The dispatch
had matched the name join before it had asked whether this particular join was
the builtin or a local definition standing in front of it.
The interpreter and the type checker had always respected that shadowing; the C backend had a separate dispatch path that didn’t. The same story repeated in smaller ways — a string-equality function that existed in the interpreter and the Wasm backend but not in C, ordering comparisons that type-checked on numbers but not on strings. Each was a place where one backend’s coverage had quietly drifted from the others’, and each was invisible until a native build walked into it. A four-backend language pays for its parity in exactly this currency: the gaps are real, they are individually small, and they surface one dogfood at a time.
The sharpest finding: two libraries that collided
The best finding of the whole exercise needed two libraries at once. Parsing a JSON array worked. Importing the CSV library alongside it and parsing the same array failed to compile, with a variable that was used but never declared — a captured string length that had gone missing. Neither library was wrong on its own; only the combination broke, which is the signature of a problem in the machinery that composes them rather than in either piece.
The cause was worth the hunt. When the backend lifts an inner recursive function to
the top level, it has to work out which outer variables that function captured, and it
resolved the lifted functions’ names through a single global table. Both libraries
happened to contain an inner helper named loop. The JSON parser’s loop called
itself recursively — and that self-call, resolved through the global table, landed on
the CSV library’s lifted loop instead, so the JSON loop inherited a variable the
CSV loop had captured and its own callers had never heard of. Two same-named locals in
two different files, invisible to each other in the source, had been quietly merged by
the compiler. The fix was to resolve those names per host function rather than
globally, so two inner functions named loop stay distinct — and it came with a
minimal reproduction, two functions each with a loop, filed as a regression test. A
real limit on composing native libraries, found only because two of them were finally
asked to share a binary.
Where the language didn’t resist
It would be a distortion to present only the friction, because one of the log’s most useful entries records the absence of it. Midway through, the query engine was redesigned from a function that mapped one JSON value to one, into a streaming model where a selector could explode a value into many and selectors could be sequenced — a non-trivial change to the core of the tool. It was written in Mere with no resistance at all: a new case in the selector type, and ordinary recursion for the rest. This matches what every dogfood so far has said. The core of the language — algebraic data types, pattern matching, recursion over trees and streams — is where the work is comfortable. The pains cluster at the edges: native input and output, the packaging of libraries, the corners of strings and Unicode. The middle holds.
The log was the loop
The thread running through all of it is a loop, and the loop is the method. A real program is built until it resists; the resistance is written down as a precise signal; the sharp ones become fixes in the language itself; and then the program adopts the fix and drops the workaround it had been carrying. The tool started with a hand-rolled string splitter standing in for the CSV library it couldn’t yet compile against; once the composition bug was fixed, that workaround was deleted and the tool imported the real library, gaining proper quoted-field parsing for free. A character-classification routine written in terms of byte values was rewritten as the direct string comparisons it had wanted to be, once those type-checked. The workarounds are not scars; they are the receipts of a loop that closed. Choosing a domain that hurts differently was not pessimism. It was the fastest way to find the next true thing the language needed, and then to build it.