Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3,030 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JZ logo

stability npm test bench

JZ (javascript zero) is a distilled JS subset that compiles to fast, minimal WASM.

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

site  /  try it  /  examples  /  benchmarks

Usage

npm install jz
import { compile } from 'jz'

const wasm = compile('export const dist = (x, y) => (x*x + y*y) ** 0.5')
const { instance } = await WebAssembly.instantiate(wasm)

instance.exports.dist(3, 4) // 5
Options

Options are passed as jz(source, opts) or compile(source, opts):

Option Use
modules: { specifier: source } Static ES imports to bundle. CLI import resolution does this from files automatically.
imports: { mod: host } Host imports import { fn } from "mod".
memory Pass memory: N for owned memory with N initial pages, or memory: jz.memory() / WebAssembly.Memory to share across modules. maxMemory: N caps growth; importMemory: true imports env.memory instead of exporting own.
host: 'js' | 'wasi' Runtime-service lowering. Default js; wasi for standalone runtimes.
optimize false/0 off, 1 minimal, true/2 default (all stable passes), 3/'speed' trades size for speed, 'size' for smallest wasm. (Object form for per-pass overrides is internal/unstable.)
define Compile-time constants injected as top-level bindings, e.g. { DEBUG: false, PORT: 8080 } (numbers, booleans, strings, null, or literal arrays/objects).
strict: true Skip jzify lowering and reject dynamic fallbacks such as obj[k], for-in, and unknown receiver methods.
alloc: false Omit allocator exports (_alloc/_clear) from modules that never marshal heap values.
noSimd: true Disable auto-vectorization. Explicit f32x4 and i32x4 intrinsics still compile.
whyNotSimd: true Report the first operation that prevented each loop from being vectorized. Warnings go to the warnings sink.
experimentalStencil: true Vectorize neighbour-load stencils such as b[i] = f(a[i-1], a[i], a[i+1]) and 2-D 5-point sweeps to f64x2. Unstable and off by default.
experimentalOuterStrip: true Strip-mine a pixel loop containing an inner reduction into f64x2. Each lane keeps scalar accumulation order. Unstable and off by default.
randomSeed Set a number for a reproducible Math.random sequence. The default uses host entropy; true requests entropy explicitly.
wat: true compile() returns WAT text instead of WASM binary.
names: true Emit a WASM name section (function symbols) for profilers/debuggers.
profile Mutable sink for compile-stage timings (entries/totals per phase).

CLI

npm install -g jz

jz program.js              # → program.wasm
jz program.js --wat        # → program.wat
jz program.js -o out.wasm  # custom output (- for stdout)
jz program.js -O3          # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
jz program.js --host wasi  # standalone WASI output
jz --strict program.js     # pure canonical subset (also implied by .jz extension)
jz -e "1 + 2"              # eval → 3
jz --help
jz - min JS → WASM compiler

Usage:
  jz <file.js>              Compile JS to WASM (full JS subset; .jz = strict)
  jz --strict <file.js>     Strict mode: pure canonical subset, no lowering
  jz --jzify <file.js>      Transform JS → jz source (auto-derives output file)
  jz -e <expression>        Evaluate expression
  jz --help                 Show this help

Examples:
  jz program.js                    # → program.wasm
  jz program.js --wat              # → program.wat
  jz program.js -o out.wasm        # custom output name
  jz program.js -o -               # write to stdout
  jz program.js -O3                # optimize for speed
  jz program.js -Os                # optimize for size
  jz program.js -D DEBUG=false     # inject a compile-time constant
  jz program.js --memory 64        # 64 initial pages (4 MB)
  jz program.js --host wasi        # emit WASI Preview 1 imports
  jz --strict program.js           # strict mode
  jz --jzify lib.js                # → lib.jz
  jz -e "1 + 2"

