# Crank · Malte Therkildsen

> The binary format, validation and every instruction of the standard but its vectors, run one step at a time in plain JavaScript. It passes the official test suite, and every result is held against your browser.

Source: https://therkildsen.dev/writing/crank

[Writing](https://therkildsen.dev/writing)/Crank
W06NotesEngines from nothing

# Crank, a WebAssembly interpreter written from nothing

Crank reads WebAssembly’s binary format, checks it, and runs it one instruction at a time, in plain JavaScript and without the browser’s own WebAssembly. It was written from an empty file and the specification, and it passes every test in the official suite it runs. Below, it runs four small programs where you can watch it, and every result is held against your browser running the same bytes.

PostedKind**Notes**Series**Engines from nothing**

        Run
        Step
        Step over the call
        Start over
         The text it was written in

          Steps
          ·
          The stack, top first

          The calls, innermost first

      ·

    Crank prints the listing from the bytes it runs instead of from the source, with the names stored in the module’s name section. The marked line is the instruction Crank takes next. When a run ends, your browser’s own WebAssembly runs the same bytes with the same arguments, and the two answers are set side by side.

## Bytes first

      A WebAssembly module starts with four bytes that spell `\0asm` and four more for the version. After them come sections, each an id, a length and its contents, in a fixed order: the types of the functions, what the module imports, its functions, tables, memory and globals, what it exports, and last the code and the data. Numbers are written in `LEB128`, seven bits to a byte with the eighth saying whether another byte follows, so a small number takes one byte and a large one takes more.

        ·

      Crank’s decoder is strict where the specification is strict. A number may not use more bytes than its width needs, and the unused bits of its last byte must be zero, or repeat the sign. A name must be valid . Writing that last check turned up a trap in the platform rather than the format: JavaScript’s `TextDecoder` drops a byte order mark at the start of its input, so a name that begins with one came out shorter than its bytes. The test suite has exactly such a name, and Crank’s decoder now sets `ignoreBOM` on `TextDecoder`, so the mark stays in the name.

## Checked before it runs

      Before a single instruction runs, the whole module is validated. Crank walks every function once with a stack of types instead of values: `i32.add` must find two `i32` on it and leaves one, a block must end with exactly the types in its signature, and a branch must leave on the stack the types of its target block. Code after an unconditional branch can never run, and there the stack becomes polymorphic, so any type it asks for is taken as given.

      After validation, the interpreter runs without checks of its own. It never checks a type, never looks for a missing value, and never asks whether a branch target exists. Validation also writes down where every block’s `else` and `end` are, so a branch is a jump to a known place and not a search.

## A stack of its own

      A natural way to write an interpreter is to have a WebAssembly call become a JavaScript call. It is short, and it hands the depth of recursion to JavaScript, whose stack runs out long before WebAssembly’s should, with an error that names the wrong thing. Crank keeps its own stack of frames instead. A call pushes a frame with the function, its locals, the place to return to and its labels, and a return pops it, so the loop that runs instructions never calls itself.

      That is also what makes stepping possible. Between any two instructions the whole state of the program is data: the values on the stack and the frames above them, which is what the panel beside the listing shows. A run is the same loop taking a few hundred thousand steps at a time and giving the page back between them. When a recursion goes too deep, Crank stops at a fixed number of frames and says the call stack is exhausted, as the specification asks.

      A label is where a branch goes. A `block` is left forwards, at its end, and a `loop` is left backwards, at its start, so `br` to a loop is how WebAssembly repeats. A branch keeps as many values as its label takes, drops everything else down to the height the block started at, and goes on from there.

## Numbers the way the machine has them

      An `i32` is a JavaScript number kept in range with `| 0` and `Math.imul`. An `i64` does not fit in a double, so it is a `BigInt`, wrapped to sixty four bits after every operation. An `f32` is a double rounded with `Math.fround` after each step, which gives the right answer because a double has more than twice the precision of a float.

      Two corners needed more. The first is NaN. WebAssembly says which bits a NaN has, and some instructions must keep them, but JavaScript has one NaN and forgets the rest. Crank carries a NaN with particular bits as an object that holds them and still compares as NaN. The second is turning a large `i64` into an `f32`. Through a double, the number is rounded twice, and a value just above halfway between two floats can land exactly on halfway after the first rounding and go the wrong way on the second. Crank keeps one extra bit, set when anything was cut off, and rounds once.

## Held against the specification

      The WebAssembly specification comes with a test suite: scripts that define modules and say what calling them must return, where they must trap, and which modules must be refused as malformed, invalid or unlinkable. The command runs every script for the second version of the standard. WABT, the reference tools for WebAssembly, turns each module’s text into bytes, and Crank does the rest on its own.

      $ node web/notes/crank/spec.mjs
89 scripts from the WebAssembly test suite, 27,831 commands
passed 27,259, failed 0, not run 572, in 9.5 s
passed, by kind of command:
  21,453  assert_return
   2,388  assert_trap
   1,475  assert_invalid
   1,126  module
     719  assert_malformed
      83  assert_unlinkable
      15  assert_exhaustion
not run, and why:
     570  malformed text
       2  WABT left out the data count section
      Nothing fails. What is not run is text that is malformed on purpose, which tests a text parser Crank does not have, and two modules where WABT writes the bytes without the section the test is about.

## Held against

      The four programs above, run again in Crank and in  own WebAssembly from the same bytes. For the Mandelbrot set, every byte each row writes to memory goes into a fingerprint, so the same result means the same image.

      $ node web/notes/crank/race.mjs
node v22.22.0, 4 programs, each the same bytes in Crank and in V8

program                             result                            steps      Crank        V8   slower
fib fib(24)                         46368                         1,650,535      67 ms   0.29 ms     230x
fac fac(0..25), 1000 times          -1307449385690794608          4,160,000     120 ms   1.37 ms      87x
sieve sieve(1000000)                78493                        40,564,220    1192 ms   2.84 ms     420x
mandelbrot 320 by 180, at most 100  1399128 903779024            57,217,382    1605 ms   4.10 ms     391x

the same results in 4 of 4
      The answers are the same, and the times are far apart.  compiles WebAssembly to machine code before it runs, and Crank looks at every instruction every time it passes, with a JavaScript value for each number on its stack. A first version was twice as slow again, because it looked the arithmetic up in an object whose keys run past the one byte opcodes, which  keeps as a slow dictionary.

          Crank
          Your browser

        Draw bothThe same program, a row at a time, in Crank and in your browser.

## What it does not have

      Crank runs the second version of WebAssembly without its vector instructions. It has no threads, no more than one memory, no tail calls, no exceptions and no garbage collected types, which are all later proposals. It reads the binary format only, so its tests and the four programs here are turned from text into bytes by WABT. It does not compile anything, which is why it is hundreds of times slower than the browser, and why every step can be watched.

      The source is in the portfolio’s repository under `web/notes/crank/`: `crank.js` for the decoder, the validator and the interpreter, `text.js` for the listing, `spec.mjs` and `race.mjs` for the commands, and the programs as text under `programs/`. The test scripts are the WebAssembly specification’s, under the Apache License.

[Newer · W07Quire, a PDF reader written from nothing](https://therkildsen.dev/writing/quire)[Older · W05Glyph, a font renderer written from nothing](https://therkildsen.dev/writing/glyph)
