jz 0.7.0 → 0.8.1

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 (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. package/src/wat/optimize.js +0 -3938
package/bench/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # jz benchmark suite
1
+ # JZ benchmark suite
2
2
 
3
- Cross-target workload suite for jz codegen quality. Each benchmark is a case
3
+ Cross-target workload suite for JZ codegen quality. Each benchmark is a case
4
4
  folder under `bench/`:
5
5
 
6
6
  ```txt
@@ -50,16 +50,32 @@ node bench/bench.mjs mat4 --targets=nat,v8,jz
50
50
  | [`json`](json/json.js) | runtime `JSON.parse` of one module-local source plus heterogeneous object/array walk with a stable inferred JSON shape |
51
51
  | [`sort`](sort/sort.js) | in-place heapsort over a typed array; exposes call-heavy nested loops and typed-array index propagation |
52
52
  | [`crc32`](crc32/crc32.js) | table-driven CRC-32 over a mutable byte buffer; exposes integer narrowing and typed-array parameter propagation |
53
- | [`dotprod`](dotprod/dotprod.js) | multiply-accumulate reduction; jz reassociates to 4 SIMD accumulators, beating strict-fp serial native |
53
+ | [`dotprod`](dotprod/dotprod.js) | multiply-accumulate reduction; JZ reassociates to 4 SIMD accumulators, beating strict-fp serial native |
54
54
  | [`bytebeat`](bytebeat/bytebeat.js) | classic integer "bytebeat" one-line-song synthesis to 8-bit PCM; pure i32, bit-exact everywhere |
55
55
  | [`fft`](fft/fft.js) | radix-2 Cooley–Tukey FFT over a transcendental-free twiddle table; the canonical numeric/audio kernel |
56
56
  | [`synth`](synth/synth.js) | minisynth audio pipeline — polynomial oscillator + ADSR + biquad per sample; loop-carried f64 |
57
57
  | [`blur`](blur/blur.js) | separable box blur on an RGBA8 image; integer stencil with edge clamp, the canonical image pipeline |
58
+ | [`conv2d`](conv2d/conv2d.js) | int8 quantized conv layer (Cin→Cout, 3×3, i32 MAC + ReLU requant); the spatial-convolution counterpart to dense `matmul`, the edge-ML inference kernel |
59
+ | [`qoi`](qoi/qoi.js) | QOI lossless image codec encode + decode round-trip; loop-carried run/index/diff/luma ops that no target can vectorize — pure scalar-codegen race |
60
+ | [`hash`](hash/hash.js) | MurmurHash3 x86_32 over a byte buffer; table-free multiply/rotate/xor mixing, a different ALU profile from `crc32` |
61
+ | [`lz`](lz/lz.js) | LZSS compress + inflate round-trip; greedy sliding-window longest-match search — the match finder that dominates every LZ-family codec |
62
+ | [`base64`](base64/base64.js) | Base64 encode + decode round-trip; table-driven 3→4 byte packing, the canonical codec / serialization kernel |
63
+ | [`wav`](wav/wav.js) | PCM-16 WAV encoder; per-sample clamp + int16 quantization behind a RIFF/WAVE header, the audio serialization kernel |
64
+ | [`raytrace`](raytrace/raytrace.js) | minimal sphere ray tracer — primary ray + closest-hit + Lambert/ambient shading to an f64 framebuffer; the canonical 3-D render kernel, a branchy loop-carried scalar `sqrt` pipeline no target vectorizes |
65
+ | [`noise`](noise/noise.js) | 2-D Perlin gradient noise summed over 5 fBm octaves; the canonical procedural-generation kernel — integer permutation-table hashing interleaved with f64 smoothstep interpolation |
66
+ | [`radixsort`](radixsort/radixsort.js) | LSD radix sort (4 × 8-bit counting passes) over u32 keys; histogram → prefix-sum → scatter with buffer ping-pong, the gather/scatter integer-sort counterpart to compare-swap `sort` |
67
+ | [`levenshtein`](levenshtein/levenshtein.js) | Levenshtein edit-distance rolling-row dynamic program; the canonical sequence-alignment / fuzzy-match kernel — a branch- and min-reduction-heavy DP with a diagonal data dependency no target vectorizes |
68
+ | [`nqueens`](nqueens/nqueens.js) | bitmask N-Queens solution counter (board sizes drawn from a runtime array so the recursion can't be constant-folded); deep backtracking recursion + per-node bitmask branch — the call/recursion-codegen probe |
69
+ | [`dict`](dict/dict.js) | open-addressing hash table (build + probe) with linear probing; multiply-shift hash scatter + dependent-load gather + unpredictable probe-chain branches — the associative-container kernel |
70
+ | [`sieve`](sieve/sieve.js) | Sieve of Eratosthenes over a byte array; pure strided scatter (j = i², i²+i, …) guarded by an outer branch — a non-contiguous write pattern |
71
+ | [`vm`](vm/vm.js) | tiny bytecode interpreter — a fetch-decode-dispatch loop (if/else opcode chain + indirect code-array loads) running an integer recurrence; the canonical VM/regex-engine inner loop |
72
+ | [`spmv`](spmv/spmv.js) | sparse matrix×vector in CSR form; the MAC inner loop gathers `x[col[k]]` through a column-index array — the data-dependent gather dense codegen handles worst (exact-integer f64, bit-identical everywhere) |
73
+ | [`hashjoin`](hashjoin/hashjoin.js) | probe-dominated relational hash join (build small, stream a large probe side, sum matched payloads) — the database/dataframe kernel and JZ's boss case: `probe()` returns a typed-array element through a param JZ can't yet prove is i32, so `sum + probe()` lowers to a polymorphic number-or-string add per probe — the only case JZ trails V8; proving the return i32 measured 8.7 → 5.2 ms, jz then beats V8 1.3× |
58
74
  | [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
59
75
  | [`jessie`](jessie/jessie.js) | the subscript/jessie JS parser over a realistic source corpus; branch-, allocation- and recursion-heavy front-end work |
60
- | [`jz`](jz/jz.js) | the jz compiler itself (scripts/self.js pipeline) compiling three small programs at L2 — the self-host row runs jz.wasm compiling JavaScript; output bytes are checksummed so the parity gate doubles as a determinism proof |
76
+ | [`jz`](jz/jz.js) | the JZ compiler itself (scripts/self.js pipeline) compiling three small programs at L2 — the self-host row runs jz.wasm compiling JavaScript; output bytes are checksummed so the parity gate doubles as a determinism proof |
61
77
 
62
- The `watr`, `jessie`, and `jz` rows are self-referential — jz (or its deps)
78
+ The `watr`, `jessie`, and `jz` rows are self-referential — JZ (or its deps)
63
79
  compiling code. They stay runnable (`--cases=watr,jessie,jz`) and gated in
64
80
  `test/bench.js`, but are hidden from the bench page and the headline geomean SVG:
65
81
  they answer a different question (compiler throughput on itself) from the
@@ -69,21 +85,45 @@ Native rows for `json` are fixed-source references, not semantic equivalents
69
85
  of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
70
86
  compile-time string, and Zig may constant-fold the whole parse+walk under
71
87
  ReleaseFast; Go uses `encoding/json` but still unmarshals the same compile-time
72
- string. The jz row parses a `let` source at runtime so `JSON.parse` is not
88
+ string. The JZ row parses a `let` source at runtime so `JSON.parse` is not
73
89
  compile-time folded, while the compiler can still specialize the stable literal
74
90
  shape. External unknown-shape JSON still uses the generic runtime parser.
75
91
 
76
92
  Native-language rows are intentionally per case. NumPy rows are used only
77
93
  where a vectorized array implementation is a meaningful Python convention.
78
94
 
79
- Pure interpreters (CPython, QuickJS, Hermes, Javy's embedded-QuickJS) are not
80
- benchmarked: this suite measures compiled-code quality, and an interpreter row
81
- inflates jz's lead for free without answering a question a user actually has.
82
- The headline field is wasm-vs-wasm, apples-to-apples: jz against the other
83
- languages compiled to the same target WebAssembly run in V8 i.e. Rust, Go,
84
- and C (`wasm32-wasi`), AssemblyScript, and Porffor, plus the JS JITs it replaces
85
- (V8/Bun/Deno/SpiderMonkey/GraalJS). Native C/Rust/Zig/Go and NumPy's vectorized
86
- C are kept only as a speed-of-light reference, never presented as the wasm peer.
95
+ Pure interpreters (CPython, QuickJS, Hermes) stay out of the **headline**
96
+ geomean: it measures compiled-code quality, and an interpreter row inflates JZ's
97
+ lead for free without answering a question a user actually has. Javy
98
+ (embedded-QuickJS) is wired as a **fenced reference** it appears per case as a
99
+ hatched row so the cost of the "ship a JS interpreter in wasm" approach is
100
+ visible, but it is excluded from `SVG_TARGETS` so it never moves the headline
101
+ number. The headline field is wasm-vs-wasm, apples-to-apples: JZ against the
102
+ other languages compiled to the same target WebAssembly run in V8 i.e. Rust,
103
+ Go, C, and Zig (`wasm32-wasi`), AssemblyScript, Porffor, and MoonBit (the last on
104
+ moonrun, MoonBit's V8 wasm runner), plus the JS JITs it replaces
105
+ (V8/Bun/Deno/SpiderMonkey/GraalJS). Native C/Rust/Zig/Go and NumPy's
106
+ vectorized C are the corpus reference band — the aggregate "what you give up by
107
+ not rewriting", with native C the speed-of-light ceiling.
108
+
109
+ **Coverage is reported, not hidden.** A target that is present but cannot compile
110
+ or run a case (Porffor OOMs on most typed-array kernels; TinyGo's stdlib subset
111
+ rejects some `.go`) records a `{ status: 'fail', reason }` entry in
112
+ `results.json`, and the page renders it as a muted row with the reason — and as a
113
+ `ran / attempted` count on the headline row. The geomean still averages only the
114
+ cases a target completed correctly (you cannot ratio a run that never produced a
115
+ number), but the gap is no longer invisible.
116
+
117
+ The **per-case** view splits into two **same-class lanes**, so every bar in a
118
+ card is a fair peer:
119
+
120
+ - **WASM** — JZ vs Rust/Go/C/Zig→wasm, AssemblyScript, Porffor, and raw JS, all
121
+ run in this V8.
122
+ - **Native** — JZ lowered to a native binary (`jz-w2c`: JZ wasm → `wasm2c` →
123
+ `clang -O3`) against the native toolchains (C/Rust/Go/Zig). This is the *fair*
124
+ native comparison — native-vs-native, not jz-wasm against a native binary — and
125
+ it's the axis for optimizing JZ's compile-to-native path. The lane hides where
126
+ no `jz-w2c` build is available (it needs wabt's wasm2c runtime + SIMDe headers).
87
127
 
88
128
  ### Parity classes
89
129
 
@@ -108,28 +148,32 @@ correctly-rounded; cascade is the same algorithm.
108
148
  | `v8` | raw JavaScript on Node/V8 |
109
149
  | `deno` | raw JavaScript on Deno/V8 |
110
150
  | `bun` | raw JavaScript on Bun/JavaScriptCore |
151
+ | `jsc` | raw JavaScript on the standalone JavaScriptCore shell — Safari's engine (`jsc`; install via `jsvu --engines=javascriptcore`, or set `JSC_BIN`) |
111
152
  | `spidermonkey` | raw JavaScript on SpiderMonkey shell (`spidermonkey`, `sm`, `js128`, `js115`, `js102`, or `js`) |
112
153
  | `shermes` | Static Hermes — JS AOT-compiled to a native binary (`shermes -O`), when installed |
113
154
  | `graaljs` | raw JavaScript on GraalJS |
114
- | `jz` | jz output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
155
+ | `jz` | JZ output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
115
156
  | `as` | AssemblyScript `asc -O3 --runtime stub`, when a matching `.as.ts` exists |
116
157
  | `rust-wasm` | Rust → `wasm32-wasip1` (`rustc --target wasm32-wasip1 -C opt-level=3`), run in node's V8 — the apples-to-apples Rust rival |
117
158
  | `go-wasm` | Go → `wasm32-wasip1` (`GOOS=wasip1 GOARCH=wasm go build`), run in node's V8 |
118
- | `c-wasm` | C → `wasm32-wasi` (`zig cc -target wasm32-wasi -O3`, no emcc/wasi-sdk needed), run in node's V8 |
119
- | `jz-wasmtime` | jz output on wasmtime |
120
- | `jz-w2c` | jz wasm translated by wabt `wasm2c`, then clang `-O3` |
159
+ | `c-wasm` | C → `wasm32-wasi` via clang/LLVM (`zig cc -target wasm32-wasi -O3` — zig supplies the wasi-libc that plain clang lacks; no emcc/wasi-sdk install), run in node's V8 |
160
+ | `jz-wasmtime` | JZ output on wasmtime |
161
+ | `jz-w2c` | JZ wasm translated by wabt `wasm2c`, then clang `-O3` |
121
162
  | `wat` | hand-written WAT baseline when a case provides `run-wat.mjs` |
122
163
  | `porf` | Porffor (`porf run`) when installed |
123
- | `jawsm` | jawsm when installed |
164
+ | `jawsm` | jawsm (JS → WasmGC) when installed |
165
+ | `javy` | Javy (`javy build` — JS in embedded QuickJS) when installed — fenced interpreter reference, never in the headline geomean |
166
+ | `tinygo` | TinyGo → `wasm32-wasip1` (`tinygo build -target=wasip1 -opt=2`) — the Go corpus through LLVM, leaner wasm than `go-wasm`; run in node's V8 |
167
+ | `moonbit` | MoonBit → `wasm` (`moon build --target wasm --release`), run on `moonrun` (MoonBit's V8 wasm runner, which supplies the monotonic clock) — a wasm-first-language rival, when `moon`/`moonrun` are installed |
124
168
 
125
169
  The `size` column reports the artifact size each target measures: the
126
170
  compiled native binary for `nat`/`rust`/`go`/`zig`, the produced
127
171
  `.wasm` for `jz`/`as`/`rust-wasm`/`go-wasm`/`c-wasm`/hand-WAT/jawsm/`jz-w2c` (the C-translated
128
172
  executable), or the source file for raw-JS interpreters where there is no
129
173
  compile step. For source files with imports, raw-JS size is only the entry file;
130
- jz size is the bundled wasm artifact.
174
+ JZ size is the bundled wasm artifact.
131
175
 
132
- Note the preset trade: these speed tables build jz at the default
176
+ Note the preset trade: these speed tables build JZ at the default
133
177
  (`optimize: 2`, speed-leaning — loop unroll + SIMD lift), while the dedicated
134
178
  size comparison (`npm run bench:size`) builds with `optimize: 'size'` —
135
179
  typically ~2× smaller on the same kernel (biquad: 3.5 kB default vs 1.6 kB
@@ -138,10 +182,10 @@ same artifact.
138
182
 
139
183
  Runtime command overrides:
140
184
 
141
- `watr` is intentionally compiled by jz with a size-oriented pass config
185
+ `watr` is intentionally compiled by JZ with a size-oriented pass config
142
186
  (`watr:false`, `smallConstForUnroll:false`): on a large compiler bundle, the
143
187
  default WAT-level optimizer and small-loop unroll grow code more than they help.
144
- This keeps the target measuring the best current jz artifact for that workload.
188
+ This keeps the target measuring the best current JZ artifact for that workload.
145
189
 
146
190
  ```sh
147
191
  BUN_BIN=/path/to/bun \
@@ -155,22 +199,23 @@ node bench/bench.mjs --targets=bun,deno,spidermonkey,shermes,graaljs,porf
155
199
 
156
200
  ## Reading the numbers (darwin/arm64, M-class)
157
201
 
158
- Snapshots from `node bench/bench.mjs --targets=v8,jz,as`.
159
- Where jz lands relative to **V8 raw JS** (`v8/node`) and **AssemblyScript**
160
- (`as`) is the headline comparison. Native and hand-WAT rows are shown where
161
- available from earlier runs.
202
+ Snapshots from `node bench/bench.mjs --targets=v8,jz,as` (the WASM lane).
203
+ Where JZ lands relative to **V8 raw JS** (`v8/node`) and **AssemblyScript**
204
+ (`as`) is the headline comparison; the hand-WAT row is the wasm-in-V8 floor.
205
+ The fair native comparison lives in the page's separate **Native lane** (`jz-w2c`
206
+ vs the native toolchains), not in these wasm snapshots — a native binary against
207
+ wasm-in-V8 would be a wrong-class single-case compare.
162
208
 
