Round-Tripping
Decompiling a Wasm module to Wax and recompiling it (wasm → wax → wasm)
reproduces the instruction stream opcode-for-opcode, dead code included. The
type section is preserved as well: structurally identical types stay distinct
and keep their order and numbering. The rest of this page lists the places where
the round trip diverges. Every divergence is semantically inert: the recompiled
module behaves identically, it just is not byte-for-byte identical.
Divergences
-
Locals and names. Local order and numbering are not preserved; every use is rewritten consistently. The
namesection is rewritten. -
eqtheneqzfuses tone. At.eqfollowed byi32.eqzdecompiles toa != b, which recompiles as a singlet.ne. (--faithfulkeeps the pair.) -
Flat cast-dispatch chains are canonicalised. A flat run of
br_on_cast_failblocks is recovered as amatch, which re-lowers to the nestedbr_on_castladder rather than the flat chain. The ladder shape itself round-trips byte-for-byte. (--faithfulkeeps the flat chain.) -
Redundant
ref.casterased (best-effort). Aref.castdecompiles to anasascription, which is dropped when the operand’s inferred type is already a subtype of the target (an identity cast or an upcast), so it recompiles to no instruction. A genuine downcast is kept and re-emitsref.cast. The decompiler also inserts scaffolding casts of its own (a member-access receiver, acall_refcallee) to nullable reference types, and these are indistinguishable from a redundant source cast to a nullable type, so cast keeping is best-effort. A few load-bearing ascriptions (a width pin, a bottom-reference type name, a continuation type immediate) are never dropped. -
A typed
selectmay lose its immediate. The value type is always preserved through the arms, but the explicit(result t)immediate form is not guaranteed to be reproduced. -
Shared spellings. A few distinct opcode streams share a single Wax spelling, so the recompiler picks one canonical encoding for both:
i64.extend32_sis spelled(x as i32) as i64_s, the same wrap-then-sign-extend pair a hand-writteni32.wrap_i64; i64.extend_i32_sproduces, and both recompile toi64.extend32_s.- A 32-bit load widened to
i64.i32.load; i64.extend_i32_uand the fusedi64.load32_uare bothm.load32(x) as i64_uand recompile toi64.load32_u. The atomic analogue behaves the same:i32.atomic.load; i64.extend_i32_uandi64.atomic.load32_uare bothm.atomic_load32(x) as i64_u. (The 8- and 16-bit narrow loads carry an inneras i32_s, so the pair and the fused load stay distinct; only the zero-extending_uload32 fuses.) - The call family.
callandcall_indirectdesugar throughcall_ref(via aref.funcor a table access) and are recovered by pattern; a shape that cannot re-fuse recompiles to the desugaredcall_refform. - Float negation of a literal.
f32.neg/f64.negof a constant folds into the negated literal (Wax spells a negative float literal-X), so bothconst -Xandconst X; negrecompile the same way. - Empty
else. Anifwith an emptyelsebranch decompiles to a branchlessif, which recompiles without theelse.
-
Types may become more precise. A declared type wider than the value it holds tightens to the value’s precise type across a round trip: the decompiler infers each binding’s type from its content and drops the wider annotation, and the recompiler re-infers the precise type. The module still validates (the precise type is a subtype of the original) and behaves identically. This affects a
localor aconstglobal declared at a supertype but only ever assigned a narrower value, a block result type declared wider than its body when no consumer pins the wider type, and abr_on_cast’s source-type immediate re-inferred from its operand.
Faithful round-tripping
The --faithful flag (wax output only) preserves the reachable instruction
structure the reshaping recoveries would otherwise change. It keeps the
eq; eqz pair rather than fusing to t.ne (divergence 2), keeps a flat
br_on_cast_fail chain rather than recovering the br_on_cast ladder
(divergence 3), and keeps a redundant non-null ref.cast as an as ascription
(divergence 4). The other divergences are inert normalisations it does not gate.
How this is checked
These guarantees are enforced by a differential fuzzing harness that compares
per-opcode histograms across a round trip, over both a curated corpus and a
wasm-smith campaign.