watr

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

Install

npm install watr

API

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.funci32.const, call_ref/return_call_refcall_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_callreturn + call Loses tail call optimization
i31ref ref.i31i32.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 - x0, x & 00
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)

Syntax

;; 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 ;)

Features

BigInt / i64

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)))`

Multi-value

(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))

Sign extension

(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

Non-trapping conversions

(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

Bulk memory

(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)

Multiple memories

(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))

Memory64

(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))

SIMD

(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))

Relaxed SIMD

(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))

Tail calls

(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))

Extended const

(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)))

Reference types

(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))

Typed function refs

(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))

GC

;; 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))

Exceptions

(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))

JS string builtins

(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)))

Threads

(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)

Wide arithmetic

;; 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))

Annotations

(@custom "name" "content")
(@name "my_module")
(@metadata.code.branch_hint "\00") if ... end  ;; unlikely
(@metadata.code.branch_hint "\01") if ... end  ;; likely

See Also