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.
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.