The Real ALU

With integer width settled, the bitwise operators the SHA-256 probe had been faking could finally exist. Six builtins — and, or, xor, not, shift-left, arithmetic shift-right — lowered to the machine operation on all four backends at once. Swapping them into the hash's fake arithmetic ALU took one block from twenty-nine milliseconds to under seven microseconds, about four thousand times faster. The interesting part was the wreckage the rewrite exposed: temporaries left at the wrong width, a helper two backends forgot to emit, and a line in last episode's own changelog that turned out to be wrong.

merebitwisebackendsperformancelanguage-design

The previous episode settled how wide an integer is. This one spends that groundwork on the feature the SHA-256 probe actually wanted, and then cleans up the mess that adding it made visible — which is the more honest half of the story.

Six builtins, one machine operation each

Bitwise arithmetic is not a place for cleverness. The language gained six builtins — AND, OR, XOR, NOT, shift-left, and an arithmetic (sign-propagating) shift-right — and each one lowers to exactly the machine’s own operation: C’s &-family operators, Wasm’s i32.and-family instructions, LLVM’s and i32-family, and the interpreter’s native land-family. They are functions rather than infix operators, following the same instinct as the Unicode work a few episodes back: add the capability as a plain builtin first, and let a real pain — not a guess — be the thing that argues for operator syntax later. Because the standard library’s prelude must run on every backend, the six landed on all four in the same release by construction; there was never a window where three backends had them and one didn’t.

The payoff was the SHA-256 example. Its inner loop had been faking every AND and XOR with a thirty-two-iteration loop of divisions and remainders — correct, and grotesquely slow. Swapping in the real operators took one compression block from about twenty-nine milliseconds, interpreted, to 6.7 microseconds native — roughly four thousand times faster — with every NIST test vector still passing. The faking had been a good way to prove the algorithm; it was a terrible way to run it.

What the rewrite dragged into the light

A clean feature would be a short episode. This one earns its length from three unrelated things the rewrite exposed by accident. First: abs, min, max, and clamp were still declaring their internal temporaries as the old 32-bit C type, left behind when the integer migration swept the rest of the backend — so min of a value above two billion silently truncated, the exact bug from last episode hiding in a helper nobody thought to recheck. Second: converting an integer to a string via a variable under a top-level binding referenced a helper function that only the show builtin had been registering — so the emitted module named a function that was never defined, on the Wasm and LLVM backends both, a latent crash that the probe’s plain print (str_of_int x) finally triggered. Third, and most humbling: last episode’s changelog had claimed the LLVM backend’s integer was already 64-bit. Measuring it said otherwise — LLVM’s int is 32-bit like Wasm’s. The claim was corrected in place, LLVM got the same out-of-range-literal compile error as Wasm, and widening it to 64 bits went onto the deferred list with the SHA-256 example as its standing forcing program. The lesson is the one the whole part keeps teaching: a feature is a small, bright thing, and the shadows it throws are where the work actually is. The next episode goes back to the bytes story — not the numbers inside a file, but whether the file’s bytes can get into the program at all.

← Back to Mere: Building a Language