Project 1: Hand-Write WAT Programs
Project 1: Hand-Write WAT Programs
Learn WebAssembly by writing it directlyโno compilers, no abstractions, just you and the stack machine
Project Overview
| Attribute | Value |
|---|---|
| Difficulty | Beginner |
| Time Estimate | Weekend (8-16 hours) |
| Language | WAT (WebAssembly Text Format) |
| Prerequisites | Basic programming in any language |
| Main Book | WebAssembly: The Definitive Guide by Brian Sletten |
| Knowledge Area | Low-Level Web / Compilers |
Learning Objectives
After completing this project, you will be able to:
- Explain stack-based execution - Describe how operations push and pop values without named variables
- Write valid WAT syntax - Use S-expressions to define functions, locals, and control flow
- Trace WASM execution mentally - Follow stack state through any sequence of instructions
- Use linear memory - Load and store values in WASMโs flat memory model
- Interface with JavaScript - Import and export functions between WASM and the host
- Understand WASMโs design constraints - Articulate why it has only 4 numeric types and structured control flow
Conceptual Foundation
1. What Is a Stack Machine?
Most programmers think in terms of registers or variablesโnamed storage locations. WebAssembly works differently. Itโs a stack machine, meaning all operations work on an implicit stack.
Traditional (register machine): Stack machine:
r1 = 3 push 3
r2 = 4 push 4
r3 = r1 + r2 add (pops 2, pushes result)
In a stack machine:
- Every instruction consumes operands from the stack
- Every instruction pushes results onto the stack
- There are no registers to name
Why Stack Machines?
Stack machines are simpler to implement and verify:
- Compact encoding: No register names to encode
- Easy validation: Stack typing is deterministic
- Portability: No target-specific register allocation
- Security: Stack discipline is easy to enforce
Stack Visualization
Consider evaluating (3 + 4) * 5:
Instruction Stack (top on right) Explanation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
i32.const 3 [3] Push 3
i32.const 4 [3, 4] Push 4
i32.add [7] Pop 3 and 4, push 7
i32.const 5 [7, 5] Push 5
i32.mul [35] Pop 7 and 5, push 35
Critical insight: The stack has a type at every point. After i32.const 3, the stack type is [i32]. After i32.add, itโs still [i32]. WASM validates that types always match.
2. WAT Syntax: S-Expressions
WAT (WebAssembly Text Format) uses S-expressionsโnested parenthetical notation:
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(export "add" (func $add))
)
Key syntactic elements:
| Element | Syntax | Example |
|---|---|---|
| Module | (module ...) |
Container for everything |
| Function | (func ...) |
Defines executable code |
| Parameters | (param $name type) |
Function inputs |
| Results | (result type) |
Function output type |
| Locals | (local $name type) |
Function-local variables |
| Export | (export "name" ...) |
Make visible to host |
| Import | (import "mod" "name" ...) |
Bring in host function |
Two Syntactic Styles
WAT supports both flat and folded (nested) syntax:
;; Flat style (what actually executes)
local.get $a
local.get $b
i32.add
;; Folded style (syntactic sugar)
(i32.add (local.get $a) (local.get $b))
Both compile to identical bytecode. Folded style is easier to read for expressions; flat style shows what the stack machine actually does.
3. WASM Types
WebAssembly has exactly four numeric types:
| Type | Bits | Description |
|---|---|---|
i32 |
32 | Signed/unsigned 32-bit integer |
i64 |
64 | Signed/unsigned 64-bit integer |
f32 |
32 | IEEE 754 single-precision float |
f64 |
64 | IEEE 754 double-precision float |
Why so few types?
- Universality: These types exist on all modern CPUs
- Simplicity: Fewer types = simpler validation
- Efficiency: Maps directly to hardware
Note: WASM doesnโt distinguish signed vs. unsigned at the type level. Instead, operations indicate signedness: i32.div_s (signed divide) vs. i32.div_u (unsigned divide).
4. Control Flow: Structured, Not Goto
Unlike traditional assembly, WASM has no goto. Instead, it uses structured control flow:
;; Block: a labeled scope
(block $exit
;; code
br $exit ;; jump to end of block
;; unreachable
)
;; Loop: a labeled scope where br goes to the START
(loop $again
;; code
br $again ;; jump to start of loop
)
;; If-else
(if (i32.gt_s (local.get $n) (i32.const 0))
(then
;; positive case
)
(else
;; non-positive case
)
)
Why structured control flow?
- Validation: Can statically determine stack types at any point
- Security: No arbitrary jumps = no ROP attacks
- Optimization: Easier to analyze and compile
Branch Depth
Branches specify a label index (depth), not an absolute address:
(block $outer ;; depth 1 from inner
(block $inner ;; depth 0 from itself
br 0 ;; jumps to end of $inner
br 1 ;; jumps to end of $outer
)
)
5. Linear Memory
WASM memory is a contiguous byte array that modules can read/write:
;; Declare 1 page (64KB) of memory
(memory 1)
;; Store i32 value 42 at byte offset 0
(i32.store (i32.const 0) (i32.const 42))
;; Load i32 from byte offset 0
(i32.load (i32.const 0)) ;; pushes 42 onto stack
Memory operations:
i32.load,i64.load,f32.load,f64.load- Load valuesi32.store,i64.store,f32.store,f64.store- Store valuesi32.load8_s,i32.load8_u, etc. - Load partial values with sign extensionmemory.grow- Expand memory (in pages of 64KB)
Critical insight: Memory is byte-addressed but must be naturally aligned. An i32.load at offset 1 works but may be slower. At offset 3, it might trap on some implementations.
6. Locals and Parameters
Functions can have parameters and local variables:
(func $example (param $x i32) (param $y i32) (result i32)
(local $temp i32) ;; declare local
local.get $x ;; push parameter value
local.get $y ;; push parameter value
i32.add ;; add them
local.set $temp ;; store result in local
local.get $temp ;; push local value (this is the return value)
)
Important: Parameters are just locals that are pre-initialized with argument values. Internally, theyโre indexed 0, 1, 2โฆ with parameters first, then declared locals.
7. The Host Boundary
WASM modules interact with their host (browser, Node.js, wasmtime) through imports and exports:
;; Import a function from the host
(import "console" "log" (func $log (param i32)))
;; Export a function to the host
(export "compute" (func $compute))
In JavaScript:
const imports = {
console: {
log: (value) => console.log(value)
}
};
const instance = await WebAssembly.instantiate(wasmBytes, imports);
instance.exports.compute(42);
Key insight: The import/export boundary is where WASMโs sandboxing happens. WASM can only call functions you explicitly provide. It cannot access the DOM, network, or filesystem directly.
Project Specification
Deliverables
You will create five WAT programs, each exploring a core concept:
Program 1: Arithmetic Calculator
- File:
arithmetic.wat - Exports:
add(i32, i32) -> i32,sub(i32, i32) -> i32,mul(i32, i32) -> i32,div(i32, i32) -> i32 - Test: Call from JavaScript, verify correct results
Program 2: Fibonacci with Recursion
- File:
fibonacci.wat - Exports:
fib(i32) -> i32 - Behavior: Return nth Fibonacci number (0, 1, 1, 2, 3, 5, 8โฆ)
- Challenge: Implement recursively first, then iteratively
Program 3: Factorial with Loops
- File:
factorial.wat - Exports:
factorial(i32) -> i64 - Behavior: Return n! (use i64 to handle larger values)
- Challenge: Use a loop, not recursion
Program 4: String Reverser (Memory)
- File:
string_reverse.wat - Memory: 1 page
- Exports:
reverse(i32, i32)- takes offset and length, reverses in place - Challenge: JavaScript writes a string to memory, calls reverse, reads it back
Program 5: Host Integration
- File:
logger.wat - Imports:
env.print_i32(i32)- print a number - Exports:
count_up(i32)- call print_i32 for each number 1 to n - Challenge: See WASM calling into the host repeatedly
Success Criteria
- All five programs compile with
wat2wasmwithout errors - All programs produce correct output when tested from JavaScript
- You can explain the stack state at any point in your code
- You can convert between flat and folded syntax fluently
- You understand why each program works (not just that it works)
Solution Architecture
High-Level Design Approach
This section describes what your solution should look like, not how to implement it.
Program 1 Architecture: Arithmetic Calculator
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Module โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ func $add(a: i32, b: i32) -> i32 โ
โ Stack: [a, b] โ [a+b] โ
โ โ
โ func $sub(a: i32, b: i32) -> i32 โ
โ Stack: [a, b] โ [a-b] โ
โ โ
โ func $mul(a: i32, b: i32) -> i32 โ
โ Stack: [a, b] โ [a*b] โ
โ โ
โ func $div(a: i32, b: i32) -> i32 โ
โ Stack: [a, b] โ [a/b] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ exports: add, sub, mul, div โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Each function: get both params โ perform operation โ result is return value
Program 2 Architecture: Fibonacci
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Recursive Version โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ func $fib(n: i32) -> i32 โ
โ if n < 2: โ
โ return n โ
โ else: โ
โ return fib(n-1) + fib(n-2) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Iterative Version โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ func $fib_iter(n: i32) -> i32 โ
โ locals: prev=0, curr=1, temp โ
โ loop n times: โ
โ temp = curr โ
โ curr = prev + curr โ
โ prev = temp โ
โ return prev โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Recursive version: Uses WASMโs call instruction to call itself Iterative version: Uses loop construct with local variables
Program 3 Architecture: Factorial
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ func $factorial(n: i32) -> i64 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ locals: result: i64 = 1 โ
โ โ
โ loop while n > 0: โ
โ result = result * (n as i64) โ
โ n = n - 1 โ
โ โ
โ return result โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Note: Multiplication needs i64.mul, requiring conversion from i32 counter
Program 4 Architecture: String Reverser
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Memory Layout โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Offset 0: [H][e][l][l][o]... โ
โ โ โ โ
โ start start+len-1 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ func $reverse(offset: i32, len: i32) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ locals: left, right, temp โ
โ โ
โ left = offset โ
โ right = offset + len - 1 โ
โ โ
โ while left < right: โ
โ temp = load8(left) โ
โ store8(left, load8(right)) โ
โ store8(right, temp) โ
โ left++, right-- โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Key operations: i32.load8_u to read bytes, i32.store8 to write bytes
Program 5 Architecture: Host Integration
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Imports โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ "env" "print_i32" : (i32) -> () โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ func $count_up(n: i32) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ locals: i = 1 โ
โ โ
โ loop while i <= n: โ
โ call $print_i32(i) โ
โ i++ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Implementation Guide
Phase 1: Environment Setup (30 minutes)
- Install wabt (WebAssembly Binary Toolkit)
# macOS brew install wabt # Ubuntu sudo apt install wabt # Verify wat2wasm --version - Create project structure
wat-programs/ โโโ arithmetic.wat โโโ fibonacci.wat โโโ factorial.wat โโโ string_reverse.wat โโโ logger.wat โโโ test.html (or test.js for Node) -
Set up your test harness
For browser (test.html):
<script type="module"> const wasmBytes = await fetch('arithmetic.wasm').then(r => r.arrayBuffer()); const { instance } = await WebAssembly.instantiate(wasmBytes); console.log(instance.exports.add(3, 4)); // Should print 7 </script>For Node.js (test.js):
const fs = require('fs'); const wasmBytes = fs.readFileSync('arithmetic.wasm'); const { instance } = await WebAssembly.instantiate(wasmBytes); console.log(instance.exports.add(3, 4));
Phase 2: Arithmetic Calculator (1-2 hours)
Guidance without spoilers:
-
Start with the module wrapper:
(module ... ) - Define the
addfunction:- Two i32 parameters
- One i32 result
- Body: get first param, get second param, add them
-
Hint for stack thinking: After
local.get $a, the stack is[a]. Afterlocal.get $b, itโs[a, b]. Afteri32.add, itโs[a+b]. This single value IS the return value. -
Add the export:
(export "add" (func $add)) -
Compile and test:
wat2wasm arithmetic.wat -o arithmetic.wasm - Repeat for sub, mul, div
Common mistake: Forgetting that i32.div_s vs i32.div_u matters for negative numbers. Use i32.div_s for signed division.
Phase 3: Fibonacci (2-3 hours)
Guidance without spoilers:
- Recursive version first (itโs more intuitive):
- Base case: if n < 2, return n
- Recursive case: fib(n-1) + fib(n-2)
- Hint for the if structure:
(if (result i32) (i32.lt_s (local.get $n) (i32.const 2)) (then ...) (else ...) )The
(result i32)is crucialโit says โthis if expression produces an i32โ -
Hint for recursion: To call a function, use
(call $fib ...)with the argument on the stack - Iterative version (use loop):
- Youโll need 3 locals: prev, curr, counter
- Use
(loop $continue ... (br $continue))pattern
Common mistake: Forgetting that loop branches go to the START, not the end. You need a block wrapper to exit the loop.
Phase 4: Factorial (1-2 hours)
Guidance without spoilers:
-
Return type is i64 to handle larger results (20! overflows i32)
-
Hint for type conversion: When multiplying, you need
i64.mul. But your counter is i32. Usei64.extend_i32_sto convert. - Loop pattern:
(block $exit (loop $continue ;; exit condition check ;; if done: br $exit ;; body br $continue ) ) - Initialize result to 1, not 0 (multiplication identity)
Common mistake: Off-by-one in loop termination. factorial(0) should return 1.
Phase 5: String Reverser (2-3 hours)
Guidance without spoilers:
-
Declare memory:
(memory (export "memory") 1)โ export it so JS can access - Memory operations for bytes:
i32.load8_uloads an unsigned bytei32.store8stores a byte- First operand is address, second (for store) is value
- JavaScript side:
const memory = instance.exports.memory; const view = new Uint8Array(memory.buffer); // Write "Hello" starting at offset 0 const str = "Hello"; for (let i = 0; i < str.length; i++) { view[i] = str.charCodeAt(i); } // Call reverse instance.exports.reverse(0, str.length); // Read back const result = String.fromCharCode(...view.slice(0, str.length)); // result should be "olleH" - Two-pointer technique: left starts at offset, right at offset+len-1. Swap and move inward.
Common mistake: Forgetting to subtract 1 from right pointer (strings are 0-indexed).
Phase 6: Host Integration (1 hour)
Guidance without spoilers:
- Import syntax:
(import "env" "print_i32" (func $print_i32 (param i32)))The module name is โenvโ, the function name is โprint_i32โ
- JavaScript side:
const imports = { env: { print_i32: (n) => console.log(n) } }; - The loop is similar to factorial but calls
$print_i32each iteration
Common mistake: Import names must match exactly between WAT and JavaScript object keys.
Testing Strategy
Unit Testing Each Program
Arithmetic
assert(add(3, 4) === 7);
assert(sub(10, 3) === 7);
assert(mul(6, 7) === 42);
assert(div(20, 4) === 5);
assert(div(-10, 3) === -3); // Signed division
Fibonacci
assert(fib(0) === 0);
assert(fib(1) === 1);
assert(fib(2) === 1);
assert(fib(10) === 55);
assert(fib(20) === 6765);
Factorial
assert(factorial(0) === 1n);
assert(factorial(1) === 1n);
assert(factorial(5) === 120n);
assert(factorial(20) === 2432902008176640000n); // BigInt in JS
String Reverse
// Write "Hello", call reverse, verify "olleH"
// Write "a", call reverse, verify "a"
// Write "", call reverse, verify ""
// Write "ab", call reverse, verify "ba"
Logger
let output = [];
const imports = { env: { print_i32: n => output.push(n) } };
// ...
count_up(5);
assert(output.join(',') === '1,2,3,4,5');
Validation Testing
Use wasm-validate from wabt:
wasm-validate arithmetic.wasm && echo "Valid!"
Manual Stack Tracing
For each function, trace the stack by hand. Example for add(3, 4):
local.get $a ; stack: [3]
local.get $b ; stack: [3, 4]
i32.add ; stack: [7]
; function returns, 7 is the result
Common Pitfalls and Debugging
Pitfall 1: Stack Underflow
Symptom: Validation error โtype mismatchโ or โpop from empty stackโ Cause: Operation expects values that arenโt there Fix: Trace your stack carefully. Every operation that consumes values must have them available.
Pitfall 2: Stack Not Empty at Block End
Symptom: โtype mismatchโ at end of block or function
Cause: Values left on stack that arenโt consumed
Fix: A function with (result i32) must leave exactly one i32 on the stack. No more, no less.
Pitfall 3: Type Mismatch
Symptom: โtype mismatch: expected i64, got i32โ
Cause: Mixing i32 and i64 without conversion
Fix: Use i64.extend_i32_s or i32.wrap_i64 to convert
Pitfall 4: Loop Goes Forever
Symptom: Browser hangs, no output
Cause: br $loop goes to start; thereโs no exit path
Fix: Wrap loop in block, use conditional br_if $exit before br $loop
Pitfall 5: Memory Access Out of Bounds
Symptom: Runtime error โout of bounds memory accessโ Cause: Address exceeds memory size Fix: 1 page = 65536 bytes. Ensure your addresses stay within bounds.
Debugging Technique: wasm2wat
If somethingโs wrong, convert back to text and inspect:
wat2wasm myprogram.wat -o myprogram.wasm
wasm2wat myprogram.wasm -o myprogram_roundtrip.wat
Compare the roundtrip version to your original. Sometimes the differences reveal misunderstandings.
Extensions and Challenges
Extension 1: Stack Calculator
Build a RPN (Reverse Polish Notation) calculator:
- Store operations in memory as bytecodes
- Interpret them using a loop
- Youโre building a tiny interpreter inside WASM!
Extension 2: Multiple Return Values
WASM supports functions returning multiple values:
(func $divmod (param $a i32) (param $b i32) (result i32 i32)
(i32.div_s (local.get $a) (local.get $b))
(i32.rem_s (local.get $a) (local.get $b))
)
Implement and call from JavaScript.
Extension 3: Indirect Calls (Function Tables)
Create a table of functions and call them by index:
(table 2 funcref)
(elem (i32.const 0) $add $mul)
(call_indirect (type $binop) (local.get $index))
This is how WASM implements function pointers.
Extension 4: Memory Allocator
Build a simple bump allocator:
- Track current allocation pointer
- Export
alloc(size) -> ptr - Export
free()(just reset pointer)
Extension 5: ASCII Mandelbrot
Compute Mandelbrot set and write ASCII art to memory. JavaScript reads and displays it. Exercises complex loops and memory extensively.
The Core Question Youโre Answering
โWhat IS a stack machine, and how does executing code without named variables force you to think differently about computation?โ
Before you write any code, sit with this question. Most programmers have only ever thought in terms of registers or named variablesโx = 3; y = 4; result = x + y. But WebAssembly forces a different mental model: values exist only on a stack, unnamed and ephemeral. Understanding this shift is the key insight of this project.
Ask yourself:
- How do you express
(a + b) * (c - d)when you cannot name intermediate results? - Why would language designers choose this constraint deliberately?
- What does it mean for a stack to have a โtypeโ at every instruction?
Concepts You Must Understand First
Stop and research these before coding:
- Stack Machine Execution
- What happens when an instruction โconsumesโ operands?
- Why is stack depth deterministic at every program point?
- How do stack machines differ from register machines in terms of instruction encoding?
- Book Reference: โEngineering a Compilerโ Ch. 7 (Stack-Based Evaluation) - Cooper & Torczon
- S-Expression Syntax
- Why do Lisp, Scheme, and WAT all use parenthetical notation?
- What is the relationship between S-expressions and abstract syntax trees?
- How does the folded form
(i32.add (local.get $a) (local.get $b))relate to the flat form? - Book Reference: โWebAssembly: The Definitive Guideโ Ch. 2 (WebAssembly Text Format) - Brian Sletten
- WASM Type System (i32, i64, f32, f64)
- Why exactly four types? Whatโs the design philosophy?
- How does signedness work if types donโt encode it?
- What happens when you need to convert between types?
- Book Reference: โComputer Systems: A Programmerโs Perspectiveโ Ch. 2 (Representing Information) - Bryant & OโHallaron
- Structured Control Flow
- Why no
goto? What problem does structured control flow solve? - How do
block,loop, andifdiffer in their branch semantics? - What is โbranch depthโ and why use relative references?
- Book Reference: โEngineering a Compilerโ Ch. 8 (Control Flow) - Cooper & Torczon
- Why no
- Linear Memory Model
- What does โlinearโ mean in โlinear memoryโ?
- How do byte addresses relate to typed loads/stores?
- What happens at the boundary between WASM memory and JavaScript?
- Book Reference: โLow-Level Programmingโ Ch. 3 (Memory Layout) - Igor Zhirkov
Questions to Guide Your Design
Before implementing, think through these:
- Stack Discipline
- For each function, what must be on the stack when it returns?
- If a function declares
(result i32), how do you ensure exactly one i32 remains? - What happens if you push two values but only consume one?
- Data Flow
- How do you pass values between the stack and local variables?
- When should you use
local.teeinstead of separate get/set operations? - How do you trace data flow through nested expressions?
- Control Flow Translation
- How would you translate a
whileloop from a high-level language? - How do you express โbreak out of loopโ vs. โcontinue to next iterationโ?
- What is the pattern for implementing a for-loop with a counter?
- How would you translate a
- Memory Layout
- Where in memory should your string data live?
- How do you track the boundary between โusedโ and โfreeโ memory?
- What alignment constraints should you respect?
- Host Integration
- What types can cross the WASM/JavaScript boundary?
- How do you pass strings when only numbers can be exported?
- What happens if an import function is missing?
Thinking Exercise
Before coding, trace this by hand:
Consider this WAT function that computes max(a, b):
(func $max (param $a i32) (param $b i32) (result i32)
(if (result i32) (i32.gt_s (local.get $a) (local.get $b))
(then (local.get $a))
(else (local.get $b))
)
)
Trace the execution for max(5, 3):
| Step | Instruction | Stack Before | Stack After | Notes |
|---|---|---|---|---|
| 1 | Enter function | [] | [] | $a=5, $b=3 in locals |
| 2 | local.get $a | [] | [5] | For comparison |
| 3 | local.get $b | [5] | [5, 3] | For comparison |
| 4 | i32.gt_s | [5, 3] | [1] | 5 > 3 is true (1) |
| 5 | if branch | [1] | [] | Condition consumed, take then |
| 6 | local.get $a | [] | [5] | Then branch executes |
| 7 | end if | [5] | [5] | If produces result |
| 8 | Return | [5] | [] | 5 is return value |
Now trace max(2, 7) yourself:
| Step | Instruction | Stack Before | Stack After | Notes |
|---|---|---|---|---|
| 1 | ย | ย | ย | ย |
| 2 | ย | ย | ย | ย |
| โฆ | ย | ย | ย | ย |
Questions while tracing:
- At step 4, what would the stack contain if a=3 and b=5?
- Why does the
ifstatement need(result i32)? - What would happen if the
thenbranch pushed two values?
The Interview Questions Theyโll Ask
Prepare to answer these:
- โExplain the difference between a stack machine and a register machine. Why did WebAssembly choose a stack-based model?โ
- Key insight: Portability, compact encoding, and easy validation
- โIf WebAssembly has only four numeric types, how do you represent strings, arrays, or structs?โ
- Key insight: Linear memory + JavaScript glue code
- โWhy doesnโt WebAssembly have a
gotoinstruction? How does structured control flow benefit security and validation?โ- Key insight: Prevents ROP attacks, enables streaming validation
- โWalk me through what happens when JavaScript calls a WebAssembly function and receives a result.โ
- Key insight: Type coercion, stack cleanup, trap handling
- โHow would you debug a WebAssembly module thatโs producing incorrect results?โ
- Key insight: wasm2wat disassembly, stack tracing, browser dev tools
- โWhat is the WebAssembly linear memory model and why canโt WASM access JavaScript objects directly?โ
- Key insight: Sandboxing, capability-based security, explicit imports
- โHow does WebAssembly achieve near-native performance despite running in a browser?โ
- Key insight: AOT/JIT compilation, no GC pauses, predictable types
Hints in Layers
Hint 1: Starting Your First Function
Every WAT file begins with (module ...). Inside, define functions with (func $name ...). Parameters and results go before the body. The body is a sequence of instructions that must leave the right number of values on the stack.
Hint 2: Stack Balance
If your function has (result i32), the body must leave exactly one i32 on the stack. Use local.get to push values, arithmetic to transform them. The final value IS your return valueโno explicit return needed.
Hint 3: Loop Pattern
WASM loops are unintuitive. br $loop jumps to the START of the loop. To exit, wrap in a block:
(block $exit
(loop $continue
;; ... your code ...
(br_if $exit (condition)) ;; exit if true
(br $continue) ;; otherwise loop
)
)
Hint 4: Memory Operations
For the string reverser, remember that i32.load8_u takes an address from the stack and pushes a byte value. i32.store8 takes an address AND a value. Order matters!
;; Load: push address, then load
(i32.load8_u (i32.const 0)) ;; loads byte at address 0
;; Store: push address, push value, then store
(i32.store8 (i32.const 0) (i32.const 65)) ;; stores 'A' at address 0
Hint 5: Debugging Type Errors If you get โtype mismatch,โ trace your stack types at each instruction. Write them in comments:
local.get $n ;; stack: [i32]
i32.const 1 ;; stack: [i32, i32]
i32.sub ;; stack: [i32]
i64.extend_i32_s ;; stack: [i64] <-- now it's i64!
Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| WebAssembly fundamentals | WebAssembly: The Definitive Guide by Brian Sletten | Ch. 1-4 (WAT format, modules, memory) |
| Stack machine theory | Engineering a Compiler by Cooper & Torczon | Ch. 7 (Code Generation), Ch. 8 (Control Flow) |
| Low-level memory concepts | Computer Systems: A Programmerโs Perspective by Bryant & OโHallaron | Ch. 2 (Data Representations), Ch. 3 (Machine-Level Programs) |
| Assembly and memory layout | Low-Level Programming by Igor Zhirkov | Ch. 1-5 (Assembly basics, memory, stack) |
| Compilation targets | Crafting Interpreters by Robert Nystrom | Ch. 14-15 (Bytecode, Virtual Machine) |
| Binary formats | Linkers and Loaders by John Levine | Ch. 3-4 (Object file formats) |
Real-World Outcome
After completing this project, you will have five working WebAssembly programs that you compiled and tested yourself. Here is exactly what you should see when running your programs:
Compiling Your WAT Files
$ wat2wasm arithmetic.wat -o arithmetic.wasm
$ wat2wasm fibonacci.wat -o fibonacci.wasm
$ wat2wasm factorial.wat -o factorial.wasm
$ wat2wasm string_reverse.wat -o string_reverse.wasm
$ wat2wasm logger.wat -o logger.wasm
# Verify they compiled correctly
$ ls -la *.wasm
-rw-r--r-- 1 user staff 87 Dec 27 10:15 arithmetic.wasm
-rw-r--r-- 1 user staff 124 Dec 27 10:15 factorial.wasm
-rw-r--r-- 1 user staff 156 Dec 27 10:15 fibonacci.wasm
-rw-r--r-- 1 user staff 98 Dec 27 10:15 logger.wasm
-rw-r--r-- 1 user staff 142 Dec 27 10:15 string_reverse.wasm
# Validate the modules
$ wasm-validate arithmetic.wasm && echo "Valid!"
Valid!
Running the Arithmetic Calculator (Node.js)
$ node test_arithmetic.js
Testing arithmetic.wasm...
add(3, 4) = 7 โ
sub(10, 3) = 7 โ
mul(6, 7) = 42 โ
div(20, 4) = 5 โ
div(-10, 3) = -3 โ (signed division)
All arithmetic tests passed!
Running the Fibonacci Calculator
$ node test_fibonacci.js
Testing fibonacci.wasm...
fib(0) = 0 โ
fib(1) = 1 โ
fib(2) = 1 โ
fib(5) = 5 โ
fib(10) = 55 โ
fib(20) = 6765 โ
Fibonacci tests passed!
Running the Factorial Calculator
$ node test_factorial.js
Testing factorial.wasm...
factorial(0) = 1n โ
factorial(1) = 1n โ
factorial(5) = 120n โ
factorial(10) = 3628800n โ
factorial(20) = 2432902008176640000n โ
Factorial tests passed!
Running the String Reverser
$ node test_string_reverse.js
Testing string_reverse.wasm...
reverse("Hello") = "olleH" โ
reverse("a") = "a" โ
reverse("ab") = "ba" โ
reverse("WebAssembly") = "ylbmessAbeW" โ
String reversal tests passed!
Running the Logger (Host Integration)
$ node test_logger.js
Testing logger.wasm (host integration)...
Calling count_up(5):
1
2
3
4
5
Logger tests passed!
Browser Console Output
When running in a browser via test.html:
[Console Output]
> Loading arithmetic.wasm...
> Module loaded successfully
> Exports available: add, sub, mul, div
> Testing: add(100, 200) = 300
> Testing: mul(12, 12) = 144
> All browser tests complete!
Disassembling to Verify Structure
$ wasm2wat arithmetic.wasm
(module
(type (;0;) (func (param i32 i32) (result i32)))
(func (;0;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(func (;1;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.sub)
(func (;2;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.mul)
(func (;3;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.div_s)
(export "add" (func 0))
(export "sub" (func 1))
(export "mul" (func 2))
(export "div" (func 3)))
This round-trip demonstrates that your hand-written WAT compiles to valid binary and can be disassembled back to equivalent text form.
Real-World Connections
Why This Matters for Modern Web
-
Performance-critical libraries: Image processing, video codecs, physics engines use WASM because it runs at near-native speed in browsers.
-
Language diversity: Games written in C++, simulations in Rust, tools in Goโall compile to WASM and run in browsers.
-
Consistent performance: Unlike JavaScript, WASM execution is predictable. No JIT surprises or GC pauses mid-frame.
Why This Matters Beyond Web
-
Edge computing: Cloudflare Workers, Fastly Compute run WASM at the edge. Understanding WASM = understanding this infrastructure.
-
Plugin systems: Figma, VS Code extensions, database UDFs use WASM for sandboxed plugins.
-
Blockchain: Smart contracts on many chains compile to WASM (Near, Polkadot, Cosmos).
How Production Compilers Work
When you compile C to WASM, clang/LLVM:
- Parses C into LLVM IR
- Optimizes the IR
- Generates WASM instructions (exactly like what you wrote!)
- Encodes to binary format
Your hand-written WAT IS what production compilers produce. The only difference is they do it automatically.
Resources
Primary References
- MDN: Understanding WebAssembly Text Format
- WebAssembly Specification ยง4: Execution
- wabt GitHub - Binary toolkit
Interactive Tools
- WebAssembly Studio - Online IDE for WAT
- WasmFiddle - Quick testing
- wat2wasm demo - Online compiler
Deep Dives
- Lin Clarkโs Illustrated Guide to WebAssembly - Excellent visuals
- WebAssembly Reference Manual - Community guide
Self-Assessment Checklist
Before moving to Project 2, verify you can:
- Write a function from scratch without copying examples
- Predict the stack state at any instruction
- Explain why WASM has no goto
- Read and write linear memory from both WASM and JavaScript
- Debug a โtype mismatchโ error by tracing stack types
- Convert between flat and folded WAT syntax
- Explain what happens at the import/export boundary
Conceptual Questions (Answer Without Looking)
- Whatโs on the stack after
i32.const 5; i32.const 3; i32.sub? - Why does
loopbranch to its start whileblockbranches to its end? - How do you exit a loop in WASM?
- Whatโs the size in bytes of one WASM memory page?
- If a function has
(result i32 i32), how many values must be on the stack at return?
Next: P02: Build a WASM Binary Parser โ understand the binary format that your WAT compiles to