Skip to content

dy/jz

Repository files navigation

jz logo

stability npm test test262 bench

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) // 5

Why?

Write 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

Usage

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) // 42
Options

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.

CLI

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 --help

Language

JZ 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

FAQ

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, resizable ArrayBuffer, DataView, dynamic RegExp patterns, the Symbol registry and most well-known symbols, the full BigInt surface. 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/false compile to 1/0. typeof is recovered statically where a boolean can be proven, but a boolean carried through an untyped binding reports "number", String(true) is "1", and parseInt(true) is 1. A real-boolean carrier is planned.
  • Objects are fixed-layout schemas — key set and order fixed at the literal, delete rejected, memory.Object({...}) must match the source key order.
  • Errors are untaggedthrow carries a value, not a typed Error; e instanceof TypeError does not discriminate.
  • Set/Map iterate 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`  // 6

Functions 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 automatically

Browser: 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-time

instantiate(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 pointer

jz.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.wasm

Pure 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.* imports

host: '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 the jz() 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.sinmath.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:

  1. 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), typeof guards, and assignments the way a human reader does. The annotation duplicates what's already in the code.
  2. 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?

Yes, via wasm2c or w2c2:

jz program.js -o program.wasm
wasm2c program.wasm -o program.c
cc program.c -o program

Benchmark

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, 0.88× native C (clang -O3). Geomean size: jz 0.85× AS. jz wasm runs at native speed — ahead of clang -O3 on the corpus geomean — and test/bench.js gates it so a regression fails CI.

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.

Alternatives

  • 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.

Build with

  • 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 .wasm binary.

MIT •

About

JS→WASM

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors