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 —
wasmirout to the WASM binary representation - Decode — WASM binary back into
wasmir
Installing and running the CLI
go install github.com/eliben/watgo/cmd/watgo@latestThe 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). |



