Bytes Get in the Door

A thirty-line CRC-32 tool was trivial on the new bitwise operators. The discovery was on the input side: reading a file returned a string, and on the C backend a string is NUL-terminated, so a binary file with a zero byte in it silently stopped there. A twenty-five-byte file read as two bytes and produced a confident, wrong checksum — while the interpreter, whose strings carry NULs, read it correctly. The promise that a string is just bytes had only ever been true on one backend.

merebytesbackendsiolanguage-design

The bitwise operators made a whole class of programs suddenly cheap to write, so the next probe was a CRC-32 checksum tool — thirty lines, the algorithm almost transcribable from its polynomial. It computed the correct checksum for the standard test string on the first try. The interesting failure was not in the arithmetic. It was in getting the file into the program at all.

The promise that held on one backend

Mere’s string has always been documented as bytes — the memory-model note says so, and the Unicode episode leaned on it. But “a string is bytes” is a claim that has to be true on every backend to mean anything, and it wasn’t. Reading a file returned a string, and on the C backend a string is a NUL-terminated char*. Hand it a binary file with a zero byte somewhere in the middle and it stops reading there, silently. The probe’s test file was twenty-five bytes with a NUL near the front; the C build read two of them and produced a checksum that was internally consistent, plausible, and completely wrong. The interpreter, whose strings are real OCaml strings that carry NULs without complaint, read all twenty-five and got the right answer. So the two backends disagreed — not with an error, but with two different confident numbers — and the one the tests ran on was the one that happened to be right. This is the same shape as the integer-width episode: a promise the language made was only kept where it was tested, and a binary file was the first input that reached past the place where it broke.

A vector of bytes, not a bytes type

The fix had a design fork in it. The honest binary-input path is a value that means the same thing everywhere, and the tempting move is to introduce a dedicated bytes type. The cheaper move — and the one taken — was to read the file into a vector of integers, one element per byte, each in the range 0 to 255, reusing the vector machinery the language already had. Eight bytes of storage per byte of file is an unflattering ratio, and it is written down as the honest cost; the day a program makes that ratio hurt is the day a real bytes type gets its forcing program. The interpreter and C backend got the reader; Wasm and LLVM reject it with a pointed compile error, because their host file interfaces still traffic in NUL-terminated strings. One subtlety earned its own debugging session: the new reader returns a vector, and the type variable naming the vector’s region has to be pinned at the point the reader is called — exactly as the vector constructor does it — or the region stays unresolved and the C backend silently declines to emit any function that takes the returned vector. The probe hit that too, which is fitting: an episode about a value silently failing to arrive found a second value silently failing to arrive, one level down in the compiler. The checksum tool now agrees with the system zlib on both text and NUL-bearing files. The next episode picks up a different file format entirely — and finds the documentation describing a language poorer than the one that exists.

← Back to Mere: Building a Language