163
209
  ### biquad — f64 typed-array DSP cascade
164
210
 
165
211
  | target | median | ×v8 | size | parity |
166
212
  | --- | ---: | ---: | ---: | --- |
167
- | **jz → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
213
+ | **JZ → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
168
214
  | AssemblyScript (asc -O3 --runtime stub) | 6.51 ms | 1.38× | 1.9 kB | ok |
169
215
  | V8 (node) raw JS | 8.98 ms | 1.00× | 3.2 kB | ok |
170
- | native C (clang -O3) | 3.86 ms | 2.32× | 32.7 kB | ok |
171
216
  | hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
172
217
 
173
- jz beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
218
+ JZ beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
174
219
  offset-fusion, and base-hoisting pipeline delivers dense-f64 loop codegen
175
220
  that matches the hand-WAT floor.
176
221
 
@@ -178,13 +223,12 @@ that matches the hand-WAT floor.
178
223
 
179
224
  | target | median | ×v8 | size | parity |
180
225
  | --- | ---: | ---: | ---: | --- |
181
- | **jz → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
226
+ | **JZ → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
182
227
  | AssemblyScript (asc -O3 --runtime stub) | 6.71 ms | 1.28× | 1.6 kB | ok |
183
228
  | V8 (node) raw JS | 8.57 ms | 1.00× | 1.2 kB | ok |
