@coffeetales.net/wasm2lang
v2026.6.113
Published
CLI tool that transpiles WebAssembly modules into equivalent asm.js, PHP, or Java source code.
Readme
wasm2lang
Compile once to WebAssembly. Ship everywhere as source code.
Try it in the Playground → -- no install, no server. Pick a sample, pick a backend, see the generated code.
wasm2lang turns a WebAssembly module into native source code that each
host toolchain can compile, inline, and optimize directly -- asm.js, JavaScript,
PHP, or Java. No interpreter. No runtime bridge. No WASM engine embedded at runtime.
Write portable logic once, compile to .wasm, and deploy it anywhere, even
into environments that refuse to load WebAssembly modules -- shared PHP hosting,
locked-down Java servers, WordPress plugins, browsers without WASM enabled.
Quick start
npx @coffeetales.net/wasm2lang \
--language-out java \
--input-data '(module (func (export "add") (param i32 i32) (result i32) (i32.add (local.get 0) (local.get 1))))' \
--emit-codeThat prints a Java class with an add method. Swap java for asmjs,
javascript, or php64 and you get the same logic in the target language.
Install once for repeated use:
npm install @coffeetales.net/wasm2langWhy
- Reach platforms WASM cannot. Shared PHP hosting. Classloader-locked Java servers. Plugin hosts. Ecosystems where "add a WASM runtime" is not an option.
- Skip the JIT-over-JIT tax. Emitted code is compiled by HotSpot, OPcache, or V8 directly -- no opaque runtime sits between the engine and your logic.
- One logic base, four ecosystems. Fix the bug in one
.wasm, re-emit to every target. Stop drift. Stop duplication.
Backends
| Backend | --language-out | What you get |
|----------------|------------------|-----------------------------------------------------------------------|
| asm.js | ASMJS | Closest semantic match to WASM; V8 AOT-compiles via "use asm". |
| JavaScript | JAVASCRIPT | Native BigInt i64, typed-array memory, resizable ArrayBuffer. |
| PHP | PHP64 | Pure PHP closures. Runs on shared hosting with zero extensions. |
| Java | JAVA | Native long i64, ByteBuffer heap, IntVector SIMD128 via Panama. |
Every backend is validated against the original .wasm on every commit:
17 test families × 5 build variants × 4 runtimes, all producing
byte-identical stdout and matching CRC32 memory snapshots.
Features
- Structural passes -- constant-condition folding, loop simplification, block-loop fusion, switch dispatch detection, if-else recovery, block-guard elision, redundant-block removal, local init folding.
- Typed coercion elimination -- no stray
|0,Math_fround, or redundant casts. - Two-step pipelines --
--pre-normalizedlets you normalize once, ship the.wasm, and emit code later. Pass analysis survives the binary round-trip via aw2l_codegen_metacustom section. - Deterministic identifier mangling -- same key, same output. Feistel-round permutation.
- Built-in pass profiling --
WASM2LANG_PROFILE=1flushes per-pass wall-clock timings to stderr. Zero cost when off. - Cast-module imports -- functions imported from a
"cast"module (i32_to_f32,f64_to_i32, ...) lower to native type casts. - Spec-compliant trapping -- NaN and out-of-range inputs trap instead of silently producing wrong results.
- Exported mutable globals -- getter/setter accessors on every backend.
CLI essentials
wasm2lang [options]| Flag | What it does |
|------------------------------|----------------------------------------------------------------------------------------------|
| --input-file <path> | .wasm binary or .wast/.wat text. Use wast:- for stdin. |
| --input-data <wast> | Inline WebAssembly text -- no file needed. |
| --language-out <lang> | ASMJS (default), JAVASCRIPT, PHP64, JAVA. |
| --normalize-wasm <bundles> | Comma list of binaryen:none\|min\|max and/or wasm2lang:codegen. Default: binaryen:min. |
| --emit-code [name] | Emit the generated source; the name becomes the output symbol. |
| --emit-metadata [name] | Emit static memory initialization. |
| --emit-web-assembly [text] | Re-emit the (normalized) WASM -- binary by default, pass text for WAT. |
| --mangler <key> | Deterministic identifier mangling. |
| --define K=V | Compile-time defines (e.g. JAVA_HEAP_SIZE=524288). Repeatable. |
| --pre-normalized | Input was pre-normalized; recover passes from the custom section. |
| --out-file <path> | Write to a file instead of stdout. |
Run wasm2lang --help for the full option list.
Heap-size defines
Each backend sizes its heap from the module's declared memory.initial
(pages × 64 KiB). Override with a --define when you need extra room for
runtime allocations beyond the static segments.
| Define | Backend |
| ----------------- | ---------- |
| ASMJS_HEAP_SIZE | asm.js |
| JS_HEAP_SIZE | JavaScript |
| PHP64_HEAP_SIZE | PHP |
| JAVA_HEAP_SIZE | Java |
If the module declares no memory, the heap falls back to 65536 bytes (one page).
End-to-end example
wasm2lang \
--input-file module.wast \
--normalize-wasm binaryen:min,wasm2lang:codegen \
--language-out JAVA \
--define JAVA_HEAP_SIZE=524288 \
--mangler my-secret-key \
--emit-metadata memBuffer \
--emit-code module \
--out-file Module.javaThis reads .wast, runs the full normalization + codegen pipeline, mangles
identifiers deterministically, and writes a Java class with a ByteBuffer
heap ready for HotSpot.
Two-step: normalize once, emit later
# Normalize + freeze pass analysis into the .wasm custom section:
wasm2lang --input-file module.wasm \
--normalize-wasm binaryen:min,wasm2lang:codegen \
--emit-web-assembly > normalized.wasm
# Later -- possibly on a different machine -- emit any target:
wasm2lang --input-file normalized.wasm \
--pre-normalized \
--language-out PHP64 \
--emit-codeHow it works
.wat ┌────────────────────────────────────────┐
.wasm >│> parse >> normalize >> passes >> emit >│> source code
.wast └────────────────────────────────────────┘- Parse -- Binaryen reads
.wasmbinary or.wasttext. - Normalize -- optional Binaryen passes restructure IR (flatten, simplify locals, reorder, vacuum).
- Passes -- wasm2lang's own structural passes recognize and rewrite control-flow patterns.
- Emit -- a traversal-based emitter walks each function body and produces target-language source with type-aware coercion elimination and identifier mangling.
No intermediate AST is constructed. Output chunks come straight from the traversal visitor, so memory overhead scales with output size, not module size.
Why asm.js? WebAssembly is asm.js's binary evolution -- same linear-memory
model, same integer-coercion semantics, same structured control flow. asm.js
is a formally specified typed bytecode that happens to be syntactically valid
JavaScript; V8 still AOT-compiles it via "use asm". This makes it the most
semantically natural target for WASM transpilation -- close to a round-trip.
Testing
export SPIDERMONKEY_JS=/path/to/js
export PHP_CLI=/path/to/php
export JSHELL_CLI=/path/to/jshell
mkdir test_artifacts && cd test_artifacts
../scripts/wasm2lang_build_tests.sh
./wasm2lang_run_tests.shEvery test is built in five variants (baseline, codegen, nomangle,
nopre, prenorm), run through V8, SpiderMonkey, PHP CLI, and jshell, and
compared against V8's execution of the original .wasm -- byte-identical
stdout and matching CRC32 memory snapshots required to pass.
Set WASM2LANG_PROFILE=1 on any invocation to get per-pass wall-clock
timings on stderr.
Building from source
yarn closure-make # → dist_artifacts/wasmxlang.jsTargets Closure Compiler ADVANCED_OPTIMIZATIONS, ES5 strict.
Changelog
Per-version files live under changelog/, indexed in
CHANGELOG.md. Current release:
v2026.06.113.
Contributing
Bug reports, issues, and pull requests welcome -- especially around backend coverage, pass behavior, and test fixtures.
Support the project
wasm2lang is an independent, self-funded project -- no company, no venture
backing, no grants. Every backend, every pass, every cross-runtime test fixture
represents focused engineering work sustained over time.
Your sponsorship funds new backends, deeper optimization passes, broader WASM coverage (threads, exception handling, more proposals), and the cross-runtime validation infrastructure that keeps correctness rigorous.
If wasm2lang saves you from rewriting logic across languages, unlocks a
deployment target a WASM runtime cannot reach, or you simply believe this
kind of tool should exist -- consider sponsoring.
