wasm — Quick start¶
This walks through the matchlen kernel end-to-end: from generator source to
a consumed .wasm binary. The same shape works for the other four kernels
(hex, popcount, toupper, memchr) — swap the package name.
Toolchain
You need Go (any modern release), and
WABT for wat2wasm.
On Debian/Ubuntu: sudo apt install wabt.
On macOS: brew install wabt.
1. Generate the .wat¶
Every kernel in github.com/go-asmgen/asmgen/examples/wasm/ is a
go run command that prints WAT to stdout:
Or, if you're inside a Go module and want it pinned to the exact version your
code was written against, drop a //go:generate directive at package scope
so go generate ./... regenerates it:
//go:generate sh -c "go run github.com/go-asmgen/asmgen/examples/wasm/matchlen@v0.6.0 > matchlen.wat"
The output is a well-formed WAT module — one (func $matchlen16 ...) with
the emit-surface stack ops laid out in evaluation order:
;; Code generated by go-asmgen/asmgen/wasm. DO NOT EDIT.
(module
(import "env" "memory" (memory 0))
(func $matchlen16 (export "matchlen16")
(param $ptrA i32) (param $ptrB i32) (param $limit i32) (result i32)
(local $i i32)
(local $matched i32)
(local $eq v128)
...
2. Compile to .wasm¶
Every kernel in v0.1.0 fits in a small binary: matchlen.wasm is 177 bytes,
memchr.wasm is 135, toupper.wasm is 155. The SIMD proposal is enabled by
default in WABT 1.0.41+.
3. Verify it functions¶
Every kernel has a wazero cross-check that runs the generated .wasm against
a Go reference. From the module root:
Expected output:
[0] OK matchlen("", "", 0) = 0, want 0
[1] OK matchlen("x", "x", 1) = 1, want 1
...
All matchlen cases passed.
The other kernels take the same form (go run . hex ../hex.wasm, etc.). The
verifier ships in a separate submodule so wazero's dependency does not leak
into the main emit surface.
4. Consume it from Go¶
The consumer wires the compiled .wasm into a host runtime and calls the
exported function via //go:wasmimport. See
go-simd/matchlen-wasm for a
production wrapper — the whole surface is under 100 lines of Go plus the
generated matchlen.wat / matchlen.wasm.
The consumer-side pattern in short:
//go:build wasm
package matchlenwasm
//go:wasmimport env matchlen16
func matchlen16(ptrA, ptrB uint32, limit uint32) uint32
func MatchLen(a, b []byte) int { ... }
5. Keep it in sync¶
A committed .wat will drift from the pinned generator over time. Two moves
keep them honest:
- CI drift gate — a job that runs
go generate ./...andgit diff --exit-code. Example:.github/workflows/wasm-drift.ymlin matchlen-wasm. - CI bench gate — a job that runs the kernel under wazero and diffs
benchstat vs the main-branch baseline, failing on statistically-significant
regression. Example:
.github/workflows/wasm-bench.yml.
Both are copy-pastable across consumers — the shape is stable.