Options:
  --output, -o <file>       Output file (.wat, .wasm, or - for stdout)
  -O<n>, --optimize <n>     Optimization level: 0 off, 1 minimal, 2 default (all
                            stable passes), 3 speed. -Os optimizes for size.
  --define, -D <K=V>        Inject a compile-time constant (VALUE parsed as JSON,
                            else string). Repeatable.
  --host <js|wasi>          Runtime-service lowering (default js)
  --memory <pages>          Initial memory size in 64 KiB pages
  --max-memory <pages>      Cap memory growth at this many pages (default unbounded)
  --import-memory           Import env.memory instead of exporting own memory
  --no-alloc                Omit _alloc/_clear allocator exports (standalone wasm)
  --no-simd                 Disable auto-vectorization (no v128) for non-SIMD engines
  --why-not-simd            Report, per loop, why the auto-vectorizer declined it
  --experimental-stencil    Vectorize neighbour-load stencils (a[i±1]); opt-in
  --experimental-outer-strip  Strip-mine pixel loops over an inner reduction to f64x2; opt-in
  --no-tail-call            Use ordinary call frames instead of return_call
  --names                   Emit wasm name section for profilers/debuggers
  --stats                   Print compile-phase timings to stderr
  --strict                  Pure canonical subset: reject full-JS syntax + dynamic fallbacks
  --jzify                   Transform JS to jz source (no compilation)
  --eval, -e                Evaluate expression or file
  --wat                     Output WAT text instead of binary
  --resolve                 Resolve bare specifiers via Node.js module resolution
  --imports <file>          JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
  --version, -v             Show version number

Examples

Chladni plate
chladni: swept-frequency nodal figures.
Dynamic Window Approach
dwa: local robot motion planning.
Hydrogen orbital
hydrogen: electron probability clouds.

See all examples.

FAQ

What JS is supported?
┌────────────────────────────────────────────────────────────────────────┐
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ jz strict                                                          │ │
│ │   let/const  arrows  rest  destructuring  import/export            │ │
│ │   if/else  for/while/do-while/of  break/continue                   │ │
│ │   try/catch/finally  throw                                         │ │
│ │   numbers  strings  booleans  arrays  objects  template literals   │ │
│ │   Math  Number  String  Array  Object  JSON  RegExp  Symbol        │ │
│ │   ArrayBuffer  DataView  typed arrays  Map  Set  Atomics           │ │
│ │   Float16Array  base64/hex codecs  TextEncoder  timers  Date       │ │
│ │   crypto randomness  URLSearchParams  structuredClone  Set algebra │ │
│ │   WASI file I/O                                                    │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ jz default (jzify)                                                     │
│   var  function  arguments  switch                                     │
│   class  new  this  extends  super  private/static fields              │
│   generators  iterator helpers  async/await  Promise  for await        │
│   loose equality  instanceof  WeakMap  WeakSet                         │
└────────────────────────────────────────────────────────────────────────┘

outside the model
  eval  Function  with  Proxy  Reflect
  property descriptors  getters/setters  live prototypes
  dynamic import  DOM  Intl  Temporal  Node APIs
What differs from JS?
  • Numbers. Numbers are f64. Proven integers use i32 and wrap at ±2³¹. Applying x | 0 to an f64 with |x| ≥ 2⁶³ saturates instead of ES-wrapping.
  • Math. Basic operations are IEEE-exact. Transcendentals use JZ's own kernels and may differ from the host library in their last bits. Math.sumPrecise accumulates exactly and rounds once.
  • Strings. Strings are UTF-8 bytes. Length, indexing, slicing, search, and regular-expression positions count bytes; case conversion is ASCII-only.
  • Objects. Literal fields have fixed slots. Computed keys use hash storage; live prototype chains and property descriptors do not exist.
  • Array indices. Indices coerce to i32. Plain arrays are bounds checked; typed arrays use raw fixed-size linear-memory access, so invalid indices can read unrelated memory or trap.
  • Memory. There is no garbage collector. Call memory.reset() between independent allocation batches. It invalidates every previous pointer.
  • Generators and async. Both lower to state machines. Jobs drain at host boundaries; try across yield or await is unsupported.
  • Dates. Date getters use UTC.
  • BigInt. BigInt is signed 64-bit, not arbitrary precision.

The compiler accepts these differences to keep the language statically compilable and its emitted modules lean. Do not compile code whose correctness depends on full ECMAScript edge semantics.

