JZ (javascript zero) is minimal modern functional JS subset, compiling to WASM.
import jz from 'jz'
// Distance between two points
const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
dist(3, 4) // 5Write plain JS, compile to WASM – fast, portable and long-lasting.
JZ distills the modern functional core – the "good parts" (Crockford) – from legacy semantics, features overhead and perf quirks.
- Static AOT – no runtime, no GC, no dynamic constructs.
- Valid jz = valid js — test in browser, compile to wasm.
- Minimal — output is close to hand-written WAT; CI gates it to stay at least as small and fast as AssemblyScript and Porffor on the bench corpus (CONTRIBUTING).
| Good for | Not for |
|---|---|
| Numeric / math compute | UI / frontend |
| DSP / audio / bytebeats | Backend / APIs |
| Parsing / transforms | Async / I/O-heavy logic |
| WASM utilities | JavaScript runtime |
import jz, { compile } from 'jz'
// Compile, instantiate
const { exports: { add } } = jz('export let add = (a, b) => a + b')
add(2, 3) // 5
// Compile only — returns raw WASM binary (no JS adaptation)
const wasm = compile('export let f = (x) => x * 2')
const mod = new WebAssembly.Module(wasm)
const inst = new WebAssembly.Instance(mod)
// Async WASM startup — jz source compilation is still synchronous
const asyncMod = await WebAssembly.compile(wasm)
const asyncInst = await WebAssembly.instantiate(asyncMod)
asyncInst.exports.f(21) // 42Options
Options are passed as jz(source, opts) or compile(source, opts). Common ones:
| Option | Use |
|---|---|
jzify: true |
Accept broader JS patterns such as var, function, switch, arguments, ==, and undefined by lowering them to the JZ subset. |
modules: { specifier: source } |
Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
imports: { mod: host } |
Wire host namespaces/functions used by import { fn } from "mod"; functions may be plain JS functions or { fn, returns } specs. |
memory |
Pass memory: N to create owned memory with N initial pages, or pass memory: jz.memory() / WebAssembly.Memory to share memory across modules. |
host: 'js' | 'wasi' |
Select runtime-service lowering. Default js uses small env.* imports auto-wired by jz(); wasi emits WASI Preview 1 imports for wasmtime/wasmer/deno. |
optimize |
false/0 disables optimization, 1 keeps cheap size passes, true/2 is the default, 3 enables aggressive experimental passes. String aliases 'size' (unroll/vectorize off, tight scalar caps — smallest wasm), 'balanced' (= default), 'speed' (full unroll + SIMD). Object form overrides individual passes/knobs (and accepts level: as a number or alias base). |
strict: true |
Reject dynamic fallbacks such as unknown receiver method calls, obj[k], and for-in instead of emitting JS-host dynamic dispatch. |
alloc: false |
Omit raw allocator exports like _alloc/_clear when compiling standalone WASM that never marshals heap values across the host boundary. |
wat: true |
compile() returns WAT text instead of a WASM binary. |
profile |
Pass a mutable sink to collect compile-stage timings; set profile.names = true to also emit a WASM name section for profiler/debugger symbolication. profileNames remains as a legacy alias. |
npm install -g jz
# Compile
jz program.js # → program.wasm
jz program.js --wat # → program.wat
jz program.js -o out.wasm # custom output (- for stdout)
# Optimization level: -O0 off, -O1 size, -O2 balanced, -O3 speed
jz program.js -O3
# Runtime-service lowering: js (default) or wasi
jz program.js --host wasi
# Evaluate
jz -e "1 + 2" # 3
# Show help
jz --helpJZ is a strict functional JS subset. Built-in jzify transform extends support to legacy patterns.
┌────────────────────────────────────────────────────────────────────────┐
│ JZify │
│ var function arguments switch new Foo() │
│ == != instanceof undefined │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ JZ │ │
│ │ let/const => ...xs destructuring import/export │ │
│ │ if/else for/while/do-while/of/in break/continue │ │
│ │ try/catch/finally throw │ │
│ │ operators strings booleans numbers arrays objects `${}` │ │
│ │ Math Number String Array Object JSON RegExp Symbol null │ │
│ │ ArrayBuffer DataView TypedArray Map Set │ │
│ │ console setTimeout/setInterval Date performance │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
Not supported
async/await Promise function* yield
this class super extends delete labels
eval Function with Proxy Reflect WeakMap WeakSet
import() DOM fetch Intl Node APIs
Where does jz differ from JavaScript?
jz compiles a distilled JS subset to static WASM — it is not a JavaScript engine and does not chase full TC39 semantics. Valid jz = valid JS means jz source always parses and runs as JS; it does not promise byte-identical results for the cases below. --wat shows exactly what was emitted.
Out of scope — by design, not unfinished. Each needs a runtime jz deliberately does not ship; the compiler errors instead of emitting something slow:
- Dynamic execution & reflection —
eval,Function,with,import(),Proxy,Reflect,WeakMap/WeakSet, property descriptors, accessors. - Concurrency —
async/await,Promise, generators, iterators. - General-runtime surface a numeric / DSP / parser kernel never touches —
Intl, realms, resizableArrayBuffer,DataView, dynamicRegExppatterns, theSymbolregistry and most well-known symbols, the fullBigIntsurface. A few of these have partial static support; the rest will not be added — that is what keeps the output close to hand-written WAT.
Behavioral divergences — valid jz whose result differs from V8. Tracked, narrowing over time:
- Booleans are numbers.
true/falsecompile to1/0.typeofis recovered statically where a boolean can be proven, but a boolean carried through an untyped binding reports"number",String(true)is"1", andparseInt(true)is1. A real-boolean carrier is planned. - Objects are fixed-layout schemas — key set and order fixed at the literal,
deleterejected,memory.Object({...})must match the source key order. - Errors are untagged —
throwcarries a value, not a typedError;e instanceof TypeErrordoes not discriminate. Set/Mapiterate slot order, not insertion order.- Memory is not reclaimed automatically — see How does memory work?.
For full TC39 conformance use porffor; jz trades completeness for low-level numeric performance by design.
How to pass data between JS and WASM?
Numbers pass directly as f64, arrays of ≤ 8 elements return as plain JS arrays (multi-value). Strings, arrays, objects, and typed arrays are heap values — inst.memory provides read/write across the boundary:
[!WARNING] jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
memory.Object({ x: 3, y: 4 })expects the same key order as the jz source{ x, y }.{ y: 4, x: 3 }with reversed keys will produce wrong values.
const { exports, memory } = jz`
export let greet = (s) => s.length
export let sum = (a) => a.reduce((s, x) => s + x, 0)
export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
export let rgb = (c) => [c, c * 0.5, c * 0.2]
export let process = (buf) => buf.map(x => x * 2)
`
// JS → WASM (write)
memory.String('hello') // → string pointer
memory.Array([1, 2, 3]) // → array pointer
memory.Float64Array([1.0, 2.0]) // → typed array pointer
memory.Int32Array([10, 20, 30]) // all typed array constructors available
// ⚠ Objects: keys and order must match the jz source declaration.
// jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
// If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
memory.Object({ x: 3, y: 4 }) // → object pointer
// Strings/arrays inside objects are auto-wrapped to pointers:
memory.Object({ name: 'jz', count: 3 }) // name auto-wrapped via memory.String
// Call with pointers
exports.greet(memory.String('hello')) // 5
exports.sum(memory.Array([1, 2, 3])) // 6
exports.dist(memory.Object({ x: 3, y: 4 })) // 5
// direct JS array return
exports.rgb(100) // [100, 50, 20]
// read pointer value
memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
jz`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x`How does template interpolation work?
Numbers and booleans inline directly into source. Strings, arrays, and objects are serialized as jz source literals and compiled at compile time — no post-instantiation allocation, no getter overhead:
jz`export let f = () => ${'hello'}.length` // 5 — string compiled as literal
jz`export let f = () => ${[10, 20, 30]}[1]` // 20 — array compiled as literal
jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3 — object compiled as literal
// Nested values work too
jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
Does it support ES module imports?
Yes — standard ES import syntax is bundled at compile-time into a single WASM.
const { exports } = jz(
'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
{ modules: { './math.jz': 'export let add = (a, b) => a + b' } }
)Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
CLI resolves filesystem imports automatically.
jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automaticallyBrowser: fetch sources yourself, pass via { modules }. The compiler stays synchronous and pure — no I/O.
// Transitive bundling — all merged into one WASM
const { exports } = jz(mainSrc, { modules: {
'./math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
// Fetch sources yourself, pass them in
'./utils.jz': await fetch('./util.jz').then(r => r.text())
} })How to run a produced .wasm without jz?
Ship the .wasm and the small host-side bridge that knows the value encoding. jz publishes the bridge under the jz/interop subpath — it has no dependency on the compiler, parser, or watr (only wasi.js), so bundlers tree-shake the compiler out entirely.
jz program.js -o program.wasm # compile once, anywhere// In production: no compiler shipped, just the wasm + the interop bridge.
import { instantiate } from 'jz/interop'
import wasmBytes from './program.wasm' // bundler-specific; or fetch(...)
const { exports, memory } = instantiate(wasmBytes)
exports.greet(memory.String('hello')) // marshal works the same as compile-timeinstantiate(wasm, opts?) accepts Uint8Array, ArrayBuffer, or a pre-built WebAssembly.Module. The returned { exports, memory, instance, module } is identical to what the jz(src) template tag returns — same memory.String/Array/Object/... constructors, same memory.read(ptr) decoder, same handling of imports / shared memory / WASI.
The bridge encodes values as NaN-boxed f64 with bump-allocated heap blobs. One boundary codec per binary — a jz wasm picks its host shape at compile time, the JS host that loads it knows which variant it asked for. Internal representation (whether a number lives as a flat i32, an SSO string stays inline, an object packs its fields) is analysis-driven and per-site, never a user-pickable preset.
How do I pass values from the host to jz?
Any host namespace — functions, constants, custom objects — wires in via the imports option. jz extracts what's needed via Object.getOwnPropertyNames, so non-enumerable built-ins (Math.sin, Date.now) work automatically:
// Custom function
const { exports } = jz(
'import { log } from "host"; export let f = (x) => { log(x); return x }',
{ imports: { host: { log: console.log } } }
)
// Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
const { exports } = jz(
'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
{ imports: { math: Math } }
)
// Date static methods
const { exports } = jz(
'import { now } from "date"; export let f = () => now()',
{ imports: { date: Date } }
)
// window / globalThis
const { exports } = jz(
'import { parseInt } from "window"; export let f = () => parseInt("42")',
{ imports: { window: globalThis } }
)For per-call data (numbers, strings, arrays, objects, typed arrays), see How to pass data between JS and WASM? above — pointers via memory.String/memory.Array/memory.Object or template interpolation.
Can two modules share data?
Yes — jz.memory() creates a shared memory that modules compile into. Schemas accumulate automatically, so objects created in one module are readable by another:
const memory = jz.memory()
const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
const b = jz('export let read = (o) => o.x + o.y', { memory })
// Object from module a, processed by module b — same memory, merged schemas
b.exports.read(a.exports.make()) // 30
// Read from JS too — memory knows all schemas
memory.read(a.exports.make()) // {x: 10, y: 20}
// Write from JS before any compilation
memory.String('hello') // → NaN-boxed pointer
memory.Array([1, 2, 3]) // → NaN-boxed pointerjz.memory() returns an actual WebAssembly.Memory (monkey-patched with .read(), .String(), .Array(), .Object(), .write(), etc). You can also pass an existing memory: jz.memory(new WebAssembly.Memory({ initial: 4 })) patches and returns the same object. Passing raw WebAssembly.Memory to { memory } auto-wraps it.
Modules sharing a memory share a single bump allocator — see How does memory work? below. Use .instance.exports for raw pointers, .exports for the JS-wrapped surface.
How does memory work? How do I reset it?
jz uses a bump allocator: every heap value (string, array, object, typed array) bumps a single pointer forward. No free list, no GC, no per-object header overhead beyond [len][cap]. Bytes 0–1023 are reserved (data segment + heap-pointer slot at byte 1020); the heap starts at byte 1024 and grows the WASM memory automatically when full.
This means memory is never reclaimed implicitly — long-running programs that allocate per call will grow without bound. The fix is to reset the heap pointer between independent batches:
const { exports, memory } = jz`
export let process = (n) => {
let xs = []
for (let i = 0; i < n; i++) xs.push(i * 2)
return xs.reduce((s, x) => s + x, 0)
}
`
for (let i = 0; i < 1000; i++) {
const sum = exports.process(100) // allocates an array each call
memory.reset() // drop everything; heap ptr → 1024
}After memory.reset() all previously returned pointers are invalid — read what you need first, then reset.
For finer control, allocate manually: memory.alloc(bytes) returns a raw offset using the same bump pointer. Pure scalar modules (no strings/arrays/objects) are compiled without the allocator at all — no _alloc, no _clear, no memory section.
Non-JS hosts (wasmtime, wasmer, deno, EdgeJS, embedded WASM) get the same allocator via two exports:
(func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
(func $_clear) ;; rewinds heap pointer to 1024
memory.reset() and memory.alloc() are JS-side aliases for these. Headers vary by type: strings store [len:i32] + utf8 bytes (offset = _alloc(4+n) + 4); arrays / typed arrays / objects store [len:i32, cap:i32] + payload (offset = _alloc(8+bytes) + 8). The pointer crossing the WASM boundary is the f64 NaN-box 0x7FF8 << 48 | type << 47 | aux << 32 | offset — see src/host.js for type codes and the canonical encoders. Call _clear() between batches to reclaim. Strip both with compile(code, { alloc: false }) if you only call functions and never marshal heap values across the boundary.
How do I run compiled WASM outside the browser?
jz program.js -o program.wasm
# Run with any WASM runtime
wasmtime program.wasm # WASI support built in
wasmer run program.wasm
deno run program.wasmPure numeric modules have no imports and instantiate with standard
WebAssembly.Module / WebAssembly.Instance, which is the right shape for JS hosts such as EdgeJS. Compile once at startup or build time, then reuse the module; do not compile JZ source per request.
Two host modes select how runtime services lower:
jz.compile(code) // host: 'js' (default) — env.* imports
jz.compile(code, { host: 'wasi' }) // wasi_snapshot_preview1.* importshost: 'js' (default) — console.log/Date.now/performance.now import from env.* and the JS host (jz() runtime) wires them automatically. Host-side stringification means jz drops __ftoa/__write_*/__to_str from the binary.
host: 'wasi' — console.log compiles to WASI fd_write, clocks to
clock_time_get. Output runs natively on wasmtime/wasmer/deno. In JS hosts, the small jz/wasi polyfill is auto-applied; pass { write(fd, text) {…} } to capture stdout/stderr. host: 'wasi' errors at compile time if a program would emit env.__ext_* (dynamic dispatch into the JS host) — annotate the receiver or stay on host: 'js'.
What host features are supported?
| JS API | host: 'js' (default) |
host: 'wasi' |
|---|---|---|
console.log() |
env.print(val: i64, fd: i32, sep: i32) — host stringifies |
WASI fd_write (fd=1), space-separated, newline appended |
console.warn/error |
same, fd=2 | WASI fd_write (fd=2) |
Date.now() |
env.now(0) -> f64 (epoch ms) |
clock_time_get (realtime) |
performance.now() |
env.now(1) -> f64 (monotonic ms) |
clock_time_get (monotonic) |
setTimeout/clearTimeout |
env.setTimeout(cb, delay, repeat) -> f64 / env.clearTimeout(id) -> f64 — host schedules; fires via exported __invoke_closure |
WASM timer queue + __timer_tick (or blocking __timer_loop on wasmtime) |
setInterval/clearInterval |
same env.setTimeout (repeat=1) / env.clearTimeout |
WASM timer queue + __timer_tick |
dynamic obj.method() |
env.__ext_call (JS resolves) |
error at compile time |
The compiled .wasm uses at most one import namespace:
- none — pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
env— JS-host services (default). Auto-wired by thejz()runtime.wasi_snapshot_preview1— standard WASI Preview 1. Run natively on wasmtime/wasmer/deno.
How do I add custom operators / extend the stdlib?
jz's emitter table (ctx.core.emit) maps AST operators → WASM IR generators. Module files in module/ register handlers on it. To add your own:
import { emitter } from './src/emit.js'
import { typed } from './src/ir.js'
// Register a custom operator: my.double(x) → x * 2
emitter['my.double'] = (x) => {
return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
}The naming convention follows the AST path: Math.sin → math.sin, arr.push → .push, typed variants like .f64:push. See any file in module/ for the full pattern — each exports a function that receives ctx and registers emitters, stdlib, globals, or helpers.
Inside a runtime module, import directly from the layer you need:
import { emit } from '../src/emit.js'
import { asF64, temp } from '../src/ir.js'
import { valTypeOf, VAL } from '../src/analyze.js'Isn't implicit inference evil?
The "explicit > implicit" reflex assumes inference is hidden, fragile, or coercive. jz inference is none of those — the rules are mechanical (name, literals, operators, member access, typeof, assignment flow, JSDoc), the chosen types are visible in --wat output, and ambiguous cases fall back to NaN-boxed f64: a safe default, never a wrong type.
Type annotations (eg. TypeScript) do two different jobs in one syntax:
- Hints to the compiler about storage (
let x: number = 5). That's compiler internals leakage into syntax — inference reads operators (x | 0→ i32), member access (s.length→ string),typeofguards, and assignments the way a human reader does. The annotation duplicates what's already in the code. - Contracts at module boundaries (
function f(id: UserId): User | null). Legitimate — but a documentation concern, not a language concern.
jz keeps the split clean: inference handles storage, JSDoc handles contracts. Valid jz = valid JS — no parallel type system to learn. Annotations don't make code faster; they sharpen what the compiler can already infer.
Can I compile jz to C?
jz program.js -o program.wasm
wasm2c program.wasm -o program.c
cc program.c -o program| jz | Node | Porffor | AS | WAT | C | Go | Zig | Rust | NumPy | |
|---|---|---|---|---|---|---|---|---|---|---|
| biquad | 6.50ms 3.4kB |
12.35ms 3.2kB |
fails | 9.03ms 1.9kB |
6.49ms 767 B |
5.30ms | 8.96ms fma |
5.04ms | 5.27ms | 3.09s |
| mat4 | 2.74ms 3.3kB |
11.96ms 1.2kB |
88.68ms 2.4kB diff |
9.32ms 1.6kB |
8.12ms 414 B |
2.76ms | 12.51ms | 2.74ms | 1.78ms | 389.44ms |
| poly | 0.37ms 1.2kB |
2.32ms 1014 B |
fails | 1.15ms 1.3kB |
0.81ms 359 B |
0.52ms | 0.80ms | 0.80ms | 0.57ms | 0.61ms |
| bitwise | 1.40ms 1.2kB |
5.32ms 1005 B |
fails | 12.13ms 1.5kB |
4.88ms 355 B |
1.30ms | 5.23ms | 4.16ms | 1.30ms | 14.77ms |
| tokenizer | 0.10ms 1.7kB |
0.21ms 2.0kB |
0.41ms 3.2kB |
0.08ms 1.6kB |
0.10ms 344 B |
0.13ms | 0.08ms | 0.14ms | 0.12ms | 5.13ms |
| callback | 0.03ms 1.4kB |
0.88ms 828 B |
fails | 1.49ms 1.9kB |
0.25ms 267 B |
0.10ms | 0.20ms | 0.01ms | 0.09ms | 1.81ms |
| aos | 1.62ms 1.8kB |
1.82ms 1.1kB |
fails | 1.91ms 2.2kB |
1.07ms 481 B |
1.20ms | 0.90ms | 0.90ms | 1.20ms | 2.55ms |
| mandelbrot | 12.55ms 1.0kB |
13.80ms 1.8kB |
13.47ms 3.0kB |
12.42ms 1.3kB |
— | 12.26ms | 12.46ms | 12.31ms | 12.23ms | — |
| json | 0.23ms 7.7kB |
0.38ms 1.2kB |
fails | — | — | 0.21ms | 1.17ms | 0.69ms | 0.68ms | 1.20ms |
| sort | 5.96ms 1.6kB |
11.13ms 1.6kB |
fails | 10.22ms 1.9kB |
— | 8.85ms | 10.36ms | 8.84ms | 9.37ms | 5.05ms |
| crc32 | 12.12ms 1.2kB |
13.43ms 1.8kB |
80.76ms 3.1kB |
12.19ms 1.4kB |
— | 10.69ms | 9.30ms | 9.45ms | 9.38ms | 0.24ms |
| watr | 1.56ms 144.4kB |
1.45ms 2.6kB |
fails | — | — | — | — | — | — | — |
Numbers from node bench/bench.mjs --targets=v8,jz,as on Apple Silicon.
Geomean speed: jz 0.41× V8, 0.40× AS, 0.32× Porffor. Geomean size: jz 0.85× AS.
optimize: 'size'|'speed'|'balanced' provides a size/speed tradeoff lever.
Optimizations
High-impact summary behind the benchmark table, not an exhaustive list.
| Optimization | Effect |
|---|---|
| Escape scalar replacement | Removes short-lived object/array literals before allocation. |
| Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
| Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
| Host-service import lowering | host: 'js' lowers console, clocks, and timers to small env.* imports instead of pulling WASI/string formatting into normal JS-host builds. |
| Static and shaped runtime JSON specialization | Constant JSON.parse sources fold to fresh slot trees; stable let JSON sources use a generated runtime parser for the inferred shape. |
| Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
| Integer/value-type narrowing | Keeps bitwise, Math.imul, charCodeAt, loop counters, and internal narrowed returns on raw i32/f64 paths instead of generic boxed-value helpers. |
| SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
| Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
| OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
| Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
npm run test:bench pins every claimed V8 win, AssemblyScript win/tie, and wasm size budget. Mandelbrot is pinned as a V8 win and AssemblyScript tie, not an AS win. Unclaimed rows stay visible as todo gaps without weakening the asserted wins.
- porffor — ahead-of-time JS→WASM compiler targeting full TC39 semantics. Implements the spec progressively (test262). Where jz restricts the language for performance, porffor aims for completeness.
- assemblyscript — TypeScript-subset compiling to WASM — small, performant output, but requires type annotations.
- jawsm — JS→WASM compiler in Rust. Compiles standard JS with a runtime that provides GC and closures in WASM.
- subscript — JS parser. Minimal, extensible, builds the exact AST jz needs without a full ES parser. Jessie subset keeps the grammar small and deterministic.
- watr — WAT to WASM compiler. Handles binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into a valid
.wasmbinary.
MIT • ॐ