184
- | native C (clang -O3) | 1.97 ms | 4.35× | 32.8 kB | ok |
185
229
  | hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
186
230
 
187
- jz is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
231
+ JZ is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
188
232
  SIMD hot path (unrolled 4×4 multiply) is the win; V8's JIT doesn't vectorize
189
233
  this from JS source.
190
234
 
@@ -192,11 +236,11 @@ this from JS source.
192
236
 
193
237
  | target | median | ×v8 | size | parity |
194
238
  | --- | ---: | ---: | ---: | --- |
195
- | **jz → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
239
+ | **JZ → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
196
240
  | AssemblyScript (asc -O3 --runtime stub) | 0.81 ms | 2.07× | 1.3 kB | ok |
197
241
  | V8 (node) raw JS | 1.67 ms | 1.00× | 1014 B | ok |
198
242
 
199
- jz is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
243
+ JZ is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
200
244
  `sum` (called with both `Float64Array` and `Int32Array`) stays on typed
201
245
  paths without falling back to generic dispatch.
202
246
 
@@ -204,13 +248,12 @@ paths without falling back to generic dispatch.
204
248
 
205
249
  | target | median | ×v8 | size | parity |
206
250
  | --- | ---: | ---: | ---: | --- |
207
- | **jz → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
251
+ | **JZ → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
208
252
  | V8 (node) raw JS | 3.87 ms | 1.00× | 1005 B | ok |