What is not supported?
  • Proxy and Reflect. Traps do not apply to structs with compile-time offsets.
  • Property descriptors and accessors. Objects store values without writable, enumerable, getter, or setter metadata.
  • Live prototype chains. __proto__, delegation, and monkey-patching are unsupported. Object.create(proto) makes a shallow copy; method dispatch is static.
  • Deleting literal properties. Literal object shapes are fixed. Dictionary-mode delete o[k] works.
  • eval, the Function constructor, and with. These would require a compiler or interpreter at runtime.
  • Intl and Temporal. ICU, CLDR, and timezone tables exceed the intended module size. Date uses UTC.
  • UTF-16 and Unicode tables. Strings are UTF-8 bytes. Unicode property classes, normalization, and locale case conversion are unsupported.
  • Arbitrary-precision BigInt. BigInt is a signed 64-bit integer and wraps past its range. Security cryptography is outside the scope.
  • Boolean identity in dynamic keys. Runtime boolean keys use their numeric carrier, so o[b] reads '1' for true. Static boolean keys fold correctly.
  • WeakRef and FinalizationRegistry. There is no garbage collector to observe. WeakMap and WeakSet use Map and Set semantics.
  • Legacy browser features, DOM, and Node APIs. ECMAScript Annex B specifies legacy compatibility features required in web browsers; JZ omits its additional syntax. DOM and Node services stay in the host.
Why no types?

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 take a slower, always-correct dynamic path.

Can I use npm packages and ES modules?

Packages compile when their source fits the JZ language. Code using the DOM or Node APIs does not; host services must cross as imports.

  • Relative imports (./dep.js) bundle at compile time.
  • Bare package specifiers (import { x } from "pkg") require the CLI's --resolve or source supplied via { modules }.
  • Transitive imports work; circular imports fail at compile time.
  • There is no runtime module resolution.
const { exports } = jz(
  'import { add } from "./math.js"; export const f = (a, b) => add(a, b)',
  { modules: { './math.js': 'export const add = (a, b) => a + b' } }
)
How do I call host functions?

Import from a named module in the compiled source, then provide that module through { imports }. Functions become WASM imports; numeric constants fold.

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 } }
)
Can I interpolate values (template literals)?

As a tagged template, jz inserts interpolated values at compile time. Numbers and booleans inline directly; strings, arrays, and objects become JZ literals:

jz`export let f = () => ${'hello'}.length`               // 5
jz`export let f = () => ${[10, 20, 30]}[1]`              // 20
jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3

const scale = (x) => x * 10
jz`export let f = (n) => ${scale}(n) + 1`                // f(2) → 21, host-called

Interpolated functions become host calls. Non-serializable values such as host objects and class instances use post-instantiation getters.

How do values cross between JS and WASM?

Numbers cross as f64 or i32. Heap values use tagged pointers internally; the wrapped exports returned by jz() marshal arguments and decode results:

const { exports } = jz`
  export const greet = s => s.length
  export const dist = p => (p.x * p.x + p.y * p.y) ** 0.5
  export const point = (x, y) => ({ x, y })
  export const sum = a => { let n = 0; for (const x of a) n += x; return n }
`

exports.greet('hello')                       // 5
exports.dist({ x: 3, y: 4 })                 // 5
exports.point(3, 4)                          // { x: 3, y: 4 }
exports.sum(new Float64Array([1, 2, 3]))      // 6

For raw instance.exports calls, memory.String, .Array, typed-array methods, and .Object allocate on the WASM heap and return a pointer; memory.read decodes a raw result. Object keys must match a compiled schema. Numeric arrays of at most eight elements may return as WASM multi-values.

What ships at runtime, and where does it run?

The compiler runs at build time or synchronously in a browser/Worker. The resulting module runs in browsers, Workers, Node, Deno, Bun, and standalone WASM engines.

  • Heap-free numeric modules ship as ordinary .wasm with no JZ runtime, memory, allocator, GC, or bundled JavaScript engine.
  • Heap values use the optional jz/interop bridge, about 6 KB gzipped, for memory codecs, errors, WASI, and host imports.
  • host: 'js' binds services such as time and console through env.*.
  • host: 'wasi' emits WASI Preview 1 imports for standalone engines. When manually instantiating a reactor, call instance.exports._initialize?.() once.

A module imports env, wasi_snapshot_preview1, or neither, according to what the source actually uses.

How does memory work?

Heap-using modules use a growing bump allocator with no free list or garbage collector. Allocations are discarded in batches:

for (let i = 0; i < 1000; i++) {
  const result = exports.process(100) // allocates on the WASM heap
  memory.reset()                      // drop the whole batch
}

memory.reset() invalidates every previous pointer. Scalar modules without heap values omit the allocator.

For threads, sharedMemory: true compiles against shared WebAssembly.Memory, with Atomics.* lowering to WASM thread operations; jz.pool(src, { threads }) runs one kernel across worker threads. Shared typed arrays and scalars cross; strings and objects stay thread-local.

