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.
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.
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.
Read the first eight bytes of a module and confirm the magic number and version are what WebAssembly requires.
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.
// 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 1return nil}
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.
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.
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.