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:

  1. Explain stack-based execution - Describe how operations push and pop values without named variables
  2. Write valid WAT syntax - Use S-expressions to define functions, locals, and control flow
  3. Trace WASM execution mentally - Follow stack state through any sequence of instructions
  4. Use linear memory - Load and store values in WASMโ€™s flat memory model
  5. Interface with JavaScript - Import and export functions between WASM and the host
  6. 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:

  1. Compact encoding: No register names to encode
  2. Easy validation: Stack typing is deterministic
  3. Portability: No target-specific register allocation
  4. 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?

  1. Validation: Can statically determine stack types at any point
  2. Security: No arbitrary jumps = no ROP attacks
  3. 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 values
  • i32.store, i64.store, f32.store, f64.store - Store values
  • i32.load8_s, i32.load8_u, etc. - Load partial values with sign extension
  • memory.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 wat2wasm without 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             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebAssembly Arithmetic Calculator Module Architecture

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                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebAssembly Fibonacci Implementations - Recursive and Iterative

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                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebAssembly Factorial Function with Loop

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--                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebAssembly String Reverser Memory Layout and Algorithm

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++                                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebAssembly Host Integration with Imports and Function Calls


Implementation Guide

Phase 1: Environment Setup (30 minutes)

  1. Install wabt (WebAssembly Binary Toolkit)
    # macOS
    brew install wabt
    
    # Ubuntu
    sudo apt install wabt
    
    # Verify
    wat2wasm --version
    
  2. Create project structure
    wat-programs/
    โ”œโ”€โ”€ arithmetic.wat
    โ”œโ”€โ”€ fibonacci.wat
    โ”œโ”€โ”€ factorial.wat
    โ”œโ”€โ”€ string_reverse.wat
    โ”œโ”€โ”€ logger.wat
    โ””โ”€โ”€ test.html (or test.js for Node)
    
  3. 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:

  1. Start with the module wrapper: (module ... )

  2. Define the add function:
    • Two i32 parameters
    • One i32 result
    • Body: get first param, get second param, add them
  3. Hint for stack thinking: After local.get $a, the stack is [a]. After local.get $b, itโ€™s [a, b]. After i32.add, itโ€™s [a+b]. This single value IS the return value.

  4. Add the export: (export "add" (func $add))

  5. Compile and test: wat2wasm arithmetic.wat -o arithmetic.wasm

  6. 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:

  1. Recursive version first (itโ€™s more intuitive):
    • Base case: if n < 2, return n
    • Recursive case: fib(n-1) + fib(n-2)
  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โ€

  3. Hint for recursion: To call a function, use (call $fib ...) with the argument on the stack

  4. 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:

  1. Return type is i64 to handle larger results (20! overflows i32)

  2. Hint for type conversion: When multiplying, you need i64.mul. But your counter is i32. Use i64.extend_i32_s to convert.

  3. Loop pattern:
    (block $exit
      (loop $continue
        ;; exit condition check
        ;; if done: br $exit
        ;; body
        br $continue
      )
    )
    
  4. 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:

  1. Declare memory: (memory (export "memory") 1) โ€” export it so JS can access

  2. Memory operations for bytes:
    • i32.load8_u loads an unsigned byte
    • i32.store8 stores a byte
    • First operand is address, second (for store) is value
  3. 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"
    
  4. 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:

  1. Import syntax:
    (import "env" "print_i32" (func $print_i32 (param i32)))
    

    The module name is โ€œenvโ€, the function name is โ€œprint_i32โ€

  2. JavaScript side:
    const imports = {
      env: {
        print_i32: (n) => console.log(n)
      }
    };
    
  3. The loop is similar to factorial but calls $print_i32 each 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:

  1. 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
  2. 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
  3. 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
  4. Structured Control Flow
    • Why no goto? What problem does structured control flow solve?
    • How do block, loop, and if differ in their branch semantics?
    • What is โ€œbranch depthโ€ and why use relative references?
    • Book Reference: โ€œEngineering a Compilerโ€ Ch. 8 (Control Flow) - Cooper & Torczon
  5. 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:

  1. 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?
  2. Data Flow
    • How do you pass values between the stack and local variables?
    • When should you use local.tee instead of separate get/set operations?
    • How do you trace data flow through nested expressions?
  3. Control Flow Translation
    • How would you translate a while loop 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?
  4. 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?
  5. 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 if statement need (result i32)?
  • What would happen if the then branch pushed two values?

The Interview Questions Theyโ€™ll Ask

Prepare to answer these:

  1. โ€œ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
  2. โ€œIf WebAssembly has only four numeric types, how do you represent strings, arrays, or structs?โ€
    • Key insight: Linear memory + JavaScript glue code
  3. โ€œWhy doesnโ€™t WebAssembly have a goto instruction? How does structured control flow benefit security and validation?โ€
    • Key insight: Prevents ROP attacks, enables streaming validation
  4. โ€œWalk me through what happens when JavaScript calls a WebAssembly function and receives a result.โ€
    • Key insight: Type coercion, stack cleanup, trap handling
  5. โ€œHow would you debug a WebAssembly module thatโ€™s producing incorrect results?โ€
    • Key insight: wasm2wat disassembly, stack tracing, browser dev tools
  6. โ€œWhat is the WebAssembly linear memory model and why canโ€™t WASM access JavaScript objects directly?โ€
    • Key insight: Sandboxing, capability-based security, explicit imports
  7. โ€œ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

  1. Performance-critical libraries: Image processing, video codecs, physics engines use WASM because it runs at near-native speed in browsers.

  2. Language diversity: Games written in C++, simulations in Rust, tools in Goโ€”all compile to WASM and run in browsers.

  3. Consistent performance: Unlike JavaScript, WASM execution is predictable. No JIT surprises or GC pauses mid-frame.

Why This Matters Beyond Web

  1. Edge computing: Cloudflare Workers, Fastly Compute run WASM at the edge. Understanding WASM = understanding this infrastructure.

  2. Plugin systems: Figma, VS Code extensions, database UDFs use WASM for sandboxed plugins.

  3. Blockchain: Smart contracts on many chains compile to WASM (Near, Polkadot, Cosmos).

How Production Compilers Work

When you compile C to WASM, clang/LLVM:

  1. Parses C into LLVM IR
  2. Optimizes the IR
  3. Generates WASM instructions (exactly like what you wrote!)
  4. Encodes to binary format

Your hand-written WAT IS what production compilers produce. The only difference is they do it automatically.


Resources

Primary References

Interactive Tools

Deep Dives


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)

  1. Whatโ€™s on the stack after i32.const 5; i32.const 3; i32.sub?
  2. Why does loop branch to its start while block branches to its end?
  3. How do you exit a loop in WASM?
  4. Whatโ€™s the size in bytes of one WASM memory page?
  5. 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