Projects/Build a WASM Runtime

Build a WASM Runtime

Start by reading the eight-byte module header and decoding a single LEB128 integer, and end with a runtime that decodes a real compiled `.wasm` module from raw bytes and runs its exported function. Every lesson gives you a concrete spec with exact bytes and expected values to hit - the sign-extension boundary, i32 wraparound, divide-by-zero traps, the block-versus-loop branch target - and the interpreter grows one honest opcode family at a time.

47 lessonsMedium~20 min / lessonStack machineBytecode interpretersLEB128
The project

What you'll build over the next 47 lessons

Over 47 lessons you build a working WebAssembly runtime - an interpreter, not a JIT - that decodes the `.wasm` binary format directly and executes it on a stack machine. You begin at the very first bytes: the `\0asm` magic and version, then LEB128 integer decoding, the section framing, and the type, function, export, and code sections that describe what a module contains. From there you stand up the engine - a value stack and a decode-and-execute loop - and grow the instruction set one family at a time: the full i32 numeric and comparison operations, then i64, f32, and f64; locals and parameters; structured control flow (`block`, `loop`, `if`, `br`, `br_if`, `br_table`) with the label and branch-arity model that trips everyone up; function calls, the call stack, and `call_indirect` through a table; and finally linear memory with alignment, offsets, and bounds traps, plus globals.

By the end you have a runnable tool: point it at a compiled `.wasm` module, name an exported function, pass arguments, and get the result back - the capstone decodes an iterative factorial module from raw bytes and runs it. Every operation is pinned to exact expected values, including the edges that break naive ports: signed LEB128 sign-extension, i32 wraparound at `0x7FFFFFFF + 1`, division that truncates toward zero, the divide-by-zero and out-of-bounds traps, and the difference between branching to a block and branching to a loop.

This is a teaching-grade runtime built around WebAssembly's real MVP core: correct enough to decode and run compiled modules that stay within that core, but deliberately stopping short of what a production engine ships - SIMD, threads and atomics, the full WASI host-import surface, bulk-memory operations, exception handling, complete validation, and the WAT text-format parser. What you finish with is the numeric, control-flow, call, and memory core that all of those are built on top of.

build-a-wasm-runtime / lesson-01.md
Lesson 01 · The binary format

The module preamble

Every WebAssembly module starts with the same eight bytes: a four-byte magic number and a four-byte version. Today you read and validate that preamble, the first step of turning raw bytes into a module.

The goal

Read the first eight bytes of a module and confirm the magic number and version are what WebAssembly requires.

Start here - the target
TO DO
Scenario: Validating the module header
Givena byte slice beginning with 00 61 73 6D 01 00 00 00
Whenthe preamble is read
Thenthe magic bytes equal 00 61 73 6D (the ASCII for a NUL byte followed by "asm") and the version equals 1
Anda slice whose first four bytes are not 00 61 73 6D is rejected with an error, and so is a slice shorter than eight bytes
Background

Every .wasm file opens with a fixed eight-byte preamble: the four magic bytes 00 61 73 6D - a NUL byte followed by the ASCII letters asm - and then a four-byte version number stored little-endian. For the MVP the version is always 1, encoded as 01 00 00 00. Before a runtime does anything else, it reads these eight bytes and checks them: the magic proves this really is WebAssembly, and the version proves it is a format your runtime understands.

Starting here is deliberate. A runtime is fundamentally a program that reads bytes and acts on them, so the very first thing to build is the entry point that reads the first bytes of all. Validating the header is a tiny, self-contained target, and getting it right means every later lesson can assume it is working with a genuine module rather than random data.

Make it work
// The magic is the four bytes 0x00 'a' 's' 'm'; the version is a
// little-endian uint32 that must be 1 for the MVP.
var wasmMagic = []byte{0x00, 0x61, 0x73, 0x6D}
func readPreamble(b []byte) error {
if len(b) < 8 { return errShort }
if !bytes.Equal(b[:4], wasmMagic) { return errMagic }
// version = little-endian uint32 of b[4:8]; require 1
return nil
}
CheckpointDONE
You can recognize a valid WebAssembly module by its header and reject anything else. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

A genuinely working WebAssembly MVP interpreter that decodes the binary format from raw bytes and runs the core end to end - the full i32/i64/f32/f64 numeric families, structured control flow, direct and indirect calls, and linear memory with bounds traps - behind a runnable CLI whose built-in demo decodes and runs an iterative factorial with no external asset. It stops at the MVP core: no host imports or WASI, minimal validation (it runs correct modules rather than rejecting every malformed one), and no bulk-memory, SIMD, threads, or exception handling.

Extend it next
  • Add a validation pass so a malformed module fails fast with a clear error instead of trapping or misbehaving partway through execution
  • Support the import section and a minimal host-function ABI so modules can do I/O and call into the host, not just return a value
  • Add the bulk-memory operations (memory.copy, memory.fill) and saturating truncation conversions that real compiler output (Rust, clang) commonly emits
  • Round out the MVP-adjacent instructions this build does not reach, such as tail calls and the typed select
  • Write a WAT text-format parser so modules can be authored as text instead of only hand-assembled bytes or a compiler
  • Extend toward fuller spec coverage: SIMD, threads and atomics, and multiple memories and tables
Recommended reading

Books & references that go deeper

  • The authoritative reference. The Binary Format and Execution sections define every byte and every opcode you decode and run in this project - keep it open as you work.

  • A gentler orientation to what a module is, how the stack machine works, and how host and module fit together - good background before the spec.

  • The little-endian base-128 variable-length integer encoding that WebAssembly uses everywhere. The unsigned and signed decoders you build in chapter one are exactly this, sign extension and all.

  • wazero · Tetrate

    A zero-dependency WebAssembly runtime written in Go - a readable, production-grade reference for the decoder and interpreter you are building here.

  • A small, fast interpreter (not a JIT) for WebAssembly - a good second reference for how a real stack-machine execution loop is structured.