Converting WAT to Wasm with wat2wasm
This guide covers the assembler at the centre of every hand-written WebAssembly workflow: turning a
.wat file into a .wasm binary, enabling the proposals a module uses, reading the errors when it
refuses, and going back the other way to inspect something a compiler produced.
wat2wasm is small and unglamorous, and it is the tool that makes the text format useful rather than
decorative. Feature-detection probes, test fixtures, minimal reproductions for engine bugs and
teaching examples all start here.
Prerequisites
- [ ]
wabtinstalled —wat2wasm,wasm2wat,wasm-validate(wat2wasm --version) - [ ] A
.watfile, or a.wasmyou want to read - [ ]
xxdor similar, if you intend to embed the bytes in JavaScript - [ ] Knowledge of which proposals your module uses, because they are opt-in
Step-by-step procedure
1. Assemble
wat2wasm add.wat -o add.wasm
wasm-validate add.wasm && echo 'valid'
Assembly does not imply validity — wat2wasm will produce output that a stricter validator rejects in
some edge cases, so running wasm-validate afterwards is a habit worth having.
2. Enable the proposals the module uses
wat2wasm --enable-simd simd-probe.wat -o simd-probe.wasm
wat2wasm --enable-threads --enable-bulk-memory shared.wat -o shared.wasm
wat2wasm --enable-all experimental.wat -o experimental.wasm
--enable-all is convenient while iterating and a poor default for a build, because it will happily
assemble a module using instructions your target engines do not support.
3. Read the errors
The assembler reports a line, a column and a caret, which makes most mistakes obvious:
add.wat:4:5: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
i32.add
^^^^^^^
That message is a stack-depth error: one operand was missing at that instruction. Tracking the stack in comments as you write is the practical prevention.
4. Disassemble a compiler’s output
wasm2wat app_bg.wasm -o app.wat
wasm2wat --fold-exprs app_bg.wasm | head -40
--fold-exprs produces the nested s-expression form, which is easier to read for following data flow;
the flat default is easier for reasoning about stack effects.
5. Emit bytes for embedding
wat2wasm --enable-simd probe.wat -o probe.wasm
xxd -i probe.wasm > probe.h # C-style array
node -e "console.log(JSON.stringify([...require('fs').readFileSync('probe.wasm')]))"
Generating the array in the build rather than pasting it once is what keeps an embedded probe correct after an edit.
6. Wire it into the build
{
"scripts": {
"build:fixtures": "for f in test/fixtures/*.wat; do wat2wasm --enable-simd \"$f\" -o \"${f%.wat}.wasm\"; done"
}
}
Expected output
$ wat2wasm add.wat -o add.wasm && ls -l add.wasm
-rw-r--r-- 1 dev dev 41 add.wasm
$ wasm2wat add.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)
(export "add" (func 0)))
Forty-one bytes for a complete module with one exported function is a useful thing to have seen: the format’s overhead is genuinely small, which is why hand-built probe modules cost nothing to embed.
Notice also what the round trip lost. The original file’s $a and $b parameter names became 0 and
1, because names live in a custom section the assembler does not emit by default. The behaviour is
identical; the readability is not.
Gotchas
error: unexpected token on valid-looking code. Usually a proposal that needs enabling — v128,
memory.copy, an atomic instruction. The assembler will not guess.
A validator accepts what an engine rejects. The engine may not support a proposal the assembler does. Test in the target runtime, not just the toolchain.
The disassembled output does not match the source. Names and comments are lost. Keep the .wat in
the repository as the source; treat disassembly of a compiler’s output as read-only.
Embedded bytes drift from the source. They were pasted once and the .wat changed afterwards.
Generate them in the build.
wat2wasm succeeds and the module traps immediately. Assembly checks syntax and types, not logic. A
start function that traps assembles perfectly.
Performance note
Assembly is instant for the module sizes anyone writes by hand — a few milliseconds — so there is no reason to cache or skip it in a build. Where the tool has a performance role at all is in the artifacts it produces: a hand-written probe module for feature detection is thirty bytes, which is what makes validating one cheap enough to run unconditionally at startup.
The other place it pays is fixtures. A test that needs a module with a specific, deliberate shape — an
import that is missing, a memory with a small maximum, a function that traps on the second call — is far
easier to write in WAT than to coax a compiler into producing, and the resulting .wasm is stable
across toolchain upgrades in a way a compiled fixture is not.
Building a fixture library
The most valuable thing this tool produces is a set of small, deliberate modules that exercise exactly one behaviour each. A handful of them makes testing a host — a loader, a runtime embedding, an error handler — straightforward in a way that compiled artifacts never quite manage.
A useful starter set, all of them under twenty lines:
fixtures/
minimal.wat a module with no imports, one export, no memory
traps.wat an export whose body is `unreachable`
needs-import.wat imports env.log; instantiating without it must LinkError
grows.wat exports a function that calls memory.grow
big-memory.wat declares a memory with a large minimum
simd-probe.wat one v128 instruction, for feature detection
invalid.wasm a truncated binary, for testing your error path
Each one answers a question a compiled module answers ambiguously. Does the loader surface a LinkError
distinctly from a CompileError? Does the code that caches views survive a growth? Does the error path
show a useful message when the bytes are corrupt? Those are exactly the paths that are hard to trigger
with a real module and trivial with a purpose-built one.
Build them in the test setup rather than committing the binaries, so the fixtures stay readable and a toolchain upgrade does not leave stale artifacts behind:
{
"scripts": {
"pretest": "for f in test/fixtures/*.wat; do wat2wasm --enable-simd \"$f\" -o \"${f%.wat}.wasm\"; done"
}
}
The exception is the deliberately invalid file, which cannot be assembled by definition — that one is generated by truncating a valid binary, or committed as a small blob with a comment explaining what is wrong with it.
Reading someone else’s module
The reverse direction is the one you will use more often. Disassembling an unfamiliar .wasm answers
three questions quickly: what does it import, what does it export, and how large is its memory. Those
three determine what the module can reach and what it will cost you to run.
wasm2wat module.wasm | grep -E '^\s*\((import|export|memory)' | head -30
If the module was stripped the function names will be indices, but imports, exports and the memory declaration survive stripping — they are structural rather than debug information. That is enough for a capability review even on a binary you cannot otherwise read, which makes this a two-minute check worth running on anything third-party before it goes near production.
Frequently Asked Questions
Should the .wasm be committed?
Generally no — commit the .wat and build the binary, exactly as with any other source. The exception
is a fixture that must be byte-stable across toolchain versions, where committing the binary is the
point.
Can I write a whole application in WAT? You can, and you should not. It is the right tool for probes, fixtures and reproductions; for anything with real logic a compiler produces better code and far more maintainable source.
Is wasm-tools parse an alternative?
Yes, and it handles component text as well. For core modules the two are interchangeable; wabt has
the longer history and wasm-tools the newer format support.
Can I generate WAT programmatically? Yes, and for a family of similar fixtures it beats maintaining them by hand — a small script emitting text and piping it through the assembler produces exactly the modules a test matrix needs. Because the format is plain text with no whitespace significance, generating it needs no library.
Does the assembler optimise anything?
No, and that is a feature. What you write is what you get, byte for byte, which is exactly what a
fixture or a probe needs. Optimisation is wasm-opt’s job, and running it on a hand-written module is
usually pointless — there is nothing to remove.
Is there a formatter for WAT?
Not a canonical one; disassembling with wasm2wat is the closest equivalent and normalises the layout.
Related
- WebAssembly text format (WAT) basics — the language this tool assembles.
- Writing your first WAT module by hand — the module to assemble first.
- Detecting SIMD support at runtime — the probe this workflow produces.
- How to decode Wasm files manually — reading the bytes it emitted.
← Back to WebAssembly Text Format (WAT) Basics