209
253
  | AssemblyScript (asc -O3 --runtime stub) | 9.15 ms | 0.42× | 1.5 kB | ok |
210
- | native C (clang -O3) | 0.95 ms | 4.08× | 32.9 kB | ok |
211
254
  | hand-WAT → V8 wasm | 3.59 ms | 1.11× | 355 B | ok |
212
255
 
213
- jz is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
256
+ JZ is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
214
257
  (`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
215
258
  overhead on every operation.
216
259
 
@@ -218,22 +261,22 @@ overhead on every operation.
218
261
 
219
262
  | target | median | ×v8 | size | parity |
220
263
  | --- | ---: | ---: | ---: | --- |
221
- | **jz → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
264
+ | **JZ → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
222
265
  | AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.15× | 1.6 kB | ok |
223
266
  | V8 (node) raw JS | 0.13 ms | 1.00× | 2.0 kB | ok |
224
267
 
225
- jz is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
268
+ JZ is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
226
269
  `charCodeAt`-heavy scan. Both are well ahead of V8.
227
270
 
228
271
  ### callback — `Array.map` closure + i32 fold
229
272
 
230
273
  | target | median | ×v8 | size | parity |
231
274
  | --- | ---: | ---: | ---: | --- |
232
- | **jz → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
275
+ | **JZ → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
233
276
  | V8 (node) raw JS | 0.62 ms | 1.00× | 1.3 kB | ok |
234
277
  | AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.78× | 1.9 kB | ok |
235
278
 
236
- jz is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
279
+ JZ is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
237
280
  `Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
238
281
  V8's JIT does not inline the closure across the `map` boundary.
239
282
 
@@ -241,11 +284,11 @@ V8's JIT does not inline the closure across the `map` boundary.
241
284
 
242
285
  | target | median | ×v8 | size | parity |
243
286
  | --- | ---: | ---: | ---: | --- |
244
- | **jz → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
287
+ | **JZ → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
245
288
  | V8 (node) raw JS | 1.33 ms | 1.00× | 1.1 kB | ok |
246
289
  | AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.95× | 2.1 kB | ok |
247
290
 
248
- jz is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
291
+ JZ is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
249
292
  reads are direct field offsets; the gap is small because the workload is
250
293
  memory-bound.
251
294
 
@@ -254,42 +297,42 @@ memory-bound.
254
297
  | target | median | ×v8 | size | parity |
255
298
  | --- | ---: | ---: | ---: | --- |
256
299
  | AssemblyScript (asc -O3 --runtime stub) | 8.90 ms | 1.11× | 1.3 kB | ok |
257
- | **jz → V8 wasm** | **8.45 ms** | **1.17×** | **1.4 kB** | **ok** |
300
+ | **JZ → V8 wasm** | **8.45 ms** | **1.17×** | **1.4 kB** | **ok** |
258
301
  | V8 (node) raw JS | 9.90 ms | 1.00× | 1.8 kB | ok |
259
302
 
260
- jz is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
303
+ JZ is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
261
304
  conditional break compacts to 1.0 kB — the smallest wasm in the suite.
262
305
 
263
306
  ### json — runtime `JSON.parse` plus stable-shape walk
264
307
 
265
308
  | target | median | ×v8 | size | parity |
266
309
  | --- | ---: | ---: | ---: | --- |
267
- | **jz → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
310
+ | **JZ → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
268
311
  | V8 (node) raw JS | 0.27 ms | 1.00× | 1.2 kB | ok |
269
312
 
270
- jz is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
313
+ JZ is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
271
314
  inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
272
315
 
273
316
  ### sort — in-place heapsort over typed array
274
317
 
275
318
  | target | median | ×v8 | size | parity |
276
319
  | --- | ---: | ---: | ---: | --- |
277
- | **jz → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
320
+ | **JZ → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
278
321
  | AssemblyScript (asc -O3 --runtime stub) | 7.96 ms | 0.98× | 1.9 kB | ok |
279
322
  | V8 (node) raw JS | 7.79 ms | 1.00× | 1.6 kB | ok |
280
323
 
281
- jz is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
324
+ JZ is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
282
325
  nested loops with typed-array index propagation stay on the i32 path.
283
326
 
284
327
  ### crc32 — table-driven CRC-32 over byte buffer
285
328
 
286
329
  | target | median | ×v8 | size | parity |
287
330
  | --- | ---: | ---: | ---: | --- |
288
- | **jz → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
331
+ | **JZ → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
289
332
  | AssemblyScript (asc -O3 --runtime stub) | 8.76 ms | 1.11× | 1.4 kB | ok |
290
333
  | V8 (node) raw JS | 9.71 ms | 1.00× | 1.8 kB | ok |
291
334
 
292
- jz is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
335
+ JZ is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
293
336
  typed-array parameter propagation keep the LUT lookup on raw i32.
294
337
 
295
338
  ### Audio + image showcase (cross-language, bit-exact)
@@ -300,70 +343,130 @@ math (bytebeat, blur) or transcendental-free f64 (fft, synth: in-source Taylor
300
343
  polynomials, not `Math.sin`, which differs per libm). Go's arm64 auto-FMA gives
301
344
  the documented `fma` parity class on the f64 cases.
302
345
 
303
- | case | jz | vs V8 | vs AS | vs fastest native | jz wasm |
346
+ | case | JZ | vs V8 | vs AS | vs fastest native | JZ wasm |
304
347
  | --- | ---: | ---: | ---: | --- | ---: |
305
348
  | **synth** — osc + ADSR + biquad | **2.32 ms** | **1.33×** | **1.07×** | **beats all** (Rust 0.89×) | 2.0 kB |
306
349
  | **fft** — radix-2 Cooley–Tukey | **1.14 ms** | **1.27×** | **1.13×** | **ties** (Rust 1.07×) | 2.3 kB |
307
350
  | **blur** — RGBA box blur | **0.90 ms** | **9.4×** | **5.8×** | trails (native SIMDs the stencil, 1.5×) | 3.4 kB |
308
351
  | **bytebeat** — integer one-liner | **0.67 ms** | **3.7×** | **5.4×** | trails (native vectorizes, 1.3×) | 1.5 kB |
309
352
 
310
- The headline: jz beats the JS field (V8, AssemblyScript) on **every** audio/image
353
+ The headline: JZ beats the JS field (V8, AssemblyScript) on **every** audio/image
311
354
  case, **ties native on FFT**, and is the **fastest of all targets on the synth
312
355
  pipeline** — its per-sample loop is loop-carried (oscillator phase + biquad
313
- feedback), so native can't auto-vectorize it either, and jz's tight scalar f64
356
+ feedback), so native can't auto-vectorize it either, and JZ's tight scalar f64
314
357
  codegen with no NaN-box overhead wins outright. The two stateless integer kernels
