Skip to content

Instantly share code, notes, and snippets.

@evacchi
Created March 25, 2026 20:46
Show Gist options
  • Select an option

  • Save evacchi/678c66e909f288327d833bd565546811 to your computer and use it in GitHub Desktop.

Select an option

Save evacchi/678c66e909f288327d833bd565546811 to your computer and use it in GitHub Desktop.
Assessment: Reimplementing wazero interpreter on wazevo's SSA

Assessment: Reimplementing the Interpreter on Wazevo's SSA

What exists today

Current interpreter (~11k lines of non-test code in internal/engine/interpreter/):

  • Compiles Wasm bytecode into a flat array of unionOperation instructions (a custom IR)
  • Executes via a giant switch statement in callNativeFunc() over 82+ operation kinds
  • Uses a uint64 value stack and a call frame stack
  • Control flow is resolved to absolute PC addresses at compile time
  • Two-pass compilation: first translates opcodes, then resolves label addresses

Wazevo SSA (internal/engine/wazevo/ssa/ + frontend/):

  • Full SSA IR with ~100+ opcodes, basic blocks, block arguments (PHI equivalent), and value types
  • Complete Wasm-to-SSA frontend (lower.go, ~4,360 lines) handling 468+ Wasm opcodes
  • Optimization passes: dead code elimination, redundant PHI elimination, block layout
  • Currently feeds into architecture-specific backends (arm64/amd64) for native code generation

What "interpreter on SSA" means

You'd replace the current interpreter's custom IR + compiler with the wazevo SSA frontend, then walk/interpret the SSA IR instead of lowering to machine code. The work breaks down into:

1. The SSA interpreter core (large effort)

You need a new execution engine that walks SSA basic blocks and interprets SSA instructions. This is architecturally different from the current interpreter:

  • Current: linear PC-based dispatch over a flat instruction array
  • SSA: graph-based dispatch over basic blocks, following successor edges for control flow, with values addressed by SSA Value IDs rather than stack positions

You'd need:

  • A value map (map[ssa.Value]uint64 or a dense array) instead of a stack, since SSA values are named, not stack-based
  • Block traversal logic: maintain a "current block" pointer, iterate instructions in a block, then follow successor edges based on branch/jump targets
  • Argument passing to blocks: when jumping to a block with parameters (PHI nodes), copy the branch arguments into the target block's parameter values
  • A dispatch loop over the ~100+ SSA opcodes — roughly comparable in size to the current interpreter's switch, but operating on different data structures

Estimated scope: ~3,000-4,000 lines for the core interpreter loop, comparable to the current interpreter.go (4,669 lines) but somewhat simpler because SSA is more regular (no stack depth tracking, no label resolution).

2. Runtime plumbing (medium effort)

The wazevo SSA frontend bakes in assumptions about the compiler backend:

  • executionContextPtr and moduleContextPtr: The first two parameters of every SSA function are pointers to runtime structures. In a native backend, these are registers/memory addresses. In an interpreter, you'd need to simulate these — either by providing actual Go pointers or by intercepting SSA instructions that load from these pointers (e.g., memory base, table base, global values, imported function pointers).
  • Memory bounds checks: The frontend generates SSA instructions for bounds checking that exit with trap codes. You'd need to implement the OpcodeExitWithCode / OpcodeExitIfTrueWithCode semantics.
  • reloadAfterCall(): After function calls, the frontend reloads memory base/length because the callee might have grown memory. Your interpreter needs to handle this.
  • Host function calls: The SSA uses CallIndirect for imported functions, loading function pointers from module context. You'd need to intercept these and dispatch to Go host functions.

Estimated scope: ~1,000-2,000 lines of runtime integration code.

3. Leveraging existing code (significant savings)

The big win is you get the entire Wasm-to-SSA frontend for free — that's the frontend/lower.go (~4,360 lines) plus the SSA builder infrastructure. This is the equivalent of the current interpreter's compiler.go (3,675 lines), but more complete and already maintained for wazevo.

You'd also inherit:

  • SSA optimization passes (dead code elimination, redundant PHI elimination) which could improve interpreter performance by reducing the number of instructions to execute
  • The type system and validation
  • Full coverage of Wasm features (SIMD, atomics, bulk memory, tail calls, etc.)

4. What you'd delete

The current interpreter's compiler.go (3,675 lines), operations.go (2,845 lines), and signature.go (767 lines) would become unnecessary — roughly 7,300 lines replaced by the shared wazevo frontend.

Summary

Component Effort Lines (estimate)
SSA interpreter dispatch loop Large ~3,000-4,000 new
Runtime/context plumbing Medium ~1,000-2,000 new
Wasm-to-SSA compilation Free 0 (reuse frontend)
Delete current interpreter IR Deletion -7,300 removed
Test adaptation Medium ~2,000-3,000 modified

Net result: Roughly 4,000-6,000 lines of new code, while deleting ~7,300 lines of IR/compiler code. Total codebase size would likely shrink slightly.

Key risks and challenges

  1. Performance: An SSA interpreter is slower than a stack-based one for interpretation. SSA values require random access (array/map lookup) vs. the current stack which has great cache locality. You'd be trading interpretation speed for code sharing with wazevo.

  2. The executionContextPtr / moduleContextPtr abstraction gap: The SSA frontend generates loads and stores against opaque pointer offsets (e.g., "load memory base from moduleCtx+offset"). A native backend just emits loads; an interpreter needs to understand what's at those offsets and either provide real memory or intercept the operations. This is the gnarliest part of the integration.

  3. Call convention mismatch: The SSA assumes a specific calling convention with hidden parameters. You'd need to bridge between "Wasm function call" and "SSA function with 2 hidden pointer parameters."

  4. Optimization pass assumptions: Some SSA passes (like block layout, critical edge splitting) are tuned for code generation, not interpretation. You might need to skip or adapt some passes.

Bottom line

This is a medium-large project. The main value proposition is unifying the Wasm-to-IR compilation so you don't maintain two separate frontends. The main cost is building the SSA interpreter core and bridging the runtime abstraction gap that the SSA frontend assumes a native code backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment