@coffeetales.net/wasm2lang
v2026.4.109
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.
Launch the Playground -- no install, no server. Pick a sample, choose a backend, and see generated asm.js, PHP, or Java code instantly in your browser.
wasm2lang reads WebAssembly modules and emits equivalent, human-readable
source code. Not an interpreter. Not a runtime bridge. Actual source code that
compiles, runs, and optimizes like anything else written in the target language.
One logic base. Multiple ecosystems. Zero runtime dependencies.
The problem
You already compile to WebAssembly. That investment is real: tested algorithms, validated correctness, portable IR. But then you hit a wall.
Your target environment refuses to run WASM. A WordPress plugin on shared hosting cannot load a WebAssembly module. A managed PHP platform forbids native extensions. An enterprise Java application server locks down its classloader. Your portable bytecode is suddenly not portable at all.
Your WASM runtime is the bottleneck. Running WebAssembly inside a host-language interpreter adds an abstraction layer the platform optimizer cannot see through. In Java, a WASM runtime is a JIT-over-JIT -- HotSpot never touches your actual logic. In PHP, it is an opaque extension the OPcache/JIT cannot reason about. For compute-intensive workloads -- cryptography, codecs, compression, numerical kernels -- the performance gap between interpreted WASM and natively optimized host code can be substantial.
You are rewriting the same logic for every platform. Without a transpilation path, each target language gets its own hand-rolled implementation. Bugs are fixed in one place and rediscovered in another. Behavior drifts. Maintenance compounds.
The solution
wasm2lang treats WebAssembly not just as a runtime format, but as a portable
intermediate representation for source-code generation.
Write once. Compile to .wasm. Run wasm2lang. Get native-feeling source code
that each platform's toolchain can compile, inline, and optimize directly.
How it works
┌──────────────────────────────────────┐
.wasm ─>│ WASM2LANG │─> source
.wast │ parse ─> normalize ─> passes ─> emit │ code
└──────────────────────────────────────┘- Parse -- reads
.wasmbinary or.wasttext via the Binaryen API. - Normalize -- optional Binaryen optimization passes restructure the IR (flatten, simplify locals, reorder, vacuum).
- Passes -- wasm2lang's own structural passes analyze and transform the control flow graph: loop simplification, block-loop fusion, switch dispatch detection, local usage analysis, and drop-const elision.
- Emit -- a traversal-based code emitter walks each function body and produces target-language source, applying type-aware coercion elimination and identifier mangling along the way.
No intermediate AST is constructed. The emitter produces output chunks directly from the traversal visitor callbacks, keeping memory overhead minimal.
Features
- Three production backends -- asm.js, PHP, Java; CRC32-validated byte-identical output across all runtimes.
- SIMD128 -- Java backend emits
IntVectorv128 ops that HotSpot auto-vectorizes natively. - Typed coercion elimination -- expression categories eliminate redundant
|0,Math_fround, and type casts. - Cast-module imports --
"cast"module functions lowered to native type casts instead of calls. - Spec-compliant truncation trapping -- NaN and out-of-range inputs trap instead of silently producing wrong results.
- Structural passes -- loop simplification, block-loop fusion, switch dispatch detection, local init folding.
- Deterministic identifier mangling -- Feistel-round permutation; same key = same output.
- Exported mutable globals -- getter/setter accessors across all backends.
Backends
| Backend | --language-out | Strength | Status |
| ---------- | ---------------- | --------------------------------------------------- | ----------------------------------------------------------------------- |
| asm.js | ASMJS | Closest semantic match to WASM; AOT-compiled by V8 | Active -- full function-body emission, validated by V8 and SpiderMonkey |
| PHP | PHP64 | Runs on shared hosting with no extensions | Active -- full function-body emission, validated by PHP CLI |
| Java | JAVA | HotSpot/Graal optimize the output directly; SIMD128 | Active -- full function-body emission, validated by jshell |
Why asm.js? WebAssembly was designed as the binary evolution of asm.js --
they share the same linear memory model, integer coercion semantics, and
structured control flow. asm.js is not merely a JavaScript subset; it is a
formally specified typed bytecode that happens to be syntactically valid
JavaScript, and engines such as V8 still recognize the "use asm" directive
and apply ahead-of-time compilation. This makes asm.js the most semantically
natural transpilation target for WASM: the mapping is nearly a round-trip.
The project covers WebAssembly MVP features plus post-MVP extensions where they
map well to host capabilities. The Java backend already emits SIMD128 operations
as IntVector expressions via the Vector API, turning WASM SIMD intrinsics into
code that HotSpot auto-vectorizes natively. The longer-term ambition is broader
still: more WASM proposals, more backends, and deeper optimization passes.
Installation
npm install @coffeetales.net/wasm2langOr run directly without installing:
npx @coffeetales.net/wasm2lang --input-file module.wast --emit-codeFor development, clone the repo and build from source (see Building).
Quick start
# Inline a .wast module and emit PHP:
npx @coffeetales.net/wasm2lang \
--language-out php64 \
--input-data '(module (func (export "add") (param i32 i32) (result i32) (i32.add (local.get 0) (local.get 1))))' \
--normalize-wasm binaryen:min \
--mangler secret \
--emit-codeCLI reference
wasm2lang [options]After npm install, the wasm2lang command is available in your project.
When developing from a local clone, use node wasm2lang.js --dev to load
source files directly from src/.
Options
| Flag | Type | Description |
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --input-file <path> | string | Path to a WebAssembly file. Files ending in .wat/.wast (or prefixed with wast:) are read as text; all others as binary. Use wast:- to read text from stdin. |
| --input-data <string> | string | Inline WebAssembly text to compile (alternative to --input-file). |
| --language-out <lang> | enum | Output backend: ASMJS (default), PHP64, JAVA. |
| --normalize-wasm <bundles> | list | Comma-separated normalization bundles (see below). Default: binaryen:min. |
| --emit-code [name] | string | Emit generated source code. The name becomes the output variable/class name (default: code). |
| --emit-metadata [name] | string | Emit static memory initialization. The name becomes the output variable name (default: metadata). |
| --emit-web-assembly [text] | string | Emit the (normalized) WebAssembly module. Defaults to binary format; pass text for WAT output. |
| --define <K=V> | string | Set a compile-time define (repeatable). Used to configure backend constants. |
| --mangler <key> | string | Enable deterministic identifier mangling. Same key = same output; different keys = different names. |
| --out-file <path> | string | Write output to a file instead of stdout. |
| --help | -- | Print option descriptions to stderr and exit. |
Normalization bundles
Bundles are passed as a comma-separated list to --normalize-wasm and
control how the WebAssembly IR is transformed before code emission.
| Bundle | Phase | Description |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
| binaryen:none | binaryen | No Binaryen normalization; raw input is used as-is. |
| binaryen:min | binaryen | Lightweight Binaryen passes: flatten, simplify-locals, merge-blocks, reorder-locals, vacuum. |
| binaryen:max | binaryen | Full post-lowering optimization (constant propagation, inlining, local coalescing, DCE) for smaller, faster output. |
| wasm2lang:codegen | wasm2lang | Internal wasm2lang passes (loop simplification, block-loop fusion, switch dispatch detection, etc.). |
Common combinations:
binaryen:none-- useful when the input is already in the shape you want.binaryen:min,wasm2lang:codegen-- recommended for general code generation.binaryen:none,wasm2lang:codegen-- skip Binaryen but still apply wasm2lang's structural passes.
Backend defines
Each backend reads specific defines from --define to control output
parameters.
| Define | Backend | Default | Description |
| ----------------- | ------- | ------- | --------------------------------------- |
| ASMJS_HEAP_SIZE | asm.js | 65536 | Size of the ArrayBuffer heap (bytes). |
| PHP64_HEAP_SIZE | PHP | 65536 | Size of the binary string heap (bytes). |
| JAVA_HEAP_SIZE | Java | 65536 | Size of the ByteBuffer heap (bytes). |
Usage examples
Emit asm.js with memory initialization
wasm2lang \
--input-file module.wast \
--normalize-wasm binaryen:min \
--language-out ASMJS \
--define ASMJS_HEAP_SIZE=524288 \
--emit-metadata memBuffer \
--emit-code module > output.asm.jsThe output will contain a var memBuffer = new ArrayBuffer(...) block
followed by var module = function asmjsModule(stdlib, foreign, buffer) { ... }.
Emit PHP code
wasm2lang \
--input-file module.wast \
--normalize-wasm binaryen:none,wasm2lang:codegen \
--language-out PHP64 \
--define PHP64_HEAP_SIZE=524288 \
--emit-metadata memBuffer \
--emit-code module > output.phpPHP output is a closure-based module: $module = function(array $foreign, string &$buffer): array { ... }.
Functions are emitted as PHP closures with use clauses capturing the heap
buffer and other function references by reference.
Emit Java code
wasm2lang \
--input-file module.wast \
--normalize-wasm binaryen:none,wasm2lang:codegen \
--language-out JAVA \
--define JAVA_HEAP_SIZE=524288 \
--emit-metadata memBuffer \
--emit-code module > output.javaJava output is a class wrapping all exported functions as methods, with a
ByteBuffer-based heap.
Inline WebAssembly text
wasm2lang \
--language-out java \
--input-data '(module (func (export "f") (param i32) (result i32) (local.get 0)))' \
--normalize-wasm binaryen:min \
--mangler secret \
--emit-code--input-data passes the WAT source directly as a CLI argument -- no pipe or
temp file needed.
--input-file wast:- can also read WAT from stdin, but note that piping may
fail on some platforms (e.g. MINGW/Git Bash on Windows reports
"stdin is not a tty").
Re-emit normalized WebAssembly
# Emit normalized WAT (text):
wasm2lang \
--input-file module.wasm \
--normalize-wasm binaryen:min \
--emit-web-assembly text
# Emit normalized WASM (binary):
wasm2lang \
--input-file module.wasm \
--normalize-wasm binaryen:min \
--emit-web-assembly > normalized.wasmUse identifier mangling
wasm2lang \
--input-file module.wast \
--normalize-wasm binaryen:none,wasm2lang:codegen \
--language-out JAVA \
--mangler my-secret-key \
--emit-code moduleInternal identifiers are replaced with short, opaque names derived from the key. The same key always produces the same output.
Combine metadata and code emission
--emit-metadata and --emit-code can be used together. The metadata
(memory initialization) is emitted first, followed by the code.
wasm2lang \
--input-file app.wast \
--normalize-wasm binaryen:min \
--language-out ASMJS \
--define ASMJS_HEAP_SIZE=131072 \
--emit-metadata heapData \
--emit-code myModuleOutput:
var heapData = new ArrayBuffer(131072);
var i32_array = new Int32Array(heapData);
var myModule = function asmjsModule(stdlib, foreign, buffer) {
'use asm';
// ...
};Multiple defines
--define is repeatable:
wasm2lang \
--input-file app.wast \
--define ASMJS_HEAP_SIZE=262144 \
--define CUSTOM_FLAG=true \
--emit-codeCompile from .wasm binary
wasm2lang \
--input-file module.wasm \
--normalize-wasm binaryen:min \
--language-out PHP64 \
--emit-code moduleBinary .wasm files are detected automatically (no wast: prefix needed).
Building
yarn closure-make # produces dist_artifacts/wasmxlang.jsThe project targets Closure Compiler ADVANCED_OPTIMIZATIONS (ES5 strict).
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.shThe test harness runs 14 tests covering MVP ops, control flow, arithmetic, memory types, algorithms, i64 ops, type casts, and SIMD:
- Generates
.wasttest fixtures fromtests/*.build.jsscripts. - Builds each fixture in two variants --
codegen(withwasm2lang:codegenpasses + mangling) andnone(raw, no codegen passes). - For each variant, transpiles to all enabled backends (asm.js, PHP, Java). Per-test
.build.languagesfiles can restrict which backends run. - Runs the original
.wasmthrough V8 as a reference. - Runs each backend's output through its runtime (V8, SpiderMonkey, PHP CLI, jshell).
- Compares stdout output and a CRC32 memory snapshot across all backends.
All backends must produce byte-identical output and matching memory checksums to pass.
Browser Playground
Live version: https://coffeetales.github.io/wasm2lang/
The playground includes selectable WAT samples (including data-segment examples that showcase metadata output), backend and normalization selectors, identifier mangling, and shows both the generated metadata + code and normalized WAT output. No install, no server -- everything runs client-side in your browser.
Changelog
Release history, new features, and breaking changes are tracked in
CHANGELOG.md. Each version documents what was added,
changed, and fixed -- useful for understanding what the generated output
looks like at a given release.
Contributing
Bug reports, issues, and pull requests are welcome. Good starting points:
- Backend emission correctness and coverage
- Traversal, schema, and pass behavior
- Test fixtures and validation coverage
Support the project
wasm2lang is built and maintained as an independent, self-funded project.
There is no company behind it, no venture backing, no grants -- just focused
engineering work, sustained over time.
Every backend, every optimization pass, every test fixture that validates byte-identical output across three runtimes represents hours of careful design. Sponsorship is what makes that level of rigor sustainable.
Your sponsorship directly funds:
New backends and language targets -- expanding where WebAssembly can ship as native source code.
Deeper optimization passes -- better loop recognition, smarter coercion elimination, tighter generated code.
Broader WebAssembly coverage -- building on the SIMD128 foundation toward threads, exception handling, and advanced proposals.
Validation infrastructure -- the cross-runtime test harness that guarantees correctness is not free to build or maintain.
If wasm2lang saves you from rewriting logic across languages, if it unlocks
a deployment target that a WASM runtime cannot reach, or if you simply believe
this kind of tool should exist -- consider sponsoring.