315
358
  (bytebeat, blur) are where `clang`/`zig`/`rustc` auto-vectorize an
316
- embarrassingly-parallel loop jz emits as scalar — native is the floor there, but
317
- jz still beats the JS field by 3.7–9.4× there. (Numbers: darwin/arm64, Apple M4 Max; the live snapshot
359
+ embarrassingly-parallel loop JZ emits as scalar — native is the floor there, but
360
+ JZ still beats the JS field by 3.7–9.4× there. (Numbers: darwin/arm64, Apple M4 Max; the live snapshot
318
361
  is [results.json](results.json).)
319
362
 
363
+ ### General-codegen kernels — 3-D render, procedural, sort, sequence DP
364
+
365
+ Four kernels added to probe JZ's **general** scalar/branch/gather-scatter codegen,
366
+ beyond the SIMD-friendly reductions and stencils above — the cases a graphics,
367
+ procedural, data, or text workload actually hits. All bit-identical across targets
368
+ (`raytrace`/`noise` are transcendental-free f64, so Go's arm64 auto-FMA is the
369
+ documented `fma` class; `radixsort`/`levenshtein` are pure integer, exact
370
+ everywhere).
371
+
372
+ | case | JZ | vs V8 | vs AS | vs fastest native | JZ wasm |
373
+ | --- | ---: | ---: | ---: | --- | ---: |
374
+ | **radixsort** — LSD u32 sort | **2.75 ms** | **1.21×** | **1.19×** | trails (clang 1.5×) | 1.6 kB |
375
+ | **noise** — Perlin fBm (5 oct) | **6.59 ms** | **1.36×** | 0.23× | trails (Rust 5.2×) | 8.0 kB |
376
+ | **raytrace** — sphere render | **2.00 ms** | **1.07×** | 0.89× | trails (Rust 1.9×) | 4.8 kB |
377
+ | **levenshtein** — edit-distance DP | ~6 ms | ~1.0× | 0.35× | trails (clang ~3×) | 7.5 kB |
378
+
379
+ These are the honest mixed bag — and the point. JZ beats raw V8 on three of four
380
+ (and the smallest wasm of the whole field on `radixsort`, 1.6 kB), but it only
381
+ beats AssemblyScript on `radixsort`; it **trails AS** on `noise` (~4.3×) and
382
+ `levenshtein` (~2.5–2.9×), and only **ties V8** on `levenshtein` (the median
383
+ straddles 1.0× run-to-run). The two AS gaps localize the next codegen work:
384
+ `noise`'s nested permutation-table indirection (`perm[perm[X]+Y]`) and
385
+ `levenshtein`'s branchy `min`-reduction DP over a rolling typed-array row are
386
+ exactly the scalar/gather shapes JZ does not yet lower as tightly as AS's
387
+ `asc -O3`. They sit out the curated regression gate (`test/bench.js`) — like
388
+ `heat`/`matmul`/`nbody`/`particle`/`lorenz` — until JZ closes the gap and they
389
+ ratchet in as wins.
390
+
391
+ ### Control-flow & gather/scatter kernels — recursion, hashing, sieve, VM, sparse
392
+
393
+ A second probe set, aimed squarely at the patterns the first batch flagged —
394
+ recursion, branchy probe chains, scatter, and indirect gather — to map where JZ's
395
+ general (non-SIMD) codegen actually stands. All bit-identical across every target
396
+ (`spmv` is f64 over exact small integers, so even Go-native FMA agrees; `nqueens`
397
+ draws its board sizes from a runtime array so clang/rustc can't fold the recursion
398
+ to a constant).
399
+
400
+ | case | JZ | vs V8 | vs AS | vs native C | what it probes |
401
+ | --- | ---: | ---: | ---: | ---: | --- |
402
+ | **vm** — bytecode dispatch | **7.30 ms** | **1.55×** | **2.20×** | **1.13×** | if/else opcode dispatch + indirect fetch |
403
+ | **spmv** — sparse A·x (CSR) | **2.67 ms** | **1.78×** | **1.25×** | 0.90× | indirect gather `x[col[k]]` |
404
+ | **sieve** — Eratosthenes | **7.18 ms** | **1.53×** | **1.30×** | 0.80× | strided scatter + outer branch |
405
+ | **nqueens** — backtracking | **6.05 ms** | **1.29×** | 0.93× | 0.78× | deep recursion + bitmask branch |
406
+ | **dict** — hash table | 4.11 ms | 0.78× | 0.81× | 0.39× | hash scatter + probe-chain gather |
407
+
408
+ The split is sharp and informative. **JZ is excellent at dense dispatch and
409
+ in-cache gather**: it wins `vm` against the *entire* field — including native
410
+ `clang -O3` (the if/else interpreter loop lowers to tight branches with no
411
+ NaN-box overhead, where AS pays per-access bounds checks), and wins `spmv`/`sieve`
412
+ over the JS field while sitting near native. But it **loses `dict` to both V8
413
+ (1.28×) and AS (1.24×)** — open-addressing's hash-scatter + dependent-load probe
414
+ chain is the same gather/branch shape as `noise` and `levenshtein`, and the
415
+ clearest, most reproducible deficiency the suite has surfaced. `nqueens` is a
416
+ near-tie with AS on recursion. Together the nine new cases triangulate it: JZ's
417
+ gap to the AS/native frontier is concentrated in **scatter-heavy hashing and
418
+ dependent-gather / branchy-DP** kernels, not in dense scalar or dispatch loops.
419
+ Like the batch above, these stay bench-only (out of `test/bench.js`) until the gap
420
+ closes.
421
+
320
422
  ### watr — WAT-to-wasm compiler on small corpus
321
423
 
322
424
  | target | median | ×v8 | size | parity |
323
425
  | --- | ---: | ---: | ---: | --- |
324
426
  | V8 (node) raw JS | 1.38 ms | 1.00× | 2.6 kB | ok |
325
- | **jz → V8 wasm** | **1.17 ms** | **1.17×** | **238.4 kB** | **ok** |
427
+ | **JZ → V8 wasm** | **1.17 ms** | **1.17×** | **238.4 kB** | **ok** |
326
428
 
327
- jz is 1.07× slower than V8 raw JS on this large compiler bundle. The size
429
+ JZ is 1.07× slower than V8 raw JS on this large compiler bundle. The size
328
430
  (144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
329
431
  has the advantage of profile-guided tiering on a long-running compiler.
330
432
 
331
433
  ### Where the gaps live
332
434
 
333
- Aggregate geomean (jz / target):
435
+ Aggregate geomean (JZ / target):
334
436
 
335
437
  | target | speed | size |
336
438
  | --- | ---: | ---: |
337
439
  | V8 (node) | **0.40×** | — |
338
440
  | AssemblyScript | **0.36×** | **1.16×** |
339
441
 
340
- jz wins or ties V8 on every kernel case; the only V8 losses are the
442
+ JZ wins or ties V8 on every kernel case; the only V8 losses are the
341
443
  self-hosting rows `watr` (1.07×) and `jessie` (1.28×). AS is beaten on
342
- speed across the shared cases. On size jz is ~1.1× AS (geomean 1.16×,
343
- median 1.10×) — jz wins on speed, AS on bytes.
444
+ speed across the shared cases. On size JZ is ~1.1× AS (geomean 1.16×,
445
+ median 1.10×) — JZ wins on speed, AS on bytes.
344
446
 
345
447
  Against the systems languages compiled to the same target — **WebAssembly, run in
346
- V8** — jz is **2.4× faster than C, 2.6× than Rust, and 4.9× than Go** (geomean),
347
- and **competitive with native C** itself (1.07×, the lone non-wasm reference).
348
- That apples-to-apples wasm field is the headline chart above.
448
+ V8** — JZ is **2.4× faster than C, 2.6× than Rust, and 4.8× than Go** (geomean).
449
+ That apples-to-apples wasm field is the headline chart above, and JZ holds
450
+ **geomean parity with native `clang -O3`** itself (1.11×) the lone non-wasm
451
+ reference row, the speed-of-light ceiling, never a per-case beat-claim.
349
452
 
350
453
  Case-by-case summary:
351
454
 
352
- * **biquad, mat4, poly, bitwise, callback: large wins.** jz beats V8 by
455
+ * **biquad, mat4, poly, bitwise, callback: large wins.** JZ beats V8 by
353
456
  2.0–12.9× and AS by 1.4–9.1×. Typed-array scalarization, i32 narrowing,
354
457
  and closure lowering are the drivers.
355
- * **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** jz beats V8 by
458
+ * **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** JZ beats V8 by
356
459
  1.1–2.4× and ties or beats AS. These are memory-bound or branch-heavy
357
460
  workloads where codegen quality matters less than data layout.
358
- * **json: solid win.** jz beats V8 by 1.3× on runtime JSON parsing; AS
461
+ * **json: solid win.** JZ beats V8 by 1.3× on runtime JSON parsing; AS
359
462
  cannot run this case.
360
- * **alpha: the native floor.** jz beats V8 2.0× and every wasm rival (Rust/C/Go
463
+ * **alpha: the native floor.** JZ beats V8 2.0× and every wasm rival (Rust/C/Go
361
464
  → wasm), but trails the native-C reference ~14× (374 µs vs 26 µs) — alpha's hot
362
- path is an unsigned i32 multiply. jz already lifts it to 128-bit SIMD (an i16x8
465
+ path is an unsigned i32 multiply. JZ already lifts it to 128-bit SIMD (an i16x8
363
466
  widening byte-map); native C pulls ahead on width alone — 256-bit AVX2 with a
364
467
  fused byte multiply-add, run as native code with no per-load bounds checks. The
365
468
  residual is the wasm-v128-vs-AVX2 ceiling, not a missing pass. A known gap we
366
469
  publish rather than hide.
367
- * **watr: near parity.** jz is 1.07× slower than V8 on a 144 kB compiler
470
+ * **watr: near parity.** JZ is 1.07× slower than V8 on a 144 kB compiler
368
471
  bundle. It is one of two self-host rows (with jessie) where V8's
369
- profile-guided JIT tiers beat jz's AOT wasm.
472
+ profile-guided JIT tiers beat JZ's AOT wasm.