 [](https://www.npmjs.com/package/jz) [](https://github.com/dy/jz/actions/workflows/test.yml) [](https://github.com/dy/jz/actions/workflows/bench.yml)
**JZ** (_javascript zero_) compiles JavaScript to fast, minimal WASM.
**[site](https://jz.js.org/)** / **[guide](https://jz.js.org/guide/)** / **[try it](https://jz.js.org/repl/)** / **[examples](https://jz.js.org/examples/)** / **[benchmarks](https://jz.js.org/bench/)**
| Good for | Not for |
|---|---|
| DSP, audio, synthesis | UI, DOM, frontend state |
| Images, video, pixels | Network, hot I/O, serving HTTP |
| Simulation, physics, games | Dynamic object models and monkey-patching |
| Parsers, codecs, compression | Allocation-heavy, long-lived object graphs |
| Scientific, numeric, edge ML | Security-sensitive cryptography and arbitrary-precision integers |
| Hashing, checksums, RNG | Tiny calls where the JS/WASM boundary dominates |
Used by: [color-space](https://github.com/colorjs/color-space), [audiojs](https://github.com/audiojs/)
## Usage
```sh
npm install jz
```
```js
import jz from 'jz'
const { exports } = jz`export const square = x => x * x`
exports.square(7) // 49
```
Options
`jz(source, opts)` compiles and instantiates. `compile(source, opts)` returns the wasm, and
`instantiate(wasm, { imports, memory, wasi })` runs it, also from `jz/interop` without the compiler:
`memory` is the memory of a module compiled to import one (`jz()` makes it from the descriptor),
`wasi: { write, read }` the stdio of a `host: 'wasi'` module in a JS host. An import or a memory the
module needs and the call does not give is a TypeError naming it.
These are all the options: any other key throws a `TypeError` naming it.
| Option | Use |
|---|---|
| `modules` | Sources for static imports, `{ './dep.js': source }`, each winning over a built-in module of its name (`math`, `string`). The CLI reads them from disk. |
| `imports` | Host modules for `import { fn } from "mod"`: functions, constants, or a whole namespace such as `Math`. |
| `define` | Compile-time constants injected as bindings, `{ DEBUG: false, N: 1024 }`: each key an identifier, each value a number, boolean, string, null, undefined, or an array or plain object of them. |
| `host` | `'js'` (default) or `'wasi'` for standalone runtimes. |
| `memory` | Initial pages, a `WebAssembly.Memory` or `jz.memory()` shared between modules, or `{ initial, maximum, shared, import, fixed }`: `initial` and `maximum` in 64 KiB pages, `shared` for a threads memory, `import` to take `env.memory` from the host, `fixed: ['process']` to require named Wasm exports to use fixed storage. |
| `optimize` | `true` (default), `'speed'`, `'size'`, `false`, or `{ level, simd, tailCall, exceptions, alloc, compact, rationalConst }`: `level` takes the same four values; `simd: false` and `tailCall: false` for engines without SIMD or tail calls; `exceptions: false` traps on a throw in a module that catches nothing; `alloc: false` is the raw standalone ABI, no allocator exports; `compact: false` keeps what a call replaced until `memory.reset()` ([Memory](#memory)); `rationalConst: false` rounds constant arithmetic per operation, as JavaScript does. |
| `randomSeed` | Number for a reproducible `Math.random`. |
| `names` | Emit the wasm name section for profilers and debuggers. |
| `sourceMap` | Debug build with statement locations and function names: `true` embeds the map; `{ source, url }` names the entry and an external map URL. The returned bytes also expose `.sourceMap`. Requires `optimize` absent or off (`false`, `{ level: false }`). |
| `wat` | `compile` only: return WAT text instead of bytes. |
| `warnings` | Callback for advisories (dynamic fallbacks, heap growth), each `{ code, message, file, line, column, fn }` in the program's own source and names; `file` is absent for the entry source, the position for an advisory about the whole program. |
| `why` | With `warnings`: also report each loop the vectorizer and each arena the rewind declined. |
## CLI
```sh
npm install -g jz
jz kernel.js # → kernel.wasm
jz kernel.js --wat # → kernel.wat
jz kernel.js -o out.wasm # custom output, - for stdout
jz kernel.js -O3 # fastest code; -Os smallest; -O0 none
jz kernel.js --host wasi # standalone WASI module
jz kernel.js --why # what the optimizer declined, and why
jz kernel.js --source-map # debug build + kernel.wasm.map, with source content
```
`jz --help` lists the rest: `-D`, `--memory`, `--no-simd`, `--no-tail-call`, `--names`, and `-O '{…}'` for the optimize object.
A `--host wasi` module is a WASI command: its `_start`, which `wasmtime` runs, runs the top-level
code, then the program's own `export function _start()` where it has one, then the timers they
armed until none is pending. A program that exports its own `_initialize` and no `_start` is a
reactor instead: its `_initialize` runs the top-level code, then its body. Every other export runs
the top-level code first where the entry has not, so the module also serves `wasmtime --invoke f`; `run` and every other name are ordinary
exports. In a JS host, `instantiate()` runs the top-level code and drives the timers;
`instance.exports._start()` runs the rest of the command.
## Examples
 chladni |
 robot motion |
 hydrogen |
See [all examples](https://jz.js.org/examples/).
## Memory
A heap module owns its linear memory and allocates by moving a pointer: no
free list, no collector thread, no finalizers. Three rules say when memory
comes back.
1. **A call's temporaries go at its return.** What a call allocated and did
not store into something older than itself (a module binding, state an
earlier call made, an argument) is gone when it returns: closures, objects
and growing buffers included. A loop whose iterations keep nothing does
the same per iteration. This costs one pointer restore.
2. **Kept state is compacted.** What a call stores into module state stays.
State replaced later (`state = { ...state, x }`, `buf = new
Float64Array(n)` each block, `log = log + line`) leaves the replaced value
behind until a compaction: as the outermost call returns (the heap is
measured every sixteenth), once the kept memory holds twice what the last
compaction walked and a page besides, live values slide down over dead
ones and every pointer is rewritten.
Within a call, a loop that keeps what it allocates compacts the same way
from the function's locals, once the memory above the loop's entry holds
twice what the last compaction walked and 16 MB besides. A compaction
walks everything the module reaches, at about a gigabyte a second on a
laptop, and comes no more often than once per twice that much allocation,
except as the heap nears its cap (`memory: { maximum }` in pages, else the
4 GB address space): then it comes once per eighth of the walk, so a
program whose live data is a large share of the cap keeps running where
it would otherwise fail to grow.
3. **Nothing the host holds moves.** The host's own allocations
(`memory.Array` and the other constructors) and every value a call handed
it (a closure, a typed array's view, an argument of a host import) pin the
heap below them. Make those before the state that churns; drop them to
let the memory go. `memory.reset()` returns the module to its start.
What a program holds, at `optimize: 'speed'`:
| program | memory held |
|---|---|
| a kernel reading and writing preallocated typed arrays | its buffers; it allocates nothing and never compacts |
| temporaries in a call or a loop iteration | what one call or iteration allocates, freed at its end |
| state replaced call after call | live state, at most twice over plus a page between compactions |
| a loop carrying a growing value | the live value, at most twice over plus 16 MB |
| a large kept structure beside churn | the structure, plus churn up to twice its size between compactions |
| any program under `memory: { maximum }` | within the cap while the live data leaves an eighth of it and 64 MB free |
A real-time callback follows the first row: allocate buffers once at module
level, write into them, return numbers or views of them. A plain JS typed
array passed to an export is copied in, and its numeric writes back out, on
every call. For a kernel called repeatedly, allocate its buffer in the
module's memory once (`const buf = memory.Float32Array(n)`) and pass that
handle: nothing is copied, and `memory.read(buf)` is a view of the same bytes
to fill and read (take it again after the memory grows). `optimize: {
compact: false }` turns compaction off, as `optimize: 'size'` does
(`{ level: 'size', compact: true }` keeps it); imported or shared memory never
compacts. `memory.used` reads what the module keeps.
## FAQ
What is not supported?
- **Runtime code:** `eval`, `Function`, `with`.
- **Reflection:** `Proxy`, `Reflect` and general prototype-chain lookup or mutation. Classes, inheritance and statically resolved prototype members are supported.
- **Accessor descriptors:** `Object.defineProperty` with `get`/`set` is unsupported; declare accessors on a class or fixed-layout object literal instead. Data-property definitions and descriptor reads work, with the differences below.
- **Object accessors:** getters/setters need a fixed property layout; mixing them with runtime-computed keys or a spread of unknown keys is unsupported.
- **Module dynamics:** top-level `await` other than `await import('./x.js')`, and `import()` of a specifier computed at run time. A literal `import('./x.js')` bundles `x.js`, which evaluates at the first `import()` unless a static import loaded it.
- **Platform:** Node modules, `Intl`, `Temporal`. Web APIs (streams, `Response`, the DOM) are the host's, reached as host globals under the JS host.
- **Builtins as values:** `f(Math)`, `[Map]`, `xs.map(parseInt)`. `typeof Math` answers; members (`xs.map(Math.sqrt)`), `Number`, `Boolean` and the typed-array constructors are values.
Modern JavaScript is supported: classes, generators, async/await, destructuring,
BigInt, typed arrays, Map/Set, RegExp, Date, JSON, timers and the Web codecs.
Where behaviour differs from JS:
- **Memory is freed by scope.** A call releases on return what it allocated and did not store into something older than itself; what it keeps stays, replaced state is reclaimed each time kept memory doubles, and what the host holds pins the heap below it ([Memory](#memory)). `WeakMap`, `WeakSet` and `WeakRef` hold strongly.
- **In-place array arguments.** An exported function that stores into an array argument and reads it back takes a Float64Array, a Float32Array or an Array there, which round as JS rounds them; another kind throws a TypeError.
- **Float sums in lanes.** At the default level and `'speed'`, a loop that sums floats may add in two lanes: the last digits of the sum can differ.
- **Constant arithmetic.** Compile-time `+ - * /` chains use exact rationals and round once: `0.1 + 0.2 - 0.3` becomes `2.7755575615628914e-17`. Set `optimize: { rationalConst: false }` for JavaScript's per-operation rounding.
- **Bitwise operands under 2^63.** `|`, `&`, `^`, `~`, the shifts and `Math.imul` convert an operand of magnitude under 2^63 as JS does. A larger one reads as -1, or as 0 when negative, where JS takes it modulo 2^32: `1e300 | 0` is -1. A store to an integer typed array converts exactly at any magnitude.
- **Math within 50 ulp of V8.** Measured against V8's results: `sin`, `cos`, `tan` up to 49 ulp, `asin`, `acos` 1, `atan`, `atan2` 10, exponentials, logarithms, hyperbolics and `hypot` 5, `pow` 11, a constant `** 2.4`-style exponent 40; `sqrt` and `cbrt` agree. A test comparing them bit for bit needs a tolerance. `x ** 0.5` is `Math.sqrt(x)`, so `(-Infinity) ** 0.5` is NaN.
- **BigInt is 64-bit.** It wraps past its range and has no `**`. `BigInt64Array` and `BigUint64Array` share one representation: a value of either passes `instanceof` for both, and `.constructor` of either reads undefined.
- **No holes.** `[1, , 3]` and a write past the end fill the gap with `undefined` elements: `1 in a`, `Object.keys` and `forEach` see them.
- **32-bit element indices.** An array holds up to 2²⁹−1024 elements, eight bytes each in wasm32's memory: a longer length, from `new Array(n)`, `a.length = n` or a store past it, throws the RangeError of an allocation, where JavaScript makes a sparse array. A store under a number that is not an index (`a[1.5] = v`, `a[-1] = v`) is dropped, where JavaScript gives the array a named property.
- **Regexes compile at build time.** `new RegExp(pattern)` needs a literal; `\p{…}`, `d` and `v` flags are unsupported.
- **ASCII case, UTC dates.** No locale or timezone tables: case conversion is ASCII, `normalize` returns its input, Date getters use UTC.
- **Fixed shapes.** Object fields are slots resolved at compile time; `Object.freeze` does nothing and errors carry `name` and `message` only.
- **Data descriptors only.** `Object.defineProperty` stores the value but ignores `writable`, `enumerable` and `configurable`. `Object.getOwnPropertyDescriptor` reports all three flags as true; a property whose value is `undefined` is reported as absent.
- **No runtime prototype chain.** `Object.create(null)` creates an empty object; `Object.create(obj)` copies supported object layouts instead of linking a prototype. Class inheritance and supported prototype assignments are resolved at compile time.
- **Class methods stay bound.** An extracted class method retains its instance. Object-literal methods use the call receiver. A static method, getter or setter that reads `this` is, in each class deriving it, the derived class's own, bound to it where the class is defined: `hasOwnProperty` finds it there, it is another function than the base's, and a program that stores another value into the base's is rejected. Statics a class reads through a base an expression names (`extends mixin(A)`) run with that base as `this`.
- **`new` of an untraced callee.** `new` of an arrow, a method, an async function, a generator or a builtin function throws a TypeError where the callee is written in place, or is a name, a class's or a literal object's member, an alias or a `bind` of one the module never reassigns. A callee value reached another way (a parameter, a function's result, a getter's value) is called instead.
- **Numeric export parameters.** A parameter an exported function never uses as a string is compiled as a number and converted at the boundary: `export let add = (a, b) => a + b` gives `add(1, '2')` as 3, where JavaScript concatenates. A use as a string counts through a copy, a call and a sum: `(s) => (s + s).slice(1)` takes a string.
- **Host globals bind at instantiation.** A global the program names without declaring it is the host's value when the module is instantiated: one the host defines or replaces later is not seen. `globalThis.Name` reads it as it stands.
- **Host iterators stay open.** A for-of over a host iterable that leaves early (`break`, `return`, a throw) does not call the iterator's `return()`, unless the program makes iterators of its own.
- **Exported classes dispatch on the instance.** A method taken from an exported class and called on an instance whose class overrides it (`Base.prototype.m.call(derived)`) throws a TypeError, and a host class extending an exported one constructs the exported class's instance.
- **Host methods named like builtins.** A host object the host passes in runs its own method of any name, a string's or an array's included. Where that builtin returns a number, a boolean or a string (`charCodeAt`, `includes`, `toUpperCase`), the call converts the host method's result to that type: an `o.charCodeAt(1)` that returns `'7'` gives 7.
Why no type annotations?
Ordinary code already carries useful type evidence: `let x = 0.5`,
`Float32Array`, an array index, a loop counter. JZ infers it instead of
turning the file into another language. Ambiguous values fall back to a
slower dynamic path, and `why` shows where.
How do values cross the boundary?
Numbers pass directly. Strings, arrays and typed arrays are copied in and
decoded on the way out; numeric writes to an array argument are copied back
after the call at the length the function left it, its non-numeric elements are
not. A typed array keeps its element kind: a Float32Array block runs as one. The copies
are released after the call unless the function keeps them. A plain object
passes by reference: the module reads, writes, lists and serializes it through
the host, so the caller sees its changes; a typed array the module stores on it
is a view of the module's memory, read back as the same array (the host's view
detaches if the memory grows). A JZ buffer (`memory.Float64Array(n)`,
`memory.Float32Array(n)`) is the storage itself, so hand hot loops one of those
instead of copying per call. A function the module returns, or stores on a host
object, reaches the host as a function that calls back into it. A typed array
the module hands a host object's method arrives as a copy the host may keep (a
stream queues it); what the host writes into it during the call is copied back.
A class the program exports is a class on the host: `new mod.C(…)` runs its
constructor in the module and returns the instance, whose fields read and
write the instance as it stands and whose methods, getters and setters run in
the module; handed back to the module it is the instance itself, and the
host's console prints it by its fields. An instance of a class deriving from it
that the program does not export reaches the host with its own overrides.
The class's static fields read and write the class, and its static methods,
getters and setters run in the module, those it inherits from its base included.
A property the program stores on an instance outside its fields reads, lists
and writes as it stands too; one the host adds stays on the host's object.
An export that is not a function reads as the binding's current value, as an
ES module's live binding does: `export let n` reads what the module last
assigned to it, and the host cannot assign it (strict code throws a
TypeError). The value crosses as a function's result does: an object or an
array is a fresh copy at each read, a typed array a view.
Under the JS host a name no declaration of the program binds is the host's
global: `new Response(text)`, `x instanceof WritableStream` and
`typeof WebSocket` ask the host, and reading a global the host lacks throws the
ReferenceError JavaScript throws. So do `new C()` and `x instanceof C` of a
constructor a variable or parameter holds (a class of the program answers for
its own instances, a generator function for its objects, though two that one
expression makes answer for each other's; `instanceof` of an ordinary function
the program declares rejects, as `new` of one calls it), `typeof` of a host
function (`'function'`) and a for-of
over a host iterable, which takes its iterator's values one per step. A member
of `console`, `crypto`, `navigator` or `performance` that JZ does not
implement (`crypto.subtle`) is the host's, and `console.log` hands each
argument to the host's console as the value it is. The `warnings` callback names
each such global; without a JS host it stays an error.
```js
const { exports } = jz`
export const greet = s => s.length
export const point = (x, y) => ({ x, y })
export const rgb = c => [c, c * 0.5, c * 0.2]
export const counter = () => { let n = 0; return { next: () => ++n } }
`
exports.greet('hello') // 5
exports.point(3, 4) // { x: 3, y: 4 }
exports.rgb(100) // [100, 50, 20]
const c = exports.counter()
c.next(); c.next() // 2
```
Host functions come in through `imports`; a tagged template inlines values and
functions at compile time:
```js
jz('import { log } from "host"; export const f = x => { log(x); return x }',
{ imports: { host: { log: console.log } } })
jz('import { sin, PI } from "math"; export const f = () => sin(PI / 2)',
{ imports: { math: Math } })
const scale = x => x * 10
jz`export const f = n => ${scale}(n) + ${[10, 20, 30]}[1]` // f(2) → 40
```
Ship the `.wasm` and load it with `instantiate` from `jz/interop`, a 20 KB
gzipped runtime that marshals values and decodes errors; the compiler stays in
the build.
How does memory work?
Heap modules use a bump allocator: no free list, no garbage collector. A call
gives back what it allocated when it returns, unless it wrote a value it made
into something older than itself: a module binding, state an earlier call
made, an argument. Whatever the call built and dropped goes, closures,
objects and growing buffers included, and the copies of its arguments with
it; a loop whose iterations keep nothing does the same per iteration. A value
the call returns is the caller's: the host takes a copy of a string, an
array, an object or a collection, and the call's memory goes; a typed array
is a view of the module's memory, and a call that returns one it made keeps
what it allocated (`heap-return` in `warnings`). A call that does keep a value keeps what the value reaches
and whatever it allocated before it: the heap goes back to the end of the
highest block kept, so state made first and temporaries after it cost the
state alone (at `optimize: 'size'` such a call keeps all it allocated).
State replaced call after call (`state = { ...state, x }`, `buf = new
Float64Array(n)` each block, `log = log + line`) leaves what it replaced
behind, which the module compacts: as an outermost call returns, once the
kept memory holds twice what the last compaction walked (and a page besides),
the live values slide down over the dead ones and every pointer to them is
rewritten, so a program keeps what it reaches and no more. A loop whose
iteration allocates and keeps some of it does the same within a call, with
the function's locals as roots, so `s = x + s` or `a = a.concat(b)` over a
long loop holds the live value, not every one it replaced. Nothing the host
holds moves: its own allocations (`memory.Array` and the other constructors)
and every value a call handed it (a closure, a typed array's view, an
argument of a host import) pin the heap below them, so make those before
the state that churns. `optimize: { compact: false }` turns compaction off,
as `optimize: 'size'` does (`{ level: 'size', compact: true }` keeps it);
imported or shared memory never compacts. What a call keeps stays until
`memory.reset()`.
For owned memory, reset returns the module to its state after instantiation and invalidates every
earlier pointer: a binding, an array, a collection and an object the module
made as it started read as they started (an object's field that holds a number
keeps it). Imported/shared memory resets the entire arena; instantiate the module
again before calling it. `memory.used` reads the bytes above the reset mark, so a host can see a call
that keeps memory; `warnings` names each export that keeps memory on
every call and why at compile time (`heap-per-call`).
```js
for (let i = 0; i < 1000; i++) {
exports.process(100) // allocates on the WASM heap and keeps some
memory.reset() // drop the batch
}
```
Memory that cannot grow, past `memory: { maximum }`, the 4 GiB of wasm32 or
the engine's limit, throws the RangeError of an allocation, which the program
can catch; after a `memory.reset()` the module runs again.
Host Number values remain numbers, including NaNs with custom payloads (one
whose bits spell the undefined or null box enters as the canonical NaN). Pass
allocated memory by its bigint handle from `memory.Array`, `memory.Float64Array`
or the other constructors. A handle crosses as its BigInt, and nothing reads a
Number's payload as one, `memory.read` included. An export's Number result reads
as a Number whatever its payload; one that may be undefined or null, and a void
export's undefined, read as those on every engine.
For DSP, `memory: { fixed: ['process'] }` checks the final Wasm call graph:
the selected exports and their typed variants must run without heap allocation,
memory growth, host calls, recursion or reachability tracing. Initialization
can allocate persistent state. An export whose memory behavior cannot be proved
is a compile error with the failing call path.
```js
const { instance, memory } = jz(`
export function process(a) {
const tmp = new Float32Array(128)
for (let i = 0; i < 128; i++) tmp[i] = a[i] * 0.5
for (let i = 0; i < 128; i++) a[i] = tmp[127 - i]
}
`, { memory: { fixed: ['process'] } })
const input = memory.Float64Array(new Float64Array(128)) // prepare once
instance.exports.process(input) // borrowed storage through the raw Wasm ABI
```
The compiler places eligible local typed arrays in the selected functions and
their named helpers in reusable scratch storage,
clears them at each construction, and shares equal-layout slots across disjoint
lifetimes. It currently handles constant sizes up to 64 KiB per array with
numeric indexed accesses and length reads; escaping buffers retain their normal
allocation and cannot pass the fixed-memory check. Storage belongs to one
instance, so imported and shared memories are rejected.
In module-owned memory, optimized builds (the default level and `'speed'`) place
such arrays in any function, without a check: where nothing between an array's
construction and its last use can enter the function again (a host import, a
closure value, a callback, a suspension), the array is static storage, and a
helper every call hands it to reads it at that address. Any other array stays a
heap allocation.
Fixed mode disables `arenaReach` for the module. General mode can use that
runtime walk to find how far an escaping call's heap can rewind. The fixed
guarantee covers Wasm execution, excluding JS argument/result marshalling and
OS scheduling; it is not an audio-deadline guarantee. Prepare buffers before
processing and use the matching raw exports when the host must avoid marshalling.
`jz.memory()` creates one memory for several modules, so one can read what
another allocated:
```js
const memory = jz.memory()
const a = jz('export const make = () => ({ x: 10, y: 20 })', { memory })
const b = jz('export const read = o => o.x + o.y', { memory })
b.exports.read(a.exports.make()) // 30
```
`memory: { shared: true }` compiles for threads, with `Atomics` lowering to
wasm atomics. Shared typed arrays and numeric scalars cross; strings, objects and
Symbols stay local to their instance. Symbol values retain identity and description
across ordinary host calls, and `Symbol.for` uses the host registry.
Can I use npm packages and ES modules?
Packages compile when their source fits the language. `import`/`export` bundle
into one module at compile time; there is no runtime module resolution. The CLI
reads relative and bare specifiers from disk; in a browser, pass sources through
`modules`. Circular imports fail at compile time.
```js
jz('import { add } from "./math.js"; export const f = (a, b) => add(a, b)',
{ modules: { './math.js': 'export const add = (a, b) => a + b' } })
```
Is it fast? How small?
Faster than V8 and AssemblyScript on almost every kernel we measure, about 2×
on average. Every number, and every loss, is on the
[bench page](https://jz.js.org/bench/).
V8 also compiles hot JS to machine code. JZ can beat it by analyzing the whole
program ahead of time: inferred types, fixed layouts, direct calls and SIMD on
suitable loops, with no garbage collector.
A large module pays the engine's warm-up, which the bench times as it does a
JIT's: V8 runs each wasm function in its baseline tier until the optimizing tier
has compiled it. On the 4-core CI runners that is 9 to 13 of watr's 21 timed
rounds (a 320 KB module), so watr trails V8 and JSC there. With every round
optimized (`--no-liftoff`), one runner timed it at 996 µs against JSC's 1866 µs.
Nothing ships that the program does not reach: a heap-free numeric module has
no memory, allocator or startup, and an empty program is an empty module.
Size-optimized JZ is about a fifth smaller than AssemblyScript (geomean 0.79×
over the 51 cases both compile) while keeping JavaScript's bounds checks.
How do I inspect or debug the output?
- A rejected program names the construct that faulted: its module (when there
are several), line and column, and the source line.
- `jz kernel.js --wat` or `compile(src, { wat: true })` prints the WAT. Search
for `v128` to confirm vectorization and for `__dyn_get` or `__ext_call` to find
dynamic fallbacks.
- `--why` names the first operation that kept each loop scalar, each arena
unreclaimed and each fixed-count array's checks in place (`array-open`);
`warnings` collects the same advisories from the API, with each
property read left dynamic (`deopt-prop-read`), each class kept as closures
(`class-generic`) and the first cause an object shape is lost by (`shape-lost`).
- Float loop counters, plain arrays and loop-carried dependencies are the common
reasons a kernel stays scalar.
How does JZ compare with Porffor, AssemblyScript, scriptc etc.?
- **[Porffor](https://github.com/CanadaHonk/porffor)** targets broad engine
coverage with an AOT JS→IR→C/native compiler; its current alpha line has no
WASM target. JZ emits WASM first for an inferred typed subset. JZ may not lose
to Porffor's native artifact on speed or size per case or by geomean.
- **[js2wasm](https://github.com/loopdive/js2)** AOT-compiles JS/TS to WasmGC with full ECMAScript compatibility as its goal, guarding, boxing or calling host imports where code stays dynamic; its authors call it a research prototype. JZ compiles a subset to linear memory without WasmGC and aims at kernel speed rather than full conformance.
- **[scriptc](https://github.com/vercel-labs/scriptc)** also AOT-compiles typed JS/TS without an engine (TS annotations → LLVM); untyped values compile as boxed checked values. It is native-first with WASI as a target; JZ is WASM-first, infers types from idiomatic untyped JS, and keeps dynamic fallbacks inside the WASM module.
- **[Static Hermes](https://github.com/facebook/hermes)** (`shermes`, folded into Hermes V1) AOT-compiles JS through C to native code linked with the Hermes runtime and GC; tmikov's [`shermes-wasm`](https://github.com/tmikov/hermes/tree/shermes-wasm) branch builds the same output with that runtime for WASI through wasi-sdk. Sound TS/Flow annotations unlock typed codegen; untyped code keeps Hermes's dynamic semantics. JZ needs no annotations and ships no engine runtime in its output.
- **[TypeScriptCompiler](https://github.com/ASDAlexander77/TypeScriptCompiler)** (`tslang`) compiles TypeScript through LLVM/MLIR to native executables or WebAssembly. Our native benchmark lane feeds it the unchanged JavaScript corpus saved as `.ts`, with its default library and garbage collector.
- **[Perry](https://perryts.com/)** compiles JS/TS through LLVM to native executables with a linked runtime and garbage collector. Our [benchmarks](bench/README.md) cover its native output on the unchanged JS corpus.
- **[Tov](https://github.com/richiemcilroy/Tov)** is a TypeScript-shaped language compiled through C to native binaries. It rejects untyped JS (no `any`, typed arrays or `Math.imul`), so the benchmarks run it like AssemblyScript, on hand-ported cases.
- **[Goose](https://github.com/aardappel/goose)**, Wouter van Oortmerssen's memory-safe systems language, has no heap: every dynamic value lives inline on bump-allocated data stacks the compiler assigns, and a program compiles to one C file. Its source is not JavaScript, so the benchmarks run it like Tov, on hand-ported cases, as a native contender.
- **[AssemblyScript](https://github.com/AssemblyScript/assemblyscript)** produces lean WASM from a TypeScript dialect with WASM types (`i32`, `f64`, …); its source is not JavaScript that runs as is. JZ's source is plain JavaScript that runs unchanged in any engine.
- **Rust, C, Zig, Go, and MoonBit** compiled to wasm run behind JZ by geomean on
the corpus: rustc, clang and zig about 2×, Go and MoonBit over 4×. As native
binaries Rust, Zig and Go still trail and C is level, so a rewrite buys a
second toolchain and test suite for slower wasm.
- **[Javy](https://github.com/bytecodealliance/javy)**,
**[ComponentizeJS](https://github.com/bytecodealliance/ComponentizeJS)**,
**[componentize-qjs](https://github.com/andreiltd/componentize-qjs)** and
QuickJS-NG builds for WASI such as
[quickjs-ng-wasi-wasm](https://github.com/guest271314/quickjs-ng-wasi-wasm)
accept broader JavaScript by shipping an interpreter or engine inside WASM
(StarlingMonkey or QuickJS). JZ ships no engine in its output.
Is JZ production-ready?
Since 1.0 the API, options and CLI flags change only in a major version: pin
the major and re-test upgrades. CI runs the core suite, selected test262 tests,
the benchmark gate and a self-compile
(`npm run test:self` builds and tests a fresh compiler in a temporary directory).
The same WASM lowers to a native executable through wasm2c
(`node scripts/native/compile.mjs app.js`); see the
[native pipeline](scripts/native/README.md).
Adoption is ejectable: remove the JZ build step and the source remains JavaScript.
## Stability
The entry points `jz` (the default export with `jz.memory`, `compile`
and `instantiate`), `jz/interop` (`instantiate`), the options above and the CLI flags are the contract; removing or
changing them takes a major version. The class of an error is stable within a
major; its message may change. Accepted programs preserve JavaScript values,
exceptions and evaluation order at every optimization level, outside the
differences listed above: a value a call throws reaches the host as JavaScript
throws it, an Error as the host's Error of its class. The raw wasm ABI (pointer
layout, the `__jz_` exports, custom sections) is not stable across versions: load a
prebuilt module with `jz/interop` from the jz that built it.
[CONTRIBUTING](CONTRIBUTING.md#public-contract-and-abi) has the details.
MIT, ॐ