watgo is a WebAssembly toolkit for Go: a pure-Go, zero-dependency library and command-line tool for working with WAT and WASM. It occupies the same niche as the C++ wabt and the Rust wasm-tools, and is now generally available.

The package set covers four operations, all built around wasmir, a semantic representation of a WebAssembly module that callers can inspect and modify:

  • Parse — WAT text into wasmir
  • Validate — checks well-formedness and safety using the official WebAssembly validation semantics
  • Encode — wasmir out to the WASM binary representation
  • Decode — WASM binary back into wasmir
Block diagram showing the different parts of watgo; described in the next paragraph

Installing and running the CLI

go install github.com/eliben/watgo/cmd/watgo@latest
The CLI targets compatibility with wasm-tools [1]. A parse, validate and encode pass over a WAT file looks like this:

watgo parse stack.wat -o stack.wasm

Working with the API

Because wasmir models a module semantically rather than syntactically, its API is straightforward to traverse. A short example parses a WAT program and runs some analysis over it:

package main

import (
  "fmt"

  "github.com/eliben/watgo"
  "github.com/eliben/watgo/wasmir"
)

const wasmText = `
(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add
  )
  (func (param f32 i32) (result i32)
    local.get 1
    i32.const 1
    i32.add
    drop
    i32.const 0
  )
)`

func main() {
  m, err := watgo.ParseWAT([]byte(wasmText))
  if err != nil {
    panic(err)
  }

  i32Params := 0
  localGets := 0
  i32Adds := 0

  // Module-defined functions carry a type index into m.Types. The function
  // body itself is a flat sequence of wasmir.Instruction values.
  for _, fn := range m.Funcs {
    sig := m.Types[fn.TypeIdx]
    for _, param := range sig.Params {
      if param.Kind == wasmir.ValueKindI32 {
        i32Params++
      }
    }

    for _, instr := range fn.Body {
      switch instr.Kind {
      case wasmir.InstrLocalGet:
        localGets++
      case wasmir.InstrI32Add:
        i32Adds++
      }
    }
  }

  fmt.Printf("module-defined funcs: %d\n", len(m.Funcs))
  fmt.Printf("i32 params: %d\n", i32Params)
  fmt.Printf("local.get instructions: %d\n", localGets)
  fmt.Printf("i32.add instructions: %d\n", i32Adds)
}

WAT's syntactic conveniences do not survive the lowering step. Folded instructions become unfolded (linear) ones, and function and type names are resolved to numeric indices — the form that matches WASM's validation and execution semantics and its binary encoding. The syntax-aware layer lives in the textformat package, which parses WAT into an AST; those details are dropped on the way down to wasmir. textformat is internal for now, though it may be exposed publicly later.

How correctness is established

Although watgo is young, heavy testing from the outset gives reasonable confidence in it. WebAssembly ships a large official test suite — around 200K lines of WAT spread across modules with expected execution semantics and assorted error cases, held in .wast files that rely on a custom spec interpreter — and watgo reuses it rather than writing its own corpus.

A custom harness reads the .wast files, converts their WAT to binary WASM with watgo, then runs the result on Node.js [2]. Building that harness was substantial work, but the coverage it buys is worth it: watgo passes the complete WASM spec core test suite. In the same spirit, watgo is tested against wabt's interp test suite through a simpler Node-based harness, and against the collection of realistic WAT programs kept in the wasm-wat-samples repository.


[1]Though not all of wasm-tools's functionality is supported yet.
[2]To stick to a pure-Go approach also for testing, I originally tried using wazero for this, but had to give up because wazero doesn't support some of the recent WASM proposals that have already made it into the standard (most notably Garbage Collection).