Skip to content

Locals and the Stack

WebAssembly is a stack machine. There is no register file like in x86 or ARM — instead, instructions communicate by pushing and popping values on an implicit value stack. Understanding this model is the key to reading WAT fluently.

Every instruction operates on the top of the stack. An instruction pops its inputs and pushes its output. For example, i32.add pops two i32 values and pushes one i32 result.

flowchart LR
  A["Stack: []\ni32.const 10"] --> B["Stack: [10]\ni32.const 3"]
  B --> C["Stack: [10, 3]\ni32.add"]
  C --> D["Stack: [13]"]
Value stack during i32.const 10 / i32.const 3 / i32.add

At the end of a function, whatever is left on the stack must exactly match the declared result types. Leave too much or too little and the module fails validation.

Besides the stack, functions can declare local variables using (local $name type). These are mutable slots that live for the duration of the function call.

(module
(func (export "example") (param $x i32) (result i32)
(local $tmp i32)
local.get $x ;; push $x
i32.const 1
i32.add ;; pop two, push sum
local.set $tmp ;; pop and store in $tmp
local.get $tmp)) ;; push $tmp back

Parameters are also locals — you can read them with local.get just like declared locals, but they are initialized with the call arguments.

local.tee is like local.set but it leaves a copy of the value on the stack after storing. It is useful when you need to both save a value and immediately use it:

(module
(func (export "storeAndDouble") (param $n i32) (result i32)
(local $saved i32)
local.get $n
local.tee $saved ;; stores $n in $saved AND leaves the value on stack
i32.const 2
i32.mul)) ;; doubles the value that tee left behind

Without local.tee you would need local.set followed by local.get, which is two instructions instead of one.

WebAssembly
OptionBenefitCost
Writing WAT by handForces you to actually understand the value stack and locals — no syntax sugar hides it, great for learningYou have to track stack depth yourself the whole way; forgetting one local.set can break validation
Letting a compiler generate WasmThe compiler handles stack allocation and register-to-local mapping automatically — much more productive for real appsYou don’t control local lifetime in fine detail — you have to trust the compiler
  • Ending a block or function with more values left on the stack than the declared signature — e.g. forgetting to pop a value pushed along the way — so the module fails to validate
  • Using local.tee when local.set was intended, leaving an unwanted value sitting on the stack without realizing it
  • Forgetting that a branch like br/br_if jumping out of a block has to leave the stack in exactly the shape the branch’s target expects, not just whatever falls out of normal sequential flow

💡 Real-world example

Engineers debugging a production .wasm blob on any Wasm-hosting platform commonly reach for wasm2wat to turn the binary back into readable WAT, so they can step through the stack instruction-by-instruction while tracking down a compiler-generated bug.

What does `local.tee $x` do?
In a stack machine, what happens when `i32.add` executes?
Where must `(local $x i32)` appear in a function?