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.
The value stack
Section titled “The value stack”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]"]
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.
Local variables
Section titled “Local variables”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 backParameters are also locals — you can read them with local.get just like declared locals, but they are initialized with the call arguments.
local.tee: store and keep
Section titled “local.tee: store and keep”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 behindWithout local.tee you would need local.set followed by local.get, which is two instructions instead of one.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Writing WAT by hand | Forces you to actually understand the value stack and locals — no syntax sugar hides it, great for learning | You have to track stack depth yourself the whole way; forgetting one local.set can break validation |
| Letting a compiler generate Wasm | The compiler handles stack allocation and register-to-local mapping automatically — much more productive for real apps | You don’t control local lifetime in fine detail — you have to trust the compiler |
Common mistakes
Section titled “Common mistakes”- 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.teewhenlocal.setwas intended, leaving an unwanted value sitting on the stack without realizing it - Forgetting that a branch like
br/br_ifjumping 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
.wasmblob on any Wasm-hosting platform commonly reach forwasm2watto turn the binary back into readable WAT, so they can step through the stack instruction-by-instruction while tracking down a compiler-generated bug.