jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,369 @@
1
+ # jz benchmark suite
2
+
3
+ Cross-target workload suite for jz codegen quality. Each benchmark is a case
4
+ folder under `bench/`:
5
+
6
+ ```txt
7
+ bench/<case>/<case>.js JavaScript source used by V8, jz, etc.
8
+ bench/<case>/<case>.c optional native C baseline
9
+ bench/<case>/<case>.rs optional Rust baseline
10
+ bench/<case>/<case>.go optional Go baseline
11
+ bench/<case>/<case>.zig optional Zig baseline
12
+ bench/<case>/<case>.as.ts optional AssemblyScript baseline
13
+ bench/<case>/<case>.npy.py optional NumPy baseline
14
+ bench/<case>/<case>.wat optional hand-written WAT baseline
15
+ ```
16
+
17
+ Every case prints the same line:
18
+
19
+ ```txt
20
+ median_us=<int> checksum=<u32> samples=<int> stages=<int> runs=<int>
21
+ ```
22
+
23
+ The orchestrator runs selected cases against selected targets and flags checksum
24
+ drift as `DIFF`.
25
+
26
+ ## Run
27
+
28
+ ```sh
29
+ npm run bench
30
+ node bench/bench.mjs --targets=nat,rust,go,numpy,v8,jz
31
+ node bench/bench.mjs --targets=jz --cases=biquad,mat4,poly,bitwise
32
+ node bench/bench.mjs --targets=v8,deno,bun,spidermonkey,graaljs
33
+ node bench/bench.mjs --cases=biquad,fft,synth,bytebeat,blur
34
+ node bench/bench.mjs biquad
35
+ node bench/bench.mjs mat4 --targets=nat,v8,jz
36
+ ```
37
+
38
+ ## Cases
39
+
40
+ | id | purpose |
41
+ | --- | --- |
42
+ | [`biquad`](biquad/biquad.js) | DSP filter cascade; dense f64 typed-array loop and offset-fusion baseline |
43
+ | [`mat4`](mat4/mat4.js) | fixed-size typed-array loops; exposes loop unrolling and offset fusion gaps |
44
+ | [`poly`](poly/poly.js) | same `sum` called with `Float64Array` and `Int32Array`; exposes bimorphic typed-array dispatch |
45
+ | [`bitwise`](bitwise/bitwise.js) | long `i32` narrowing chains with `Math.imul`, shifts, and unsigned conversion |
46
+ | [`tokenizer`](tokenizer/tokenizer.js) | string-heavy scan with `charCodeAt`, branches, and integer token accumulation |
47
+ | [`callback`](callback/callback.js) | `Array.map` callback path; exposes closure/call-indirect and array allocation cost |
48
+ | [`aos`](aos/aos.js) | array-of-object rows copied into typed arrays; exposes schema-slot read cost |
49
+ | [`mandelbrot`](mandelbrot/mandelbrot.js) | 256×256 escape-time iteration; dense f64 hot loop with conditional break and i32 counter |
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
+ | [`sort`](sort/sort.js) | in-place heapsort over a typed array; exposes call-heavy nested loops and typed-array index propagation |
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 |
54
+ | [`bytebeat`](bytebeat/bytebeat.js) | classic integer "bytebeat" one-line-song synthesis to 8-bit PCM; pure i32, bit-exact everywhere |
55
+ | [`fft`](fft/fft.js) | radix-2 Cooley–Tukey FFT over a transcendental-free twiddle table; the canonical numeric/audio kernel |
56
+ | [`synth`](synth/synth.js) | minisynth audio pipeline — polynomial oscillator + ADSR + biquad per sample; loop-carried f64 |
57
+ | [`blur`](blur/blur.js) | separable box blur on an RGBA8 image; integer stencil with edge clamp, the canonical image pipeline |
58
+ | [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
59
+ | [`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 |
61
+
62
+ The `watr`, `jessie`, and `jz` rows are self-referential — jz (or its deps)
63
+ compiling code. They stay runnable (`--cases=watr,jessie,jz`) and gated in
64
+ `test/bench.js`, but are hidden from the bench page and the headline geomean SVG:
65
+ they answer a different question (compiler throughput on itself) from the
66
+ cross-language kernel comparison the page is about.
67
+
68
+ Native rows for `json` are fixed-source references, not semantic equivalents
69
+ of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
70
+ compile-time string, and Zig may constant-fold the whole parse+walk under
71
+ 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
73
+ compile-time folded, while the compiler can still specialize the stable literal
74
+ shape. External unknown-shape JSON still uses the generic runtime parser.
75
+
76
+ Native-language rows are intentionally per case. NumPy rows are used only
77
+ where a vectorized array implementation is a meaningful Python convention.
78
+
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.
87
+
88
+ ### Parity classes
89
+
90
+ The `parity` column is `ok` when the run's checksum matches the most common
91
+ checksum across all targets, `DIFF` when it diverges in a way that suggests
92
+ a bug, and `fma` when the divergence is the documented FMA-fusion class.
93
+ The Go arm64 backend auto-fuses `a*b + c` to `FMADDD` (mandatory in ARMv8,
94
+ no compiler flag to disable it), which alters bit-level rounding on
95
+ recurrence-style loops like `biquad`. Result is still IEEE-754
96
+ correctly-rounded; cascade is the same algorithm.
97
+
98
+ ## Targets
99
+
100
+ | id | what it measures |
101
+ | --- | --- |
102
+ | `nat` | clang `-O3` native C baseline, when a matching C workload exists |
103
+ | `natgcc` | gcc `-O3`, when real gcc is installed |
104
+ | `rust` | Rust `rustc -C opt-level=3 -C target-cpu=native` — native baseline; the headline rival is `rust-wasm` |
105
+ | `go` | Go native compiler — native baseline; the headline rival is `go-wasm` |
106
+ | `zig` | Zig `build-exe -O ReleaseFast` — native baseline |
107
+ | `numpy` | vectorized NumPy, when a matching `.npy.py` exists |
108
+ | `v8` | raw JavaScript on Node/V8 |
109
+ | `deno` | raw JavaScript on Deno/V8 |
110
+ | `bun` | raw JavaScript on Bun/JavaScriptCore |
111
+ | `spidermonkey` | raw JavaScript on SpiderMonkey shell (`spidermonkey`, `sm`, `js128`, `js115`, `js102`, or `js`) |
112
+ | `shermes` | Static Hermes — JS AOT-compiled to a native binary (`shermes -O`), when installed |
113
+ | `graaljs` | raw JavaScript on GraalJS |
114
+ | `jz` | jz output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
115
+ | `as` | AssemblyScript `asc -O3 --runtime stub`, when a matching `.as.ts` exists |
116
+ | `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
+ | `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` |
121
+ | `wat` | hand-written WAT baseline when a case provides `run-wat.mjs` |
122
+ | `porf` | Porffor (`porf run`) when installed |
123
+ | `jawsm` | jawsm when installed |
124
+
125
+ The `size` column reports the artifact size each target measures: the
126
+ compiled native binary for `nat`/`rust`/`go`/`zig`, the produced
127
+ `.wasm` for `jz`/`as`/`rust-wasm`/`go-wasm`/`c-wasm`/hand-WAT/jawsm/`jz-w2c` (the C-translated
128
+ executable), or the source file for raw-JS interpreters where there is no
129
+ compile step. For source files with imports, raw-JS size is only the entry file;
130
+ jz size is the bundled wasm artifact.
131
+
132
+ Note the preset trade: these speed tables build jz at the default
133
+ (`optimize: 2`, speed-leaning — loop unroll + SIMD lift), while the dedicated
134
+ size comparison (`npm run bench:size`) builds with `optimize: 'size'` —
135
+ typically ~2× smaller on the same kernel (biquad: 3.5 kB default vs 1.6 kB
136
+ size-tuned). Pick the preset for what you ship; the two tables are not the
137
+ same artifact.
138
+
139
+ Runtime command overrides:
140
+
141
+ `watr` is intentionally compiled by jz with a size-oriented pass config
142
+ (`watr:false`, `smallConstForUnroll:false`): on a large compiler bundle, the
143
+ 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.
145
+
146
+ ```sh
147
+ BUN_BIN=/path/to/bun \
148
+ DENO_BIN=/path/to/deno \
149
+ SPIDERMONKEY_BIN=/path/to/js \
150
+ SHERMES_BIN=/path/to/shermes \
151
+ GRAALJS_BIN=/path/to/graaljs \
152
+ PORF_BIN=/path/to/porf \
153
+ node bench/bench.mjs --targets=bun,deno,spidermonkey,shermes,graaljs,porf
154
+ ```
155
+
156
+ ## Reading the numbers (darwin/arm64, M-class)
157
+
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.
162
+
163
+ ### biquad — f64 typed-array DSP cascade
164
+
165
+ | target | median | ×v8 | size | parity |
166
+ | --- | ---: | ---: | ---: | --- |
167
+ | **jz → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
168
+ | AssemblyScript (asc -O3 --runtime stub) | 6.51 ms | 1.38× | 1.9 kB | ok |
169
+ | 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
+ | hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
172
+
173
+ jz beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
174
+ offset-fusion, and base-hoisting pipeline delivers dense-f64 loop codegen
175
+ that matches the hand-WAT floor.
176
+
177
+ ### mat4 — fixed-size Float64Array multiply
178
+
179
+ | target | median | ×v8 | size | parity |
180
+ | --- | ---: | ---: | ---: | --- |
181
+ | **jz → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
182
+ | AssemblyScript (asc -O3 --runtime stub) | 6.71 ms | 1.28× | 1.6 kB | ok |
183
+ | 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
+ | hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
186
+
187
+ jz is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
188
+ SIMD hot path (unrolled 4×4 multiply) is the win; V8's JIT doesn't vectorize
189
+ this from JS source.
190
+
191
+ ### poly — bimorphic typed-array reduce
192
+
193
+ | target | median | ×v8 | size | parity |
194
+ | --- | ---: | ---: | ---: | --- |
195
+ | **jz → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
196
+ | AssemblyScript (asc -O3 --runtime stub) | 0.81 ms | 2.07× | 1.3 kB | ok |
197
+ | V8 (node) raw JS | 1.67 ms | 1.00× | 1014 B | ok |
198
+
199
+ jz is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
200
+ `sum` (called with both `Float64Array` and `Int32Array`) stays on typed
201
+ paths without falling back to generic dispatch.
202
+
203
+ ### bitwise — i32 narrowing chains (`Math.imul`, shifts, FNV-1a)
204
+
205
+ | target | median | ×v8 | size | parity |
206
+ | --- | ---: | ---: | ---: | --- |
207
+ | **jz → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
208
+ | V8 (node) raw JS | 3.87 ms | 1.00× | 1005 B | ok |
209
+ | 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
+ | hand-WAT → V8 wasm | 3.59 ms | 1.11× | 355 B | ok |
212
+
213
+ jz is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
214
+ (`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
215
+ overhead on every operation.
216
+
217
+ ### tokenizer — string scan with `charCodeAt` and integer accumulation
218
+
219
+ | target | median | ×v8 | size | parity |
220
+ | --- | ---: | ---: | ---: | --- |
221
+ | **jz → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
222
+ | AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.15× | 1.6 kB | ok |
223
+ | V8 (node) raw JS | 0.13 ms | 1.00× | 2.0 kB | ok |
224
+
225
+ jz is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
226
+ `charCodeAt`-heavy scan. Both are well ahead of V8.
227
+
228
+ ### callback — `Array.map` closure + i32 fold
229
+
230
+ | target | median | ×v8 | size | parity |
231
+ | --- | ---: | ---: | ---: | --- |
232
+ | **jz → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
233
+ | V8 (node) raw JS | 0.62 ms | 1.00× | 1.3 kB | ok |
234
+ | AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.78× | 1.9 kB | ok |
235
+
236
+ jz is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
237
+ `Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
238
+ V8's JIT does not inline the closure across the `map` boundary.
239
+
240
+ ### aos — array-of-object rows to typed arrays
241
+
242
+ | target | median | ×v8 | size | parity |
243
+ | --- | ---: | ---: | ---: | --- |
244
+ | **jz → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
245
+ | V8 (node) raw JS | 1.33 ms | 1.00× | 1.1 kB | ok |
246
+ | AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.95× | 2.1 kB | ok |
247
+
248
+ jz is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
249
+ reads are direct field offsets; the gap is small because the workload is
250
+ memory-bound.
251
+
252
+ ### mandelbrot — 256×256 escape-time iteration
253
+
254
+ | target | median | ×v8 | size | parity |
255
+ | --- | ---: | ---: | ---: | --- |
256
+ | 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** |
258
+ | V8 (node) raw JS | 9.90 ms | 1.00× | 1.8 kB | ok |
259
+
260
+ jz is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
261
+ conditional break compacts to 1.0 kB — the smallest wasm in the suite.
262
+
263
+ ### json — runtime `JSON.parse` plus stable-shape walk
264
+
265
+ | target | median | ×v8 | size | parity |
266
+ | --- | ---: | ---: | ---: | --- |
267
+ | **jz → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
268
+ | V8 (node) raw JS | 0.27 ms | 1.00× | 1.2 kB | ok |
269
+
270
+ jz is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
271
+ inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
272
+
273
+ ### sort — in-place heapsort over typed array
274
+
275
+ | target | median | ×v8 | size | parity |
276
+ | --- | ---: | ---: | ---: | --- |
277
+ | **jz → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
278
+ | AssemblyScript (asc -O3 --runtime stub) | 7.96 ms | 0.98× | 1.9 kB | ok |
279
+ | V8 (node) raw JS | 7.79 ms | 1.00× | 1.6 kB | ok |
280
+
281
+ jz is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
282
+ nested loops with typed-array index propagation stay on the i32 path.
283
+
284
+ ### crc32 — table-driven CRC-32 over byte buffer
285
+
286
+ | target | median | ×v8 | size | parity |
287
+ | --- | ---: | ---: | ---: | --- |
288
+ | **jz → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
289
+ | AssemblyScript (asc -O3 --runtime stub) | 8.76 ms | 1.11× | 1.4 kB | ok |
290
+ | V8 (node) raw JS | 9.71 ms | 1.00× | 1.8 kB | ok |
291
+
292
+ jz is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
293
+ typed-array parameter propagation keep the LUT lookup on raw i32.
294
+
295
+ ### Audio + image showcase (cross-language, bit-exact)
296
+
297
+ Four standard kernels added to make the audio story concrete. All are written so
298
+ the output is **bit-identical** across every engine and native target — integer
299
+ math (bytebeat, blur) or transcendental-free f64 (fft, synth: in-source Taylor
300
+ polynomials, not `Math.sin`, which differs per libm). Go's arm64 auto-FMA gives
301
+ the documented `fma` parity class on the f64 cases.
302
+
303
+ | case | jz | vs V8 | vs AS | vs fastest native | jz wasm |
304
+ | --- | ---: | ---: | ---: | --- | ---: |
305
+ | **synth** — osc + ADSR + biquad | **2.32 ms** | **1.33×** | **1.07×** | **beats all** (Rust 0.89×) | 2.0 kB |
306
+ | **fft** — radix-2 Cooley–Tukey | **1.14 ms** | **1.27×** | **1.13×** | **ties** (Rust 1.07×) | 2.3 kB |
307
+ | **blur** — RGBA box blur | **0.90 ms** | **9.4×** | **5.8×** | trails (native SIMDs the stencil, 1.5×) | 3.4 kB |
308
+ | **bytebeat** — integer one-liner | **0.67 ms** | **3.7×** | **5.4×** | trails (native vectorizes, 1.3×) | 1.5 kB |
309
+
310
+ The headline: jz beats the JS field (V8, AssemblyScript) on **every** audio/image
311
+ case, **ties native on FFT**, and is the **fastest of all targets on the synth
312
+ 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
314
+ codegen with no NaN-box overhead wins outright. The two stateless integer kernels
315
+ (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
318
+ is [results.json](results.json).)
319
+
320
+ ### watr — WAT-to-wasm compiler on small corpus
321
+
322
+ | target | median | ×v8 | size | parity |
323
+ | --- | ---: | ---: | ---: | --- |
324
+ | 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** |
326
+
327
+ jz is 1.07× slower than V8 raw JS on this large compiler bundle. The size
328
+ (144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
329
+ has the advantage of profile-guided tiering on a long-running compiler.
330
+
331
+ ### Where the gaps live
332
+
333
+ Aggregate geomean (jz / target):
334
+
335
+ | target | speed | size |
336
+ | --- | ---: | ---: |
337
+ | V8 (node) | **0.40×** | — |
338
+ | AssemblyScript | **0.36×** | **1.16×** |
339
+
340
+ jz wins or ties V8 on every kernel case; the only V8 losses are the
341
+ 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.
344
+
345
+ 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.
349
+
350
+ Case-by-case summary:
351
+
352
+ * **biquad, mat4, poly, bitwise, callback: large wins.** jz beats V8 by
353
+ 2.0–12.9× and AS by 1.4–9.1×. Typed-array scalarization, i32 narrowing,
354
+ and closure lowering are the drivers.
355
+ * **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** jz beats V8 by
356
+ 1.1–2.4× and ties or beats AS. These are memory-bound or branch-heavy
357
+ workloads where codegen quality matters less than data layout.
358
+ * **json: solid win.** jz beats V8 by 1.3× on runtime JSON parsing; AS
359
+ cannot run this case.
360
+ * **alpha: the native floor.** jz beats V8 2.0× and every wasm rival (Rust/C/Go
361
+ → 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
363
+ widening byte-map); native C pulls ahead on width alone — 256-bit AVX2 with a
364
+ fused byte multiply-add, run as native code with no per-load bounds checks. The
365
+ residual is the wasm-v128-vs-AVX2 ceiling, not a missing pass. A known gap we
366
+ publish rather than hide.
367
+ * **watr: near parity.** jz is 1.07× slower than V8 on a 144 kB compiler
368
+ bundle. It is one of two self-host rows (with jessie) where V8's
369
+ profile-guided JIT tiers beat jz's AOT wasm.
@@ -0,0 +1,102 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 474" width="720" height="474" role="img" aria-label="jz benchmark — every rival compiled to WebAssembly, run in V8 · native C = reference; geometric mean across 22 benchmark cases · lower is faster, jz = 1.00× baseline; each ball's speed is proportional to that engine's geometric-mean runtime across the corpus">
2
+ <rect width="720" height="474" rx="12" fill="#ffffff"/>
3
+
4
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
5
+ <rect x="174" y="43.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
6
+ <line x1="174" y1="38" x2="174" y2="52" stroke="#ced4da" stroke-width="2"/>
7
+ <line x1="650" y1="38" x2="650" y2="52" stroke="#ced4da" stroke-width="2"/>
8
+ <text x="156" y="41" text-anchor="end" font-size="14" font-weight="700" fill="#000000">jz</text>
9
+ <text x="156" y="56" text-anchor="end" font-size="10" fill="#aeb4ba">-O3</text>
10
+ <text x="662" y="49" font-size="13" font-weight="700" fill="#000000">1.00×</text>
11
+ <circle cx="182.0" cy="45" r="8" fill="#000000">
12
+ <animate attributeName="cx" dur="1.6s" repeatCount="indefinite" calcMode="linear"
13
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.00s"/>
14
+ </circle>
15
+ </g>
16
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
17
+ <rect x="174" y="93.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
18
+ <line x1="174" y1="88" x2="174" y2="102" stroke="#ced4da" stroke-width="2"/>
19
+ <line x1="650" y1="88" x2="650" y2="102" stroke="#ced4da" stroke-width="2"/>
20
+ <text x="156" y="91" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">native C</text>
21
+ <text x="156" y="106" text-anchor="end" font-size="10" fill="#aeb4ba">clang -O3 · ref</text>
22
+ <text x="662" y="99" font-size="13" font-weight="500" fill="#868e96">1.07×</text>
23
+ <circle cx="182.0" cy="95" r="8" fill="#adb5bd">
24
+ <animate attributeName="cx" dur="1.7120000000000002s" repeatCount="indefinite" calcMode="linear"
25
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.41s"/>
26
+ </circle>
27
+ </g>
28
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
29
+ <rect x="174" y="143.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
30
+ <line x1="174" y1="138" x2="174" y2="152" stroke="#ced4da" stroke-width="2"/>
31
+ <line x1="650" y1="138" x2="650" y2="152" stroke="#ced4da" stroke-width="2"/>
32
+ <text x="156" y="141" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">C</text>
33
+ <text x="156" y="156" text-anchor="end" font-size="10" fill="#aeb4ba">zig cc → wasm</text>
34
+ <text x="662" y="149" font-size="13" font-weight="500" fill="#868e96">2.43×</text>
35
+ <circle cx="182.0" cy="145" r="8" fill="#adb5bd">
36
+ <animate attributeName="cx" dur="3.8880000000000003s" repeatCount="indefinite" calcMode="linear"
37
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.82s"/>
38
+ </circle>
39
+ </g>
40
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
41
+ <rect x="174" y="193.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
42
+ <line x1="174" y1="188" x2="174" y2="202" stroke="#ced4da" stroke-width="2"/>
43
+ <line x1="650" y1="188" x2="650" y2="202" stroke="#ced4da" stroke-width="2"/>
44
+ <text x="156" y="191" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">Rust</text>
45
+ <text x="156" y="206" text-anchor="end" font-size="10" fill="#aeb4ba">rustc → wasm</text>
46
+ <text x="662" y="199" font-size="13" font-weight="500" fill="#868e96">2.63×</text>
47
+ <circle cx="182.0" cy="195" r="8" fill="#adb5bd">
48
+ <animate attributeName="cx" dur="4.208s" repeatCount="indefinite" calcMode="linear"
49
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-1.23s"/>
50
+ </circle>
51
+ </g>
52
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
53
+ <rect x="174" y="243.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
54
+ <line x1="174" y1="238" x2="174" y2="252" stroke="#ced4da" stroke-width="2"/>
55
+ <line x1="650" y1="238" x2="650" y2="252" stroke="#ced4da" stroke-width="2"/>
56
+ <text x="156" y="241" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">V8</text>
57
+ <text x="156" y="256" text-anchor="end" font-size="10" fill="#aeb4ba">Node (JS)</text>
58
+ <text x="662" y="249" font-size="13" font-weight="500" fill="#868e96">2.69×</text>
59
+ <circle cx="182.0" cy="245" r="8" fill="#adb5bd">
60
+ <animate attributeName="cx" dur="4.304s" repeatCount="indefinite" calcMode="linear"
61
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-1.64s"/>
62
+ </circle>
63
+ </g>
64
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
65
+ <rect x="174" y="293.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
66
+ <line x1="174" y1="288" x2="174" y2="302" stroke="#ced4da" stroke-width="2"/>
67
+ <line x1="650" y1="288" x2="650" y2="302" stroke="#ced4da" stroke-width="2"/>
68
+ <text x="156" y="291" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">AssemblyScript</text>
69
+ <text x="156" y="306" text-anchor="end" font-size="10" fill="#aeb4ba">asc -O3</text>
70
+ <text x="662" y="299" font-size="13" font-weight="500" fill="#868e96">2.80×</text>
71
+ <circle cx="182.0" cy="295" r="8" fill="#adb5bd">
72
+ <animate attributeName="cx" dur="4.4799999999999995s" repeatCount="indefinite" calcMode="linear"
73
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-2.05s"/>
74
+ </circle>
75
+ </g>
76
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
77
+ <rect x="174" y="343.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
78
+ <line x1="174" y1="338" x2="174" y2="352" stroke="#ced4da" stroke-width="2"/>
79
+ <line x1="650" y1="338" x2="650" y2="352" stroke="#ced4da" stroke-width="2"/>
80
+ <text x="156" y="341" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">Porffor</text>
81
+ <text x="156" y="356" text-anchor="end" font-size="10" fill="#aeb4ba">runs 3 / 22</text>
82
+ <text x="662" y="349" font-size="13" font-weight="500" fill="#868e96">3.49×</text>
83
+ <circle cx="182.0" cy="345" r="8" fill="#adb5bd">
84
+ <animate attributeName="cx" dur="5.5840000000000005s" repeatCount="indefinite" calcMode="linear"
85
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-2.46s"/>
86
+ </circle>
87
+ </g>
88
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
89
+ <rect x="174" y="393.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
90
+ <line x1="174" y1="388" x2="174" y2="402" stroke="#ced4da" stroke-width="2"/>
91
+ <line x1="650" y1="388" x2="650" y2="402" stroke="#ced4da" stroke-width="2"/>
92
+ <text x="156" y="391" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">Go</text>
93
+ <text x="156" y="406" text-anchor="end" font-size="10" fill="#aeb4ba">gc → wasm</text>
94
+ <text x="662" y="399" font-size="13" font-weight="500" fill="#868e96">4.91×</text>
95
+ <circle cx="182.0" cy="395" r="8" fill="#adb5bd">
96
+ <animate attributeName="cx" dur="7.856000000000001s" repeatCount="indefinite" calcMode="linear"
97
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-2.87s"/>
98
+ </circle>
99
+ </g>
100
+ <text x="360" y="440" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="11" font-weight="600" fill="#495057">every rival compiled to WebAssembly, run in V8 · native C = reference</text>
101
+ <text x="360" y="458" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="11" fill="#868e96">geometric mean across 22 benchmark cases · lower is faster, jz = 1.00× baseline</text>
102
+ </svg>