Fast WebAssembly Text Format (WAT) compiler for JavaScript/Node.js.
Supports phase 5, phase 4 and phase 3 features, full spec syntax and passes the WebAssembly test suite.
Includes stringref, rounding mode control, and wide arithmetic.
Enables polyfills and optimizations.
| Feature | Status | Polyfill |
|---|---|---|
| MVP | yes | n/a |
| BigInt / i64 | yes | n/a |
| Multi-value | yes | partial (no blocks) |
| Sign extension | yes | yes |
| Non-trapping conversions | yes | yes |
| Bulk memory | yes | copy/fill only |
| Reference types | yes | funcref only |
| Typed function refs | yes | yes |
| Tail calls | yes | yes |
| Extended const | yes | yes |
| Multiple memories | yes | n/a |
| Memory64 | yes | no |
| SIMD | yes | no |
| Relaxed SIMD | yes | no |
| Threads | yes | no |
| GC | yes | i31/struct/array/cast |
| Exceptions | yes | no |
| Annotations | yes | n/a |
| Wide arithmetic | yes | n/a |
| JS string builtins | yes | n/a |
| Stringref | yes | no |
| Rounding mode control | yes | n/a |
npm install watr
watr`...`Compile and instantiate, returns exports.
import watr from 'watr'
// basic
const { add } = watr`(func (export "add") (param i32 i32) (result i32) (i32.add (local.get 0) (local.get 1)))`
add(2, 3) // 5
// auto-import JS functions
const { test } = watr`(func (export "test") (call \${console.log} (i32.const 42)))`
test() // 42
// interpolate numbers
watr`(global (export "pi") f64 (f64.const \${Math.PI}))` // f64
watr`(func (export "f") (result i64) (i64.const \${123n}))` // i64 BigInt
// interpolate config
watr`(memory (export "mem") \${pages})` // memory size
watr`(func (export "f") (call \${0}))` // indices
watr`(func \${id} ...)` // identifiers
// interpolate binary data
watr`(memory (export "mem") (data \${new Uint8Array([1,2,3])}))`
watr`(data (i32.const 0) \${[1, 2, 3, 4]})`
// interpolate code strings
const ops = '(i32.add (i32.const 1) (i32.const 2))'
watr`(func (export "f") (result i32) \${ops})`
// string argument
watr('(func (export "f") (result i32) (i32.const 42))')
compile(source, options?)Compile to binary. Accepts a string, AST, or template literal.
import { compile } from 'watr'
compile(`(func (export "f"))`) // string
compile(['func', ['export', '"f"']]) // AST
compile`(func (export "f") (f64.const \${Math.PI}))` // template
// Uint8Array
// polyfill newer features to MVP
compile(src, { polyfill: true }) // all features
compile(src, { polyfill: 'funcref' }) // specific features
// optimize
compile(src, { optimize: true }) // all optimizations
compile(src, { optimize: 'fold' }) // specific optimizations
// both
compile(src, { polyfill: true, optimize: true })
polyfill(ast, options?)Transform AST to polyfill newer WebAssembly features for older runtimes.
import { polyfill, parse, compile } from 'watr'
// auto-detect and polyfill all
const ast = polyfill(parse(src))
compile(ast)
// specific features
polyfill(ast, 'funcref') // space-separated string
polyfill(ast, { funcref: true }) // object
Available polyfills:
| Feature | Transforms | Notes |
|---|---|---|
funcref |
ref.func → i32.const, call_ref/return_call_ref → call_indirect |
Creates hidden table |
sign_ext |
i32.extend8_s → shift pairs, etc |
Shift left + arithmetic shift right |
nontrapping |
i32.trunc_sat_f32_s → helper function, etc |
Injects helper functions |
bulk_memory |
memory.copy/fill → loop helpers |
Byte-by-byte loops |
return_call |
return_call → return + call |
Loses tail call optimization |
i31ref |
ref.i31 → i32.and, i31.get_s/u → shift/mask |
31-bit tagged integers |
extended_const |
global.get in initializers → evaluated constant |
Compile-time evaluation |
multi_value |
Multiple results → single + globals | Partial: functions only, not blocks |
gc |
struct.new/get/set, array.new/get/set/len → memory ops |
Bump allocator, type tags |
ref_cast |
ref.test, ref.cast, br_on_cast → type tag checks |
Runtime tag comparison |
Not polyfillable:
| Feature | Reason |
|---|---|
| SIMD | Scalar emulation too slow |
| Threads/Atomics | Requires host support |
| Memory64 | Cannot emulate 64-bit address space |
| Exception handling | Complex control flow transforms |
externref |
Requires JS-side reference tracking |
optimize(ast, options?)Optimize AST for smaller size and better performance.
import { optimize, parse, print, compile } from 'watr'
// auto-detect and apply all optimizations
const ast = optimize(parse(src))
compile(ast)
// specific optimizations
optimize(ast, 'fold') // constant folding only
optimize(ast, 'treeshake fold') // multiple optimizations
optimize(ast, { fold: true }) // object form
// can accept string directly
print(optimize('(func (i32.add (i32.const 1) (i32.const 2)))'))
// (func (i32.const 3))
optimize(ast) runs every default-on pass below, up to 3 rounds, until a fixpoint
or a round would inflate the encoded compile(ast) byte size (the round is then
reverted). Passes that can grow output, change observable behavior, or only make
sense when you control the host are opt-in (marked ◌); the rest are on by default.
| Optimization | Description | Example |
|---|---|---|
treeshake |
Remove unused definitions | Functions/globals/types/tables not exported or reachable |
fold |
Constant folding (incl. reinterpret/convert) | (i32.add (i32.const 1) (i32.const 2)) → (i32.const 3); (i64.reinterpret_f64 (f64.const 256)) → (i64.const 4643211215818981376) |
deadcode |
Remove unreachable code | Code after unreachable, br, return |
locals |
Remove unused locals | Locals never read or only-written |
identity |
Remove identity ops | (i32.add x (i32.const 0)) → x |
strength |
Strength reduction | (i32.mul x (i32.const 2)) → (i32.shl x (i32.const 1)) |
branch |
Simplify constant branches | (if (i32.const 1) A B) → A |
propagate |
Forward single-use locals & tiny consts | (local.set $x (i32.const 1)) … (local.get $x) → (i32.const 1) |
inline ◌ |
Inline tiny functions | Single-expression functions without locals — may duplicate bodies |
inlineOnce |
Inline functions called from exactly one site | Drops the callee and its call site; never duplicates |
vacuum |
Remove no-ops | Nops, drop-of-pure, empty branches |
mergeBlocks |
Unwrap blocks whose label is never targeted | (block $L body) with no br $L inside → body (saves block/end framing) |
coalesce |
Share local slots between non-overlapping same-type locals | Two i32 locals with disjoint live ranges → one local |
peephole |
Algebraic identities | x - x → 0, x & 0 → 0 |
globals |
Propagate immutable global constants | global.get of never-written global → its constant (size-aware) |
offset |
Fold offsets into load/store | (i32.load (i32.add ptr (i32.const 4))) → (i32.load offset=4 ptr) |
unbranch |
Remove redundant trailing br |
br $label at end of its own block |
loopify |
Collapse block+loop+br_if while-idiom |
(block $A (loop $B (br_if $A (i32.eqz cond)) … (br $B))) → (loop $B (if cond (then … (br $B)))) |
stripmut |
Strip mut from never-written globals |
Enables further globals propagation |
brif |
Convert if-then-br to br_if | (if cond (then (br $l))) → (br_if $l cond) |
foldarms ◌ |
Merge identical trailing if arms | (if C (then A X) (else B X)) → (if C (then A) (else B)) X |
dedupe |
Eliminate duplicate functions | Replace duplicates with a single canonical function |
reorder ◌ |
Reorder functions by call frequency | Hot functions first for smaller LEB indices |
dedupTypes |
Merge identical type definitions | Remove structurally duplicate (type …) nodes |
packData |
Pack data segments | Trim trailing zeros, merge adjacent constant-offset segments |
minifyImports ◌ |
Shorten import module/field names | a, b, aa… — only safe when you control the host |
◌ = opt-in (optimize(ast, 'foldarms') or optimize(ast, { foldarms: true })).
parse(source, options?)Parse to AST.
import { parse } from 'watr'
parse('(i32.add (i32.const 1) (i32.const 2))')
// ['i32.add', ['i32.const', 1], ['i32.const', 2]]
// options: comments, annotations
parse('(func ;; note\n)', { comments: true })
// ['func', [';', ' note']]
print(tree, options?)Format WAT code or AST to string. Accepts a string or AST array.
import { print } from 'watr'
// prettify string
print('(module(func(export "add")(param i32 i32)(result i32)local.get 0 local.get 1 i32.add))')
// (module
// (func (export "add") (param i32 i32) (result i32)
// local.get 0
// local.get 1
// i32.add))
// print AST
print(['module', ['func', ['export', '"f"'], ['result', 'i32'], ['i32.const', 42]]])
// (module
// (func (export "f") (result i32)
// (i32.const 42)))
// minify
print('(module\n (func (result i32)\n (i32.const 42)))', { indent: false, newline: false })
// (module (func (result i32) (i32.const 42)))
// options: indent (default ' '), newline (default '\n'), comments (default true)
;; folded
(i32.add (i32.const 1) (i32.const 2))
;; flat
i32.const 1
i32.const 2
i32.add
;; abbreviations
(func (export "f") ...) ;; inline export
(func (import "m" "n") ...) ;; inline import
(memory 1 (data "hello")) ;; inline data
;; numbers
42 0x2a 0b101010 ;; integers
3.14 6.02e23 0x1.8p+1 ;; floats
inf -inf nan nan:0x123 ;; special
1_000_000 ;; underscores
;; comments
(; block ;)
watr`(func (export "f") (result i64) (i64.const ${9007199254740993n}))`
watr`(func (export "g") (param i64) (result i64) (i64.mul (local.get 0) (i64.const 2n)))`
(func (result i32 i32) (i32.const 1) (i32.const 2))
(func (param i32 i32) (result i32 i32) (local.get 1) (local.get 0)) ;; swap
(block (result i32 i32) (i32.const 1) (i32.const 2))
(i32.extend8_s (i32.const 0xff)) ;; -1
(i32.extend16_s (i32.const 0xffff)) ;; -1
(i64.extend8_s (i64.const 0xff)) ;; -1
(i64.extend16_s (i64.const 0xffff)) ;; -1
(i64.extend32_s (i64.const 0xffffffff)) ;; -1
(i32.trunc_sat_f32_s (f32.const 1e30)) ;; clamps instead of trapping
(i32.trunc_sat_f32_u (f32.const -1.0)) ;; 0 instead of trap
(i64.trunc_sat_f64_s (f64.const inf)) ;; max i64 instead of trap
(memory.copy (i32.const 0) (i32.const 100) (i32.const 10)) ;; dst src len
(memory.fill (i32.const 0) (i32.const 0xff) (i32.const 64)) ;; dst val len
(memory.init $data (i32.const 0) (i32.const 0) (i32.const 4))
(data.drop $data)
(table.copy (i32.const 0) (i32.const 1) (i32.const 2))
(table.init $elem (i32.const 0) (i32.const 0) (i32.const 2))
(elem.drop $elem)
(memory $a 1)
(memory $b 2)
(i32.load $a (i32.const 0))
(i32.store $b (i32.const 0) (i32.const 42))
(memory.copy $a $b (i32.const 0) (i32.const 0) (i32.const 10))
(memory i64 1) ;; 64-bit memory
(memory i64 1 100) ;; with max
(i64.load (i64.const 0)) ;; 64-bit addresses
(i32.store (i64.const 0x100000000) (i32.const 42))
(v128.const i32x4 1 2 3 4)
(v128.const f32x4 1.0 2.0 3.0 4.0)
(i32x4.add (local.get 0) (local.get 1))
(f32x4.mul (local.get 0) (local.get 1))
(i8x16.shuffle 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
(local.get 0) (local.get 1))
(i32x4.extract_lane 0 (local.get 0))
(i32x4.replace_lane 0 (local.get 0) (i32.const 99))
(i32x4.relaxed_trunc_f32x4_s (local.get 0))
(f32x4.relaxed_madd (local.get 0) (local.get 1) (local.get 2)) ;; a * b + c
(f32x4.relaxed_nmadd (local.get 0) (local.get 1) (local.get 2)) ;; -(a * b) + c
(i8x16.relaxed_swizzle (local.get 0) (local.get 1))
(f32x4.relaxed_min (local.get 0) (local.get 1))
(func $factorial (param $n i64) (param $acc i64) (result i64)
(if (result i64) (i64.le_u (local.get $n) (i64.const 1))
(then (local.get $acc))
(else (return_call $factorial
(i64.sub (local.get $n) (i64.const 1))
(i64.mul (local.get $n) (local.get $acc))))))
(return_call_indirect (type $fn) (local.get 0) (i32.const 0))
(global $base i32 (i32.const 1000))
(global $offset i32 (i32.add (global.get $base) (i32.const 100)))
(global i64 (i64.mul (i64.const 1024) (i64.const 1024)))
(table $t 10 funcref)
(table.get $t (i32.const 0))
(table.set $t (i32.const 0) (ref.func $f))
(table.size $t)
(table.grow $t (ref.null func) (i32.const 5))
(table.fill $t (i32.const 0) (ref.null func) (i32.const 10))
(global $ext (mut externref) (ref.null extern))
(ref.is_null (local.get 0))
(type $fn (func (param i32) (result i32)))
(func $double (type $fn) (i32.mul (local.get 0) (i32.const 2)))
(call_ref $fn (i32.const 21) (ref.func $double)) ;; 42
(ref.null $fn)
(ref.is_null (ref.null $fn))
(block (result (ref null $fn)) (ref.null $fn))
;; struct
(type $point (struct (field $x f64) (field $y f64)))
(struct.new $point (f64.const 1.0) (f64.const 2.0))
(struct.get $point $x (local.get $p))
(struct.set $point $y (local.get $p) (f64.const 3.0))
;; array
(type $arr (array (mut i32)))
(array.new $arr (i32.const 0) (i32.const 10)) ;; value, length
(array.get $arr (local.get $a) (i32.const 0))
(array.set $arr (local.get $a) (i32.const 0) (i32.const 42))
(array.len (local.get $a))
;; recursive types
(rec
(type $node (struct (field $val i32) (field $next (ref null $node)))))
;; casts
(ref.cast (ref $point) (local.get 0))
(br_on_cast $label anyref (ref $point) (local.get 0))
(tag $e (param i32))
(throw $e (i32.const 42))
(try_table (result i32) (catch $e 0)
(call $might_throw)
(i32.const 0))
(try_table (catch_all 0) (call $fn))
(import "wasm:js-string" "length" (func $strlen (param externref) (result i32)))
(import "wasm:js-string" "charCodeAt" (func $charAt (param externref i32) (result i32)))
(import "wasm:js-string" "fromCharCode" (func $fromChar (param i32) (result externref)))
(import "wasm:js-string" "concat" (func $concat (param externref externref) (result externref)))
(memory 1 10 shared)
(i32.atomic.load (i32.const 0))
(i32.atomic.store (i32.const 0) (i32.const 42))
(i32.atomic.rmw.add (i32.const 0) (i32.const 1))
(i32.atomic.rmw.cmpxchg (i32.const 0) (i32.const 0) (i32.const 1))
(memory.atomic.wait32 (i32.const 0) (i32.const 0) (i64.const -1))
(memory.atomic.notify (i32.const 0) (i32.const 1))
(atomic.fence)
;; 128-bit addition: (a_lo, a_hi) + (b_lo, b_hi) -> (lo, hi)
(i64.add128 (local.get 0) (local.get 1) (local.get 2) (local.get 3))
(i64.sub128 (local.get 0) (local.get 1) (local.get 2) (local.get 3))
;; wide multiply: a * b -> (lo, hi)
(i64.mul_wide_s (local.get 0) (local.get 1))
(i64.mul_wide_u (local.get 0) (local.get 1))
(@custom "name" "content")
(@name "my_module")
(@metadata.code.branch_hint "\00") if ... end ;; unlikely
(@metadata.code.branch_hint "\01") if ... end ;; likely