jz 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
- <img src="jz.svg" alt="jz logo" width="120"/>
1
+ <a href="https://dy.github.io/jz/"><img src="jz.svg" alt="JZ logo" width="120"/></a>
2
2
 
3
3
  ![stability](https://img.shields.io/badge/stability-experimental-black) [![npm](https://img.shields.io/npm/v/jz?color=black)](http://npmjs.org/package/jz) [![test](https://github.com/dy/jz/actions/workflows/test.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/test.yml) [![bench](https://github.com/dy/jz/actions/workflows/bench.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/bench.yml)
4
4
 
5
- **jz** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
6
-
5
+ **JZ** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
7
6
 
8
7
  ```js
9
8
  import jz from 'jz'
@@ -12,19 +11,23 @@ const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
12
11
  dist(3, 4) // 5
13
12
  ```
14
13
 
15
- **[repl](https://dy.github.io/jz/repl/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
14
+ **[site](https://dy.github.io/jz/)** · **[repl](https://dy.github.io/jz/repl/)**<!-- · **[floatbeat](https://dy.github.io/jz/floatbeat/)**--> · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
16
15
 
17
16
 
18
17
  ## Why?
19
18
 
20
- jz distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) and **compiles JS ahead-of-time to WASM**: no runtime, no GC, no legacy, no spec creep, near-native perf with unlocked SIMD. **Valid jz is valid JS** – run and test as JS, compile to portable WASM ([known divergences](#faq)).
19
+ JZ distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) and **compiles JS ahead-of-time to WASM**: no runtime, no GC, no legacy, no spec creep, near-native perf with unlocked SIMD. **Valid JZ is valid JS** – run and test as JS, compile to WASM.
20
+
21
+ | Good for | Not for |
22
+ |------------------------------|---------------------------|
23
+ | DSP, audio, synthesis | UI, DOM, the frontend |
24
+ | Image, video, pixels | Servers, APIs, I/O |
25
+ | Simulation, physics, games | Async, promises, events |
26
+ | Parsers, codecs, compression | Dynamic, polymorphic, OOP |
27
+ | Scientific, numeric, ML | Security crypto, big-ints |
28
+ | Hashing, checksums, RNG | Glue, plumbing, orchestration |
21
29
 
22
- | Good for | Not for |
23
- |-----------------------------|----------------------------|
24
- | Numeric / math compute | UI / frontend |
25
- | DSP / audio / bytebeats | Backend / APIs |
26
- | Parsing / transforms | Async / I/O-heavy logic |
27
- | WASM utilities | JavaScript runtime |
30
+ Output `.wasm` is portable — run it in any host (browser, Node, Deno, edge, plugins), or take it native via [wasm2c](https://github.com/WebAssembly/wabt) (wasm → C → binary).
28
31
 
29
32
 
30
33
  ## Usage
@@ -32,7 +35,7 @@ jz distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_D
32
35
  `npm install jz`
33
36
 
34
37
  ```js
35
- import jz, { compile } from 'jz'
38
+ import jz, { compile, compileModule, instantiate } from 'jz'
36
39
 
37
40
  // Compile + instantiate
38
41
  const { exports: { add } } = jz('export let add = (a, b) => a + b')
@@ -41,6 +44,10 @@ add(2, 3) // 5
41
44
  // Compile only — raw WASM binary
42
45
  const wasm = compile('export let f = (x) => x * 2')
43
46
 
47
+ // Compile once → instantiate many (pays the AOT + validate cost once)
48
+ const mod = compileModule('export let f = (x) => x * 2')
49
+ instantiate(mod).exports.f(21) // 42 — repeat cheaply, no recompile
50
+
44
51
  // Async startup
45
52
  const asyncMod = await WebAssembly.compile(wasm)
46
53
  const asyncInst = await WebAssembly.instantiate(asyncMod)
@@ -56,14 +63,20 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
56
63
  |---|---|
57
64
  | `modules: { specifier: source }` | Static ES imports to bundle. CLI import resolution does this from files automatically. |
58
65
  | `imports: { mod: host }` | Host imports `import { fn } from "mod"`. |
59
- | `memory` | Pass `memory: N` for owned memory with `N` initial pages, or `memory: jz.memory()` / `WebAssembly.Memory` to share across modules. |
66
+ | `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. |
60
67
  | `host: 'js' \| 'wasi'` | Runtime-service lowering. Default `js`; `wasi` for standalone runtimes. |
61
- | `optimize` | `false`/`0` off, `1` size-only, `true`/`2` default (all stable passes), `3` trades size for speed. String aliases: `'size'`, `'balanced'` (= default), `'speed'`. Object form overrides individual passes. |
68
+ | `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.) |
69
+ | `define` | Compile-time constants injected as top-level bindings, e.g. `{ DEBUG: false, PORT: 8080 }` (numbers, booleans, strings, null, or literal arrays/objects). |
62
70
  | `strict: true` | Enforce the pure canonical subset: skip jzify lowering (so `var`/`function`/`class`/`==`/… are rejected, not accepted) **and** reject dynamic fallbacks (`obj[k]`, `for-in`, unknown receiver methods). Off by default — broader JS is lowered automatically. |
63
71
  | `alloc: false` | Omit allocator exports (`_alloc`/`_clear`) for standalone modules that never marshal heap values. |
72
+ | `noSimd: true` | Disable auto-vectorization (no jz-emitted `v128`) for engines without the SIMD proposal. Explicit `f32x4`/`i32x4` intrinsics still compile. |
73
+ | `whyNotSimd: true` | Diagnostic (CLI `--why-not-simd`): emit a `simd-why-not` warning per loop the auto-vectorizer declined, naming the first blocking op — finds loops one op away from SIMD. Noisy; off by default. Surfaced via the `warnings` sink. |
74
+ | `experimentalStencil: true` | Opt-in (CLI `--experimental-stencil`): vectorize neighbour-load stencils — `b[i] = f(a[i-1], a[i], a[i+1])` and 2-D 5-point sweeps — to f64x2. Bit-exact vs scalar (a lane-parallel map reorders nothing within a lane). Unstable / off by default until proven across the corpus. |
75
+ | `experimentalOuterStrip: true` | Opt-in (CLI `--experimental-outer-strip`): strip-mine a pixel loop whose per-pixel value is an inner reduction (e.g. metaballs' field sum over blobs) into f64x2 — two adjacent pixels per step. Bit-exact vs scalar (each lane accumulates in scalar order). Unstable / off by default. |
64
76
  | `randomSeed` | `Math.random` seeding — default draws from host entropy (non-reproducible); a number fixes it for a reproducible sequence, `true` forces entropy explicitly. |
65
77
  | `wat: true` | `compile()` returns WAT text instead of WASM binary. |
66
- | `profile` | Mutable sink for compile-stage timings; set `profile.names = true` for a WASM `name` section. |
78
+ | `names: true` | Emit a WASM `name` section (function symbols) for profilers/debuggers. |
79
+ | `profile` | Mutable sink for compile-stage timings (`entries`/`totals` per phase). |
67
80
  </details>
68
81
 
69
82
  ## CLI
@@ -74,7 +87,7 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
74
87
  jz program.js # → program.wasm
75
88
  jz program.js --wat # → program.wat
76
89
  jz program.js -o out.wasm # custom output (- for stdout)
77
- jz program.js -O3 # optimization: -O0 off, -O1 size, -O2 balanced, -O3 speed
90
+ jz program.js -O3 # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
78
91
  jz program.js --host wasi # standalone WASI output
79
92
  jz --strict program.js # pure canonical subset (also implied by .jz extension)
80
93
  jz -e "1 + 2" # eval → 3
@@ -84,7 +97,7 @@ jz -e "1 + 2" # eval → 3
84
97
  <summary><code>jz --help</code></summary>
85
98
 
86
99
  ```
87
- jz v0.5.1 - min JS → WASM compiler
100
+ jz - min JS → WASM compiler
88
101
 
89
102
  Usage:
90
103
  jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
@@ -98,8 +111,10 @@ Examples:
98
111
  jz program.js --wat # → program.wat
99
112
  jz program.js -o out.wasm # custom output name
100
113
  jz program.js -o - # write to stdout
101
- jz program.js -O3 # aggressive optimization
114
+ jz program.js -O3 # optimize for speed
102
115
  jz program.js -Os # optimize for size
116
+ jz program.js -D DEBUG=false # inject a compile-time constant
117
+ jz program.js --memory 64 # 64 initial pages (4 MB)
103
118
  jz program.js --host wasi # emit WASI Preview 1 imports
104
119
  jz --strict program.js # strict mode
105
120
  jz --jzify lib.js # → lib.jz
@@ -107,11 +122,22 @@ Examples:
107
122
 
108
123
  Options:
109
124
  --output, -o <file> Output file (.wat, .wasm, or - for stdout)
110
- -O<n>, --optimize <n> Optimization level: 0 off, 1 size-only, 2 default,
111
- 3 aggressive. Aliases: -Os/size, -Ob/balanced, -Of/speed.
125
+ -O<n>, --optimize <n> Optimization level: 0 off, 1 minimal, 2 default (all
126
+ stable passes), 3 speed. -Os optimizes for size.
127
+ --define, -D <K=V> Inject a compile-time constant (VALUE parsed as JSON,
128
+ else string). Repeatable.
112
129
  --host <js|wasi> Runtime-service lowering (default js)
130
+ --memory <pages> Initial memory size in 64 KiB pages
131
+ --max-memory <pages> Cap memory growth at this many pages (default unbounded)
132
+ --import-memory Import env.memory instead of exporting own memory
113
133
  --no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
134
+ --no-simd Disable auto-vectorization (no v128) for non-SIMD engines
135
+ --why-not-simd Report, per loop, why the auto-vectorizer declined it
136
+ --experimental-stencil Vectorize neighbour-load stencils (a[i±1]); opt-in
137
+ --experimental-outer-strip Strip-mine pixel loops over an inner reduction to f64x2; opt-in
138
+ --no-tail-call Use ordinary call frames instead of return_call
114
139
  --names Emit wasm name section for profilers/debuggers
140
+ --stats Print compile-phase timings to stderr
115
141
  --strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
116
142
  --jzify Transform JS to jz source (no compilation)
117
143
  --eval, -e Evaluate expression or file
@@ -125,7 +151,7 @@ Options:
125
151
 
126
152
  ## Language
127
153
 
128
- jz is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
154
+ JZ is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
129
155
 
130
156
  ```
131
157
  ┌────────────────────────────────────────────────────────────────────────┐
@@ -159,10 +185,12 @@ Not supported
159
185
  <details>
160
186
  <summary><strong>What are the differences with JS?</strong></summary>
161
187
 
188
+ Each follows one rule: **JZ takes WASM/native conventions over JS edge-cases when it's free and the f64 value-precision of real computation is preserved** ([rationale](CONTRIBUTING.md#principles)).
189
+
162
190
  - **Numbers are f64**; integer-proven values (loop counters, array idx, `| 0`) are `i32` and **wrap at ±2³¹**.
163
191
  - **Strings are UTF-8 bytes** — `.length`, `charCodeAt`, indexing, `slice`, `indexOf`, regex count bytes (`"中".length` is `3`); `toUpperCase`/`toLowerCase`/`trim` are ASCII-only. UTF-8 skips UTF-16's 2× and a multi-KB Unicode case table.
164
192
  - **Objects are fixed-shape structs** — literal keys sit in fixed slots; computed writes (`o[k] = v`) fall back to a per-object hash and enumerate normally, but a dot-key added after the literal (`o.b = 2`) stays readable without enumerating (`Object.keys`/`for…in`). Prefer `Map` for heavy dynamic keys.
165
- - **Typed arrays are fixed-size** — `arr.length = n` won't compile, and out-of-bounds reads give `0`.
193
+ - **Array indices are integers, typed-array access is unchecked** — an index coerces to `i32` (asm.js-style), so a fractional or `NaN` index *truncates* (`a[1.5]`→`a[1]`, `a[NaN]`→`a[0]`) rather than yielding JS's `undefined`. A `Float64Array`/etc. is fixed-size (`arr.length = n` won't compile) and read **raw**: an out-of-bounds or negative index reads arbitrary linear memory (a large one traps), not `undefined` — pass valid in-bounds integers. Plain `[]` arrays *are* bounds-checked (`undefined` past the end / for a negative index).
166
194
  - **No GC** — call `memory.reset()` between batches; `WeakMap`/`WeakSet` wired to `Map`/`Set`.
167
195
  - **`String(number)` keeps ~9 significant digits** (`String(Math.PI)` → `"3.14159265"`), so it may not round-trip; `NaN`/`Infinity`/integers are exact. Exact shortest-form needs a multi-KB Ryū/Grisu formatter.
168
196
  - **Errors are just their message** — a caught error is the value you threw (no `.message`, not `instanceof Error`), and `null.x` yields `undefined` instead of throwing. It keeps `throw` and member reads free of object machinery and per-access checks.
@@ -174,12 +202,12 @@ Not supported
174
202
  <details>
175
203
  <summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
176
204
 
177
- Only the ones that fit the jz subset. There's no runtime, so packages touching the DOM, `async`/`Promise`, the network, or Node APIs won't compile — but pure numeric/algorithmic source does.
205
+ Only the ones that fit the JZ subset. There's no runtime, so packages touching the DOM, `async`/`Promise`, the network, or Node APIs won't compile — but pure numeric/algorithmic source does.
178
206
 
179
207
  - **Relative imports** (`./dep.js`) bundle at compile time.
180
- - **Bare specifiers** (`import { x } from "pkg"`) resolve through Node module resolution only with the `--resolve` CLI flag, or by passing the source yourself via `{ modules }`. The package's source still has to be valid jz.
208
+ - **Bare specifiers** (`import { x } from "pkg"`) resolve through Node module resolution only with the `--resolve` CLI flag, or by passing the source yourself via `{ modules }`. The package's source still has to be valid JZ.
181
209
 
182
- jz is for compiling *your* numeric/DSP/parser code, not for running the npm ecosystem.
210
+ JZ is for compiling *your* numeric/DSP/parser code, not for running the npm ecosystem.
183
211
 
184
212
  </details>
185
213
 
@@ -223,7 +251,7 @@ jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
223
251
  <details>
224
252
  <summary><strong>Can I interpolate values (template literals)?</strong></summary>
225
253
 
226
- `jz` is a tagged template — interpolated values are baked into the source at compile time. Numbers and booleans inline directly; strings, arrays, and objects compile as jz literals:
254
+ `jz` is a tagged template — interpolated values are baked into the source at compile time. Numbers and booleans inline directly; strings, arrays, and objects compile as JZ literals:
227
255
 
228
256
  ```js
229
257
  jz`export let f = () => ${'hello'}.length` // 5
@@ -241,7 +269,7 @@ Interpolated functions become host calls. Non-serializable values (host objects,
241
269
  <details>
242
270
  <summary><strong>How to pass numbers, strings, arrays, objects JS ↔ WASM?</strong></summary>
243
271
 
244
- **Numbers cross natively** as `f64`/`i32`. **Heap values** — strings, arrays, objects, typed arrays — cross as NaN-boxed `f64` pointers into linear memory, allocated through the module's `_alloc`/`_clear` exports. That pointer-plus-allocator convention *is* the whole ABI (a few hundred bytes, documented in [`layout.js`](layout.js) with a worked example in [`test/abi.js`](test/abi.js)). The one shortcut: arrays of ≤ 8 elements come back as plain JS arrays via WASM multi-value.
272
+ **Numbers cross natively** as `f64`/`i32`. **Heap values** — strings, arrays, objects, typed arrays — plus the `null`/`undefined`/boolean atoms cross as the **i64 NaN-box carrier** (a `BigInt` on the JS side) holding a tagged pointer into linear memory, allocated through the module's `_alloc`/`_clear` exports. i64 rather than f64 so the NaN payload survives JSC/Safari, which canonicalizes f64 NaN bits at the boundary; numbers stay f64 (free). That carrier-plus-allocator convention *is* the whole ABI (a few hundred bytes, documented in [`layout.js`](layout.js) with a worked example in [`test/abi.js`](test/abi.js)); a per-export [`jz:i64exp`](interop.js) custom section maps which params/results ride i64, and the signature itself is self-describing for non-JS hosts. The one shortcut: arrays of ≤ 8 numeric elements come back as plain JS arrays via WASM multi-value.
245
273
 
246
274
  The `memory` codec — returned by `jz()` and by `jz/interop`'s `instantiate()` — handles both directions: it marshals arguments in, decodes pointer returns out, and turns a wasm `throw` into a real `Error`:
247
275
 
@@ -267,15 +295,15 @@ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2
267
295
  </details>
268
296
 
269
297
  <details>
270
- <summary><strong>Do I need jz at runtime?</strong></summary>
298
+ <summary><strong>Do I need JZ at runtime?</strong></summary>
271
299
 
272
300
  The compiler runs at build time. At runtime you ship the `.wasm` and, at most, a small bridge — never the compiler, the parser, or a language runtime.
273
301
 
274
- - **Pure-number modules — nothing but the `.wasm`.** Instantiate with raw `WebAssembly`, zero jz dependency: `(await WebAssembly.instantiate(wasmBytes)).instance.exports.dist(3, 4)`. Compile with `{ alloc: false }` to drop the `_alloc`/`_clear` exports too.
302
+ - **Pure-number modules — nothing but the `.wasm`.** Instantiate with raw `WebAssembly`, zero JZ dependency: `(await WebAssembly.instantiate(wasmBytes)).instance.exports.dist(3, 4)`. Compile with `{ alloc: false }` to drop the `_alloc`/`_clear` exports too.
275
303
  - **Heap values (strings, arrays, objects) — the `.wasm` plus `jz/interop`.** `import { instantiate } from 'jz/interop'` adds a ~6 KB-gzipped bridge (no compiler, no parser) that builds the same `Module`+`Instance` you'd build by hand and wires the allocator and the `memory` codec from the previous question (plus WASI / `wasm:js-string` imports if the module uses them).
276
304
  - **No JavaScript host at all** — compile with `host: 'wasi'`; see the next question.
277
305
 
278
- For contrast, Rust (`wasm-bindgen`), Go (TinyGo), and C/Zig (Emscripten/WASI-libc) emit per-build generated glue and usually bundle a language runtime. jz keeps the ABI fixed and the optional bridge ~6 KB gzipped.
306
+ For contrast, Rust (`wasm-bindgen`), Go (TinyGo), and C/Zig (Emscripten/WASI-libc) emit per-build generated glue and usually bundle a language runtime. JZ keeps the ABI fixed and the optional bridge ~6 KB gzipped.
279
307
 
280
308
  </details>
281
309
 
@@ -285,7 +313,7 @@ For contrast, Rust (`wasm-bindgen`), Go (TinyGo), and C/Zig (Emscripten/WASI-lib
285
313
  There's two possible `host` targets:
286
314
 
287
315
  - **`js`** (default) — runs inside a JavaScript host (browser, Node, Deno, Bun). `jz()` and `jz/interop` wire the needed `env.*` services automatically (overridable via `opts.imports.env`), and you get full value marshaling across the boundary.
288
- - **`wasi`** — runs on a standalone WASM engine with no JavaScript (wasmtime, wasmer, deno run). jz emits WASI Preview 1, so the module needs no host shims — but there's no host-side marshaler, so heap values must be passed by hand.
316
+ - **`wasi`** — runs on a standalone WASM engine with no JavaScript (wasmtime, wasmer, deno run). JZ emits WASI Preview 1, so the module needs no host shims — but there's no host-side marshaler, so heap values must be passed by hand.
289
317
 
290
318
  Either way the `.wasm` carries at most one import namespace (none, `env`, or `wasi_snapshot_preview1`). The difference is only in how a few runtime services are serviced:
291
319
 
@@ -301,7 +329,7 @@ Either way the `.wasm` carries at most one import namespace (none, `env`, or `wa
301
329
  <details>
302
330
  <summary><strong>How does memory work?</strong></summary>
303
331
 
304
- jz uses a **bump allocator**: every heap value (string, array, object, typed array) bumps a single pointer forward — no free list, no GC. The heap starts at byte 1024 — the first 1 KB holds static data (string/array literals laid out from offset 0, plus the bump pointer itself at byte 1020 when memory is shared across threads). It grows the WASM memory automatically when full, and if the literals overflow that 1 KB the heap simply starts past them.
332
+ JZ uses a **bump allocator**: every heap value (string, array, object, typed array) bumps a single pointer forward — no free list, no GC. The heap starts at byte 1024 — the first 1 KB holds static data (string/array literals laid out from offset 0, plus the bump pointer itself at byte 1020 when memory is shared across threads). It grows the WASM memory automatically when full, and if the literals overflow that 1 KB the heap simply starts past them.
305
333
  Memory is never reclaimed implicitly — a long-running program that allocates per call grows without bound. Reset between independent batches:
306
334
 
307
335
  ```js
@@ -334,7 +362,7 @@ Pass an existing `WebAssembly.Memory` to wrap it: `jz.memory(new WebAssembly.Mem
334
362
  Each compiled module exposes two call surfaces:
335
363
 
336
364
  - **`.exports`** — the JS-wrapped surface: it marshals JS arguments into the heap and decodes pointer return values back to JS values (and turns a wasm `throw` into an `Error`). Use it by default — it's also how you hand a value from one module to another, as in the example above (the value is re-marshaled through the shared memory).
337
- - **`.instance.exports`** — the raw `WebAssembly.Instance` exports: numbers pass through untouched, and a pointer return comes back as a raw NaN-boxed handle. Decode it on the host with `memory.read(ptr)`. Don't pass a raw pointer back *in* as an argument, though — the JS↔wasm `f64` boundary canonicalizes its NaN payload and the pointer is lost; let `.exports` marshal across instead.
365
+ - **`.instance.exports`** — the raw `WebAssembly.Instance` exports: numbers pass through untouched, and a boxed return (string/array/object/atom) comes back as its raw i64 carrier (a `BigInt`). Decode it on the host with `memory.read(ptr)`, and pass it straight back *in* as an argument — the i64 carrier preserves the NaN payload across the boundary on every engine (including JSC/Safari), so no value is lost.
338
366
 
339
367
  </details>
340
368
 
@@ -345,9 +373,9 @@ No runtime, no GC — a module is your code plus a small bump allocator. The geo
345
373
 
346
374
  - **`optimize: 'size'`** — keeps every size pass, drops loop unrolling and SIMD.
347
375
  - **`alloc: false`** — omit the allocator for pure-numeric modules that never marshal heap values.
348
- - **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `profile.names`).
376
+ - **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `names: true`).
349
377
 
350
- Hand-written WAT is still ~3–8× smaller on tight kernels — jz carries generic allocator and stdlib helpers a specialist omits; closing that gap is ongoing. Size budgets are gated in CI alongside speed ([full table](bench/README.md)).
378
+ Hand-written WAT is still ~3–8× smaller on tight kernels — JZ carries generic allocator and stdlib helpers a specialist omits; closing that gap is ongoing. Size budgets are gated in CI alongside speed ([full table](bench/README.md)).
351
379
 
352
380
  </details>
353
381
 
@@ -355,7 +383,7 @@ Hand-written WAT is still ~3–8× smaller on tight kernels — jz carries gener
355
383
  <details>
356
384
  <summary><strong>Which optimizations are applied?</strong></summary>
357
385
 
358
- Ordinary JS is already fast — jz infers the right machine type for your numbers, so you write plain JS. What it does, all on at the default `optimize: 2` (each line is also the habit that triggers it):
386
+ Ordinary JS is already fast — JZ infers the right machine type for your numbers, so you write plain JS. What it does, all on at the default `optimize: 2` (each line is also the habit that triggers it):
359
387
 
360
388
  - **Type narrowing** — parameters/results pinned to `i32`/`f64`/bool/typed-array elements from their call sites, off the boxed path. A `Float64Array`/`Int32Array` is direct memory access; a plain `[]` works too, with a little more overhead.
361
389
  - **Escape analysis & arena rewind** — fixed-shape arrays/objects/typed-arrays become WASM locals; scratch a function doesn't return is freed on exit (no manual cleanup).
@@ -363,23 +391,23 @@ Ordinary JS is already fast — jz infers the right machine type for your number
363
391
  - **SIMD-128** — independent iterations (`a[i] = a[i]*2 + b[i]`) run several lanes at once: lane-pure maps, reductions (sum/product/min·max), conditional maps (`bitselect`), byte scans (`memchr` via `i8x16`). Loops that look back (`a[i-1]`) or carry a running total stay sequential.
364
392
  - **Smaller encoding** — tree-shaking, copy-propagation + dead-store elimination, local/string-pool reordering for 1-byte indices, pointer-call specialization, constant pooling; JS strings you only read aren't copied.
365
393
 
366
- Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny `env.*` imports, a constant `JSON.parse` folds to a literal, JS strings stay zero-copy. Levels `0`–`3` or `'size'`/`'balanced'`/`'speed'` (or a per-pass object): `'balanced'` (= `2`) is the default; `'speed'` trades size for inlined constants and larger buffers; `'size'` drops unrolling and SIMD.
394
+ Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny `env.*` imports, a constant `JSON.parse` folds to a literal, JS strings stay zero-copy. Levels `0`–`3` (default `2`), or the named presets `'speed'` (= `3`, trades size for inlined constants and larger buffers) and `'size'` (drops unrolling and SIMD for the smallest wasm).
367
395
 
368
396
  </details>
369
397
 
370
398
  <details>
371
399
  <summary><strong>How do I inspect or debug the output?</strong></summary>
372
400
 
373
- - **Semantics** — valid jz is valid JS: run the same source under Node and diff results (mind the [documented divergences](#faq)); `console.log` works inside compiled modules too.
374
- - **Codegen** — `jz program.js --wat` (API: `compile(src, { wat: true })`) shows the emitted WAT: grep `v128` to confirm a loop vectorized, `__dyn_get`/`__ext_call` to spot dynamic fallbacks inference couldn't narrow.
401
+ - **Semantics** — valid JZ is valid JS: run the same source under Node and diff results (mind the [documented divergences](#faq)); `console.log` works inside compiled modules too.
402
+ - **Codegen** — `jz program.js --wat` (API: `compile(src, { wat: true })`) shows the emitted WAT: grep `v128` to confirm a loop vectorized, `__dyn_get`/`__ext_call` to spot dynamic fallbacks inference couldn't narrow. `--why-not-simd` (API: `whyNotSimd: true`) goes further — for each loop the auto-vectorizer declined it reports the first blocking op (e.g. `i32.rem_s: no lane-pure SIMD mapping`), so you don't have to grep the WAT to find what's one op away.
375
403
  - **Dynamic fallbacks** — compile with `strict: true` to turn every fallback (`obj[k]`, `for-in`, unknown receiver method) into a compile error pointing at the site.
376
- - **Profiling** — `--names` (API: `profile.names = true`) emits a wasm `name` section so DevTools profilers and disassemblers show real function names; the `profile` option collects per-stage compile timings.
404
+ - **Profiling** — `--names` (API: `names: true`) emits a wasm `name` section so DevTools profilers and disassemblers show real function names; `--stats` (API: the `profile` sink) collects per-stage compile timings.
377
405
  - **Slow kernel checklist** — a stray float literal pins a counter to f64; a plain `[]` where a typed array would do; a loop-carried dependency (`a[i-1]`, running sum) blocks SIMD. The signals in *Why no type annotations?* below are the levers.
378
406
 
379
407
  </details>
380
408
 
381
409
  <details>
382
- <summary><strong>How does jz work?</strong></summary>
410
+ <summary><strong>How does JZ work?</strong></summary>
383
411
 
384
412
  A source string flows through six stages into wasm bytes — no IR leaves the process, the whole thing is one pass per `compile()`:
385
413
 
@@ -403,7 +431,7 @@ Each stage lives in its own place: parsing in [`subscript`](https://github.com/d
403
431
  <details>
404
432
  <summary><strong>Why no type annotations?</strong></summary>
405
433
 
406
- Because `let x: i32` isn't valid JS — annotations would break the promise that valid jz runs and tests as plain JS. So jz reads the types from signals you already write:
434
+ Because `let x: i32` isn't valid JS — annotations would break the promise that valid JZ runs and tests as plain JS. So JZ reads the types from signals you already write:
407
435
 
408
436
  ```js
409
437
  export let bits = (a, b) => a | b // i32 — a bitwise op pins both operands
@@ -416,7 +444,7 @@ Literals (`0` vs `0.5`), operators (`|` `<<` `&` ⇒ i32), and how a value is us
416
444
 
417
445
 
418
446
  <details>
419
- <summary><strong>Is jz production-ready?</strong></summary>
447
+ <summary><strong>Is JZ production-ready?</strong></summary>
420
448
 
421
449
  It's **experimental** (pre-1.0) — the supported subset and the wasm ABI may still change, so pin a version and re-test on upgrade. What's solid: every push runs the full test suite, the test262 conformance subset, the benchmark gate, and the self-host build in CI, so regressions surface immediately.
422
450
 
@@ -428,13 +456,15 @@ It's **experimental** (pre-1.0) — the supported subset and the wasm ABI may st
428
456
 
429
457
  Yes. The compiler is pure and synchronous (no I/O — you hand it the sources), so it runs anywhere JavaScript does — main thread, a Web Worker, or a build step — and compiling a kernel takes single-digit-to-tens of milliseconds, fast enough to do on the fly. The `.wasm` it produces is just a module: instantiate it in any WebAssembly host — browser main thread, Web/Service Worker, Node/Deno/Bun, or a standalone engine.
430
458
 
459
+ Because compiling is that cheap, WASM becomes a *live medium*, not just a build artifact: hot-swap a compute kernel without a reload, recompile user-supplied source on the fly, or treat compiling as part of scripting — not a deploy step.
460
+
431
461
  </details>
432
462
 
433
463
 
434
464
  <details>
435
- <summary><strong>Can jz compile itself?</strong></summary>
465
+ <summary><strong>Can JZ compile itself?</strong></summary>
436
466
 
437
- Yes — fully. jz compiles its own **entire** source to `dist/jz.wasm`: the whole pipeline (parse → jzify → prepare → compile → encode) runs inside WASM, taking a source string and returning wasm bytes with no host help. In other words, `dist/jz.wasm` is jz compiled by jz.
467
+ Yes — fully. JZ compiles its own **entire** source to `dist/jz.wasm`: the whole pipeline (parse → jzify → prepare → compile → encode) runs inside WASM, taking a source string and returning wasm bytes with no host help. In other words, `dist/jz.wasm` is JZ compiled by JZ.
438
468
 
439
469
  `npm run test:self` is the CI gate — it builds `dist/jz.wasm`, then round-trips real programs through the in-wasm compiler and runs their output, proving the wasm-hosted compiler produces working modules.
440
470
 
@@ -442,7 +472,7 @@ Yes — fully. jz compiles its own **entire** source to `dist/jz.wasm`: the whol
442
472
 
443
473
 
444
474
  <details>
445
- <summary><strong>Can I compile jz to C?</strong></summary>
475
+ <summary><strong>Can I compile JZ to C?</strong></summary>
446
476
 
447
477
  Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
448
478
 
@@ -453,58 +483,49 @@ wasm2c program.opt.wasm -o program.c
453
483
  cc -O3 program.c -o program
454
484
  ```
455
485
 
456
- The full native pipeline (jz → `wasm-opt -O3` → `wasm2c` → `clang -O3 -flto` + PGO) lowers to standalone native code that beats V8 on the watr example corpus (19/21 wins, 2 ties, M4 Max). Details and the regression gate live in [`scripts/native/README.md`](scripts/native/README.md).
486
+ The full native pipeline (JZ → `wasm-opt -O3` → `wasm2c` → `clang -O3 -flto` + PGO) lowers to standalone native code that beats V8 on the watr example corpus (19/21 wins, 2 ties, M4 Max). Details and the regression gate live in [`scripts/native/README.md`](scripts/native/README.md).
457
487
 
458
- [Static Hermes](https://github.com/facebook/hermes) reaches native the same way from the other end — full JS through C/LLVM, with sound type annotations for speed; jz keeps the source plain JS and gets its types by inference.
488
+ [Static Hermes](https://github.com/facebook/hermes) reaches native the same way from the other end — full JS through C/LLVM, with sound type annotations for speed; JZ keeps the source plain JS and gets its types by inference.
459
489
 
460
490
  </details>
461
491
 
462
492
 
463
493
 
464
494
 
465
-
466
-
467
495
  ## Performance
468
496
 
469
- Geomean speed across the [bench corpus →](bench/README.md).
470
-
471
- <img src="bench/bench.svg?v=2" alt="jz vs alternatives — geomean speed across the bench corpus" width="720">
497
+ <img src="bench/bench.svg?v=8" alt="JZ vs alternatives — geometric-mean speed across the bench corpus, every rival compiled to WebAssembly and run in V8 (apples-to-apples); native C is the lone reference, JZ = 1.00× baseline" width="720">
472
498
 
473
- <sub>Local snapshot (M4 Max, darwin/arm64). Bun/Zig/Rust/Go/NumPy rows are hand-run reference points.</sub>
474
499
 
500
+ See [bench →](https://dy.github.io/jz/bench/)
475
501
 
476
502
  ## Examples
477
503
 
478
504
  <table>
479
505
  <tr>
480
- <td width="33%"><a href="https://dy.github.io/jz/examples/game-of-life/"><img src="examples/thumbs/game-of-life.webp" width="100%" alt="Game of Life"></a><br><b>game-of-life</b> — Conway's Life straight into shared pixel memory.</td>
481
- <td width="33%"><a href="https://dy.github.io/jz/examples/lenia/"><img src="examples/thumbs/lenia.webp" width="100%" alt="Lenia"></a><br><b>lenia</b> — continuous cellular automaton; smooth-kernel "digital life".</td>
482
- <td width="33%"><a href="https://dy.github.io/jz/examples/diffusion/"><img src="examples/thumbs/diffusion.webp" width="100%" alt="Diffusion"></a><br><b>diffusion</b> — Gray-Scott; organic coral / labyrinths.</td>
506
+ <td width="33%"><a href="https://dy.github.io/jz/examples/chladni/"><img src="examples/thumbs/chladni.webp" width="100%" alt="Chladni plate"></a><br><b>chladni</b> — frequency sweeps the nodal figure.</td>
507
+ <td width="33%"><a href="https://dy.github.io/jz/examples/julia/"><img src="examples/thumbs/julia.webp" width="100%" alt="Julia set"></a><br><b>julia</b> — escape-time Julia set; drag the constant to morph it.</td>
508
+ <td width="33%"><a href="https://dy.github.io/jz/examples/attractors/"><img src="examples/thumbs/attractors.webp" width="100%" alt="Strange attractor"></a><br><b>attractors</b> — de Jong map, luminous curves.</td>
483
509
  </tr>
484
510
  <tr>
485
- <td><a href="https://dy.github.io/jz/examples/interference/"><img src="examples/thumbs/interference.webp" width="100%" alt="Wave interference"></a><br><b>interference</b> — two-source wave field, recomputed every frame.</td>
486
- <td><a href="https://dy.github.io/jz/examples/plasma/"><img src="examples/thumbs/plasma.webp" width="100%" alt="Plasma"></a><br><b>plasma</b> — FBM domain-warp; the classic flowing shader plasma.</td>
487
- <td><a href="https://dy.github.io/jz/examples/chladni/"><img src="examples/thumbs/chladni.webp" width="100%" alt="Chladni plate"></a><br><b>chladni</b> — Camerata-style plate; frequency sweeps the nodal figure.</td>
488
- </tr>
489
- <tr>
490
- <td><a href="https://dy.github.io/jz/examples/mandelbrot/"><img src="examples/thumbs/mandelbrot.webp" width="100%" alt="Mandelbrot set"></a><br><b>mandelbrot</b> — escape-time fractal with smooth coloring.</td>
491
- <td><a href="https://dy.github.io/jz/examples/attractors/"><img src="examples/thumbs/attractors.webp" width="100%" alt="Strange attractor"></a><br><b>attractors</b> — de Jong map, millions of iters → luminous curves.</td>
492
511
  <td><a href="https://dy.github.io/jz/examples/raymarcher/"><img src="examples/thumbs/raymarcher.webp" width="100%" alt="SDF raymarcher"></a><br><b>raymarcher</b> — an SDF sphere field; Shadertoy on the CPU.</td>
512
+ <td><a href="https://dy.github.io/jz/examples/nbody/"><img src="examples/thumbs/nbody.webp" width="100%" alt="N-body gravity"></a><br><b>nbody</b> — three-body gravity; fading trails trace the orbits.</td>
513
+ <td><a href="https://dy.github.io/jz/examples/game-of-life/"><img src="examples/thumbs/game-of-life.webp" width="100%" alt="Game of Life"></a><br><b>game-of-life</b> — Conway's Life straight into shared pixel memory.</td>
493
514
  </tr>
494
515
  <tr>
495
- <td><a href="https://dy.github.io/jz/examples/rfft/"><img src="examples/thumbs/rfft.webp" width="100%" alt="Live spectrogram"></a><br><b>rfft</b> — live log/mel spectrogram from a jz real FFT.</td>
496
- <td><a href="https://dy.github.io/jz/examples/zzfx/"><img src="examples/thumbs/zzfx.webp" width="100%" alt="ZzFX sound synth"></a><br><b>zzfx</b> — the unmodified <a href="https://github.com/KilledByAPixel/ZzFX">ZzFX</a> sfx synth, compiled as-is.</td>
497
- <td><a href="https://dy.github.io/jz/examples/jukebox/"><img src="examples/thumbs/jukebox.webp" width="100%" alt="Floatbeat jukebox"></a><br><b>jukebox</b> — looping procedural-jazz arpeggio floatbeat; tap to play/pause.</td>
516
+ <td><a href="https://dy.github.io/jz/examples/plasma/"><img src="examples/thumbs/plasma.webp" width="100%" alt="Plasma"></a><br><b>plasma</b> — FBM domain-warp; the classic flowing shader plasma.</td>
517
+ <td><a href="https://dy.github.io/jz/examples/cloth/"><img src="examples/thumbs/cloth.webp" width="100%" alt="Cloth simulation"></a><br><b>cloth</b> — Verlet mass-spring sheet; drag it, watch it settle.</td>
518
+ <td><a href="https://dy.github.io/jz/examples/erosion/"><img src="examples/thumbs/erosion.webp" width="100%" alt="Terrain erosion"></a><br><b>erosion</b> — hydraulic droplets carve a fractal terrain.</td>
498
519
  </tr>
499
520
  </table>
500
521
 
501
- [**Browse the gallery →**](https://dy.github.io/jz/examples/)
522
+ See [all examples ](https://dy.github.io/jz/examples/)
502
523
 
503
524
 
504
525
 
505
526
  ## Alternatives
506
527
 
507
- From small, fast JS subset to full JS spec, bundled engine:
528
+ Small & fast JS subset full JS spec & bundled engine:
508
529
 
509
530
  * [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) — TS-like dialect → WASM; small, fast output, but needs type annotations (not JS).
510
531
  * [awasm-compiler](https://github.com/paulmillr/awasm-compiler) — reproducible WASM assembled through a typed *builder API*.
@@ -517,8 +538,14 @@ From small, fast JS subset to full JS spec, bundled engine:
517
538
 
518
539
  ## Built with
519
540
 
520
- * [**subscript**](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST jz needs. Jessie subset keeps the grammar small and deterministic.
521
- * [**watr**](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into valid `.wasm`.
541
+ * [**subscript**](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST JZ needs. Jessie subset keeps the grammar small and deterministic.
542
+ * [**watr**](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Binary encoding, validation, and peephole optimization. JZ emits WAT text, watr turns it into valid `.wasm`.
543
+
544
+
545
+ ## Contributing
546
+
547
+ Setup, code layout, and the bench/perf invariants are in [CONTRIBUTING.md](CONTRIBUTING.md);
548
+ the architecture & design rationale (NaN-boxing, type inference, native pipeline) in [research.md](.work/research.md).
522
549
 
523
550
 
524
- <p align=center>MIT • <a href="https://github.com/krishnized/license/">ॐ</a></p>
551
+ <p align=center><a href="https://github.com/dy">dy</a> • <a href="https://github.com/krishnized/license/">ॐ</a></p>