jz.memory() creates memory shared by multiple compiled modules. Schemas accumulate, so one module can consume an object created by another:

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

Pass an existing WebAssembly.Memory to jz.memory(memory) to wrap it.

Is it fast?

JZ leads V8 and AssemblyScript by geometric mean on the covered corpus and targets near-native speed. The release gate is stricter than an average: JZ must be the fastest WASM on every case. Per-case numbers, missing target coverage, and every measured loss stay visible on the bench page; a rival win is a bug to close, not an exception to hide.

How small is the output?

A heap-free numeric program emits no memory, allocator, or startup function; an empty program emits an empty module. Runtime helpers and standard-library kernels are included only when reachable.

In the published benchmark corpus, size-optimized JZ modules are 1.02× the size of AssemblyScript modules by geometric mean. AssemblyScript's ports use unchecked array access while JZ retains JavaScript out-of-bounds guards. Most JZ modules in the corpus are single-digit kilobytes.

  • optimize: 'size' disables unrolling and SIMD.
  • alloc: false omits allocator exports from numeric modules.
  • Function names are omitted unless names: true is set.

The compiler stays in the build step; these sizes are what ships.

Which optimizations are applied?

At the default optimize: 2, JZ applies:

  • Type and representation inference from syntax and use sites for parameters, results, objects, and arrays. Ambiguous values remain dynamic.
  • Direct typed-array memory access with proven bounds and aliases.
  • Escape analysis and arena rewind for short-lived aggregates.
  • Constant folding, common-subexpression and dead-store elimination, inlining, invariant hoisting, induction reduction, and loop unrolling.
  • SIMD-128 vectorization of independent maps, reductions, conditionals, and byte scans. Loop-carried dependencies remain scalar.
  • Tree shaking and reachability-gated runtime helpers.

optimize: 3 / 'speed' accepts additional code size for throughput; optimize: 'size' disables unrolling and SIMD.

How do I inspect or debug output?
  • Run the same source under Node and compare results, allowing for the documented differences above.
  • jz program.js --wat or compile(src, { wat: true }) prints WAT.
  • Search WAT for v128 to confirm vectorization and for __dyn_get or __ext_call to find dynamic fallbacks.
  • --why-not-simd reports the first operation blocking each loop.
  • --strict turns dynamic property and method fallbacks into compile errors.
  • --names emits symbols; --stats prints compile-stage timings.

Float loop counters, plain arrays, and loop-carried dependencies are common reasons a kernel remains slower or scalar.

How does JZ compare with Porffor, AssemblyScript, etc.?
  • AssemblyScript emits lean WASM from a typed TypeScript-like language; its source is not directly executable JavaScript.
  • Rust, C, Zig, Go, and MoonBit offer explicit static types and mature native toolchains, but require a second implementation when the source of truth is JS.
  • Javy and ComponentizeJS accept broader JavaScript by shipping an interpreter or engine inside WASM.
  • Porffor and scriptc target native executables rather than WASM. JZ emits WASM first and can then lower it to native C.

JZ keeps the source executable and testable as JavaScript, then gets speed by accepting a narrower, native-style semantic contract.

Can JZ compile itself?

Yes. npm run test:self compiles JZ's parser, jzify pass, compiler, optimizer, and encoder into dist/jz.wasm. That WASM-hosted compiler then compiles real programs, whose output is instantiated and checked against the native compiler.

dist/jz.wasm is a self-host test artifact, not a runtime shipped with compiled programs.

Can JZ compile to native?

Indirectly. JZ emits WASM, which WABT's wasm2c can translate to C and a C compiler can turn into a native executable:

JS → JZ → WASM → wasm2c → C → clang → native

The native benchmark lane uses --host wasi -O3 --no-tail-call, then wasm2c and clang -O3; it beats V8 on 19 of 21 watr examples and ties the other two on the reference M4 Max run. A host harness and wasm2c runtime must be linked, so this is currently a toolchain rather than a one-command JZ target. See the native pipeline.

Is JZ production-ready?

JZ is experimental and pre-1.0. The supported language and WASM ABI may change; pin a version and re-test upgrades. CI runs the core suite, selected test262 language and built-in tests, benchmark checks, and a self-host build.

Adoption is ejectable: remove the JZ build step and the source remains JavaScript.

Used by

color-space, audiojs

MIT.