Under the Hood
Wasm

What WebAssembly actually is: a stack machine and bytecode

A .wasm file is not a language, not a faster JavaScript, and not a container — it is a compact binary encoding of instructions for an imaginary CPU that computes by pushing and popping a stack. This lesson builds that stack machine in your head from a three-instruction example, so every later idea (linear memory, the JS boundary, why it's fast) has something concrete to attach to.

What WebAssembly actually is: a stack machine and bytecode

WebAssembly gets described in ways that are all a little wrong. "It's like assembly for the web." "It's a faster JavaScript." "It lets you run C in the browser." Each captures a symptom without naming the thing. The thing itself is surprisingly concrete and surprisingly small, and once you see it, the whole ecosystem around it — the toolchains, the memory model, the JavaScript glue — clicks into place as machinery bolted onto a simple core. So we do what the other deep tracks do: build the core first.

Here is the core in one sentence: WebAssembly is a compact binary format encoding instructions for a virtual stack machine — an imaginary, simple CPU that does all its arithmetic by pushing values onto a stack and popping them off. Every other fact about WASM is a consequence of that sentence. Let's earn it.

A stack machine, from three instructions

Forget the web for a moment. Imagine a very dumb calculator that has no variables and no registers — just a single pile of numbers called the operand stack, and a list of instructions it executes one at a time. Each instruction does one tiny thing to that pile: push a number on, or pop some numbers off, do arithmetic, and push the result back.

Say you want to compute 3 + 4. In this machine that's three instructions:

i32.const 3    ;; push the 32-bit integer 3 onto the stack
i32.const 4    ;; push the 32-bit integer 4 onto the stack
i32.add        ;; pop the top two, add them, push the result (7)

Trace the stack as it runs:

  • Start: stack is empty [].
  • After i32.const 3: [3].
  • After i32.const 4: [3, 4].
  • After i32.add: the instruction pops 4 and 3, adds them, pushes 7[7].

That's it. That is genuinely how WebAssembly computes — not a simplified teaching version of it, the real execution model. i32.add doesn't take arguments the way a function call does; it implicitly operates on whatever is on top of the stack. Every WASM instruction is defined the same way: it declares how many values it pops, what it does, and what it pushes. Complex expressions are just longer instruction sequences that leave their result on top of the stack for the next instruction to consume.

Program
Speed
i32.const 3
i32.const 4
i32.add
stack
empty

WebAssembly has no registers or named variables — it computes by pushing and popping this one operand stack. Every instruction implicitly consumes its arguments off the top and pushes its result back: i32.add doesn't know or care what the two numbers mean, it just pops the top two cells and pushes their sum. A whole expression compiles down to a sequence of pushes and pops with no other state to track.

Step through the program above and watch the stack column. The entire "virtual CPU" is that column plus a cursor pointing at the current instruction. There are no hidden mechanics — what you see executing is what the browser's WASM engine models internally.

Why a stack machine specifically

The stack design isn't arbitrary; it's chosen for three properties that matter enormously for something that ships over the network and runs untrusted:

It's compact. Because instructions get their operands implicitly from the stack, they don't need to encode operand addresses. i32.add is a single byte. Compare that to a register machine instruction like "add register 3 and register 5 into register 1," which has to name three locations. A stack-based encoding produces small binaries, and small matters when the code travels over the wire before it can run.

It's trivial to validate. Before the browser runs a .wasm module, it has to verify it's well-formed and type-safe — that you never, say, try to i32.add when the stack has a float on top, or pop from an empty stack. With a stack machine the engine can check this in a single linear pass, simulating the types on the stack without running anything. That fast, provable validation is what makes it safe to download and execute a binary from a stranger. (The streaming compilation lesson builds on exactly this.)

It's easy to compile to real hardware. A stack machine is an abstract target; your actual CPU has registers, not an operand stack. But translating a validated stack-instruction stream into efficient register machine code is a well-understood, fast transformation. WASM is deliberately a thin abstraction over what real CPUs do — close enough that the translation is cheap, abstract enough to be portable across CPU architectures.

Four types, and why that's the whole list

The example used i32 — a 32-bit integer. WebAssembly's core has an almost comically short list of value types: i32, i64 (32- and 64-bit integers), and f32, f64 (32- and 64-bit floats). That's it. No strings, no arrays, no objects, no booleans even (an i32 that's 0 or 1 stands in).

This is not a limitation the designers regret — it's the point. Those four types are exactly the types real CPUs operate on natively. By restricting the core to them, every WASM arithmetic instruction maps directly onto a hardware instruction, with no runtime type-checking and no boxing. When you see i32.add, the engine knows statically both operands are 32-bit integers, so the compiled code is a bare machine add — no "what type is this?" branch of the kind a dynamically-typed language like JavaScript must sometimes perform. This static, minimal type system is a big part of the "why it's fast" story we'll tell properly in that lesson.

The obvious question — "if the only types are numbers, how do I pass a string, or a struct, or an object?" — has a single answer that drives the next several lessons: you don't pass them as values; you lay them out as bytes in a block of memory and pass a number that points at them. Which brings us to the thing the stack machine has been quietly missing.

What the stack machine can't do (and where it goes)

Our three-instruction adder is complete and honest, but notice what it can't express. It has an operand stack for transient arithmetic, but nowhere to keep data around — no place to store an array, a string, the state of a running program. A stack is a scratchpad, not a heap. Real programs need somewhere persistent to put bytes.

That somewhere is linear memory: a single, flat, resizable array of bytes that a WASM module can read and write at integer offsets, and which is also the shared channel through which JavaScript and WASM exchange anything more complex than a number. It is the other half of the WebAssembly execution model, and it's the direct subject of the next lesson.

Everything else you'll learn hangs off the two pieces you now have or are about to get: a stack machine that computes, and a linear memory that stores. The toolchains produce instructions for the first; the JS boundary shuffles bytes through the second; "why it's fast" is a property of how cheaply both map to real hardware. Hold the stack trace from the playground in your head — it's the ground floor everything is built on.

Where this goes next

The linear memory model picks up the exact gap this lesson ended on: where a WASM program keeps anything that has to outlive a single arithmetic expression. It turns out to be one of the most elegant ideas in the whole design — memory is just a big ArrayBuffer, addressed by plain integers, which is simultaneously why C and Rust map onto WASM so cleanly and how a JavaScript program on the outside can read what a WASM program on the inside produced.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Trace the stack through i32.const 10, i32.const 20, i32.add, i32.const 2, i32.mul. What's on the stack after each instruction?
  2. Why doesn't an instruction like i32.add need to name its operands, and what does that buy you in binary size?
  3. Give the three properties the stack-machine design was chosen for, and one concrete consequence of each.
  4. Explain why 'what language is WebAssembly?' is a category error. What analogy from the Java world captures WASM's actual role?
  5. WebAssembly's core has only four value types. Why is that minimalism a performance feature rather than a limitation?
  6. If the only things WASM values can be are numbers, how does a program pass a string or a struct across to JavaScript?
  7. The stack machine has an operand stack but the lesson says it 'can't keep data around.' What's the distinction, and what piece of the model fills the gap?