jz 0.8.0 → 0.9.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 (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/README.md CHANGED
@@ -21,8 +21,8 @@ JZ distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_D
21
21
  | Good for | Not for |
22
22
  |------------------------------|---------------------------|
23
23
  | DSP, audio, synthesis | UI, DOM, the frontend |
24
- | Image, video, pixels | Servers, APIs, I/O |
25
- | Simulation, physics, games | Async, promises, events |
24
+ | Image, video, pixels | Serving HTTP, hot I/O |
25
+ | Simulation, physics, games | I/O-bound orchestration |
26
26
  | Parsers, codecs, compression | Dynamic, polymorphic, OOP |
27
27
  | Scientific, numeric, ML | Security crypto, big-ints |
28
28
  | Hashing, checksums, RNG | Glue, plumbing, orchestration |
@@ -35,7 +35,7 @@ Output `.wasm` is portable — run it in any host (browser, Node, Deno, edge, pl
35
35
  `npm install jz`
36
36
 
37
37
  ```js
38
- import jz, { compile, compileModule, instantiate } from 'jz'
38
+ import jz, { compile, compileModule, instantiate, transform } from 'jz'
39
39
 
40
40
  // Compile + instantiate
41
41
  const { exports: { add } } = jz('export let add = (a, b) => a + b')
@@ -52,6 +52,12 @@ instantiate(mod).exports.f(21) // 42 — repeat cheaply, no recompile
52
52
  const asyncMod = await WebAssembly.compile(wasm)
53
53
  const asyncInst = await WebAssembly.instantiate(asyncMod)
54
54
  asyncInst.exports.f(21) // 42
55
+
56
+ // jzify as a standalone source→source transform — the same lowering the
57
+ // compiler runs, printed back as canonical jz (also `import transform from 'jz/transform'`)
58
+ transform('var x = 1; function f() { return x }')
59
+ // → 'const f = () => {\n return x;\n};\nlet x;\nx = 1;' (decls hoist, then init order)
60
+ transform(alreadyCanonicalSource, { onlyLowered: true }) // → null (nothing to lower)
55
61
  ```
56
62
 
57
63
  <details>
@@ -151,7 +157,7 @@ Options:
151
157
 
152
158
  ## Language
153
159
 
154
- JZ is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
160
+ JZ is a **strict modern JS subset**. Built-in jzify transform extends support to legacy/async patterns.
155
161
 
156
162
  ```
157
163
  ┌────────────────────────────────────────────────────────────────────────┐
@@ -162,18 +168,23 @@ JZ is a **strict modern JS subset**. Built-in jzify transform extends support to
162
168
  │ │ try/catch/finally throw │ │
163
169
  │ │ operators strings booleans numbers arrays objects `${}` │ │
164
170
  │ │ Math Number String Array Object JSON RegExp Symbol null │ │
165
- │ │ ArrayBuffer DataView TypedArray Map Set │ │
171
+ │ │ ArrayBuffer DataView TypedArray Map Set Atomics │ │
166
172
  │ │ parseInt parseFloat encodeURIComponent Error BigInt │ │
167
173
  │ │ console setTimeout/setInterval Date performance │ │
174
+ │ │ structuredClone groupBy Set algebra iterator helpers │ │
175
+ │ │ fs.read/write (WASI hosts) fetch via async host imports │ │
168
176
  │ └────────────────────────────────────────────────────────────────────┘ │
169
177
  │ jz default (jzify) │
170
178
  │ var function arguments switch new Foo() │
171
179
  │ class new this extends super static #private │
180
+ │ function* yield yield* Foo.prototype.m = … │
181
+ │ async/await async function* for await Promise using │
182
+ │ Symbol.iterator Symbol.asyncIterator Symbol.dispose │
183
+ │ SharedArrayBuffer (→ ArrayBuffer) │
172
184
  │ == != instanceof undefined WeakMap WeakSet │
173
185
  │ │
174
186
  └────────────────────────────────────────────────────────────────────────┘
175
187
  Not supported
176
- async/await Promise function* yield
177
188
  delete getters/setters eval Function with
178
189
  Proxy Reflect
179
190
  import() DOM fetch Intl Node APIs
@@ -192,17 +203,51 @@ Each follows one rule: **JZ takes WASM/native conventions over JS edge-cases whe
192
203
  - **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.
193
204
  - **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).
194
205
  - **No GC** — call `memory.reset()` between batches; `WeakMap`/`WeakSet` wired to `Map`/`Set`.
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.
206
+ - **Pseudo-classical constructors** — `function P(x) { this.x = x }` + `P.prototype.m = function () {…}` fold into the class lowering automatically (the pre-`class` npm idiom); arrow-valued members keep lexical `this` and stay out.
207
+ - **Generators (sync) + iterator helpers** — `function*`/`yield` compile to regenerator-style state machines (no stack suspension): `next(v)`/`return(v)` are ordinary closure calls, `for-of` over a generator call desugars to a plain loop, and ES2025 helper chains (`g().map(f).filter(p).take(n)`, terminals `toArray`/`reduce`/`some`/`every`/`find`/`forEach`) fuse into ONE loop — no intermediate iterator objects. Helper results are also first-class VALUES (`let evens = nat().filter(even)` then `evens.take(5).toArray()`, `x instanceof Iterator`, `Array.from(iter)`) — helper-using programs mint decorated iterator objects, spec-shaped (value+counter callbacks, lazy, early close). `yield*` delegates to any iterable, `for-of` drives any iterator value (stored machines, hand-rolled `{ next }`, `[Symbol.iterator]()` providers) lazily, and `[...g()]` spreads via the fused path. v1 scope: no `try` across yield — rejects with a precise message.
208
+ - **async/await + Promise, no engine event loop** — an `async` function lowers to the same state machine (`await` ≡ `yield`) driven by a plain-jz promise runtime compiled into the module (pay-per-use: sync programs link none of it). Promises are ordinary fixed-shape objects with `then`/`catch`/`finally`; `Promise.resolve/reject/all/race/allSettled/any/try/withResolvers`, `new Promise(executor)`, and plain-object thenable adoption all work. Async GENERATORS ride the same machine with tagged yields (`async function*`, `yield*` delegation, serialized `next()`), and `for await (x of src)` drives async iterators, sync iterators, and arrays of promises alike. The job queue drains at host boundaries (export return, timer tick) — an async export returns a real host `Promise` that settles when the machine completes (including across `setTimeout` awaits). Divergences: job ordering is per-drain-cycle rather than per-continuation, no unhandled-rejection reporting, no `try` across `await` in v1 (precise reject), and `memory.reset()` while a promise is pending is out of contract.
209
+ - **`fetch` just works** — a bare `fetch(url)` binds from the JS host automatically (no import statement); the `Response` crosses as a live external handle, so `r.text()`/`r.json()` dispatch host-side and are awaitable in turn. Any custom host function returning a `Promise` is likewise awaitable via `{ imports }`. I/O stays host-side (the doctrine), asynchrony crosses the boundary:
210
+ ```js
211
+ const { exports } = jz(`
212
+ export let title = async (url) => {
213
+ let r = await fetch(url)
214
+ let body = await r.text()
215
+ return body.slice(body.indexOf('<title>') + 7, body.indexOf('</title>'))
216
+ }`)
217
+ await exports.title('https://example.com')
218
+ ```
219
+ Identical in browser and Node — no WASI involved (WASI preview1 has no networking; wasi-http needs the component model, which stays future). Under `--host wasi` a warning tells you to wire `env.fetch` yourself.
220
+ - **Workers v1 (shared-memory SPMD)** — `sharedMemory: true` compiles against a shared `WebAssembly.Memory` (atomic heap bump, wasm `shared` memtype); `Atomics.*` on Int32Array lowers to wasm thread ops (`wait`/`notify` included); `jz.pool(src, {threads})` runs the same kernel across node worker_threads over one memory — annotate shared-array params as `(arr = new Int32Array(0))`. v1 contract: shared typed arrays + scalars; strings/objects stay thread-local.
221
+ - **`String(number)` is ES-spec exact** — shortest round-trip digits via a built-in Ryū formatter (`String(0.1 + 0.2)` → `"0.30000000000000004"`, `String(Math.PI)` → `"3.141592653589793"`), including exponential notation and subnormals; its ~9.7 KB power-of-5 table is lazily included only in modules that stringify floats.
196
222
  - **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.
197
223
  - **`Date` getters return UTC** (`getHours` ≡ `getUTCHours`) – the IANA timezone database is hundreds of KB.
198
224
  - **`Math.random` is seedable** — default draws host entropy; pass `randomSeed: n` for a reproducible stream.
199
225
  </details>
200
226
 
227
+ <details>
228
+ <summary><strong>What will JZ never support — and why?</strong></summary>
229
+
230
+ Everything else is compiled, lowered by the built-in jzify pass, or rejected with a clean error — silent divergence is treated as a bug. These stay out **by design**; each is traded for a zero-cost guarantee:
231
+
232
+ - **Proxy, Reflect** — traps have no meaning over fixed-shape structs with compile-time-resolved offsets.
233
+ - **Property descriptors & accessors** — `defineProperty` descriptors, getters/setters, `writable`/`enumerable`: objects carry values only, no per-property metadata — that's what makes a property read one load.
234
+ - **Live prototype chains** — `__proto__`, delegation, monkey-patching: `Object.create(proto)` is a documented shallow copy; method dispatch is static.
235
+ - **`delete` on literal-key properties** — an object's shape is fixed at construction (computed-key/dictionary-mode `delete o[k]` works).
236
+ - **eval, `Function` constructor, `with`** — would require the compiler (or an interpreter) at runtime; JZ ships neither.
237
+ - **Intl, Temporal** — ICU/CLDR and timezone tables are hundreds of KB to MB, against single-digit-kB output. `Date` keeps deterministic UTC slices.
238
+ - **UTF-16 string semantics & Unicode tables** — strings are UTF-8 bytes; `\p{…}` classes, `normalize` forms and locale case tables are the same multi-KB cost Intl was refused for.
239
+ - **Arbitrary-precision BigInt** — BigInt is a raw 64-bit integer (wraps past ±2⁶³); bignum chains allocate unboundedly, and security crypto is explicitly out of scope.
240
+ - **WeakRef, FinalizationRegistry** — no GC to observe; `WeakMap`/`WeakSet` fold to `Map`/`Set`.
241
+ - **Annex B legacies, DOM, fetch, Node APIs** — the parts of JS the subset exists to shed; I/O stays host-side.
242
+
243
+ The litmus for this list: the feature either needs a runtime JZ refuses to ship, or per-access metadata that would tax every program — including the ones not using it.
244
+ </details>
245
+
201
246
 
202
247
  <details>
203
248
  <summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
204
249
 
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.
250
+ Only the ones that fit the JZ subset. There's no runtime, so packages touching the DOM or Node APIs won't compile — but pure numeric/algorithmic source does, `async`/`Promise` code included (network calls cross as awaitable host imports, not bundled clients).
206
251
 
207
252
  - **Relative imports** (`./dep.js`) bundle at compile time.
208
253
  - **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.
@@ -315,6 +360,8 @@ There's two possible `host` targets:
315
360
  - **`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.
316
361
  - **`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.
317
362
 
363
+ Under `--host wasi` module init ships as the standard WASI reactor export `_initialize` (never a wasm start section — the p1 ABI forbids WASI calls there). ABI-following hosts (wasmtime, node:wasi `initialize()`, `jz/wasi`, `jz/interop`) call it after wiring memory; instantiating by hand, call `instance.exports._initialize?.()` once before anything else. Command entries (`run`, `_start`) self-init, so invoking them directly also works.
364
+
318
365
  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:
319
366
 
320
367
  | What your code does | `js` (default) | `wasi` |
@@ -446,7 +493,7 @@ Literals (`0` vs `0.5`), operators (`|` `<<` `&` ⇒ i32), and how a value is us
446
493
  <details>
447
494
  <summary><strong>Is JZ production-ready?</strong></summary>
448
495
 
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.
496
+ 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 slice (≈3,900 language + built-ins files at **0 fail** — every excluded test is skip-classified by name, so a new failure can't hide), the benchmark gate, and the self-host build in CI, so regressions surface immediately.
450
497
 
451
498
  </details>
452
499
 
package/bench/README.md CHANGED
@@ -53,6 +53,8 @@ node bench/bench.mjs mat4 --targets=nat,v8,jz
53
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
+ | [`fftplan`](fftplan/fftplan.js) | the same butterflies through a Map-cached plan object — how every JS DSP lib ships FFT; exposes typed-array kind loss through return values / object fields / params (dynamic element access in the hot loop) |
57
+ | [`provenance`](provenance/provenance.js) | one butterfly kernel over bit-identical tables that differ only in provenance — direct return, returned-object field, Map-cached field, module-global memo; per-edge coverage for the `fftplan` kind-loss class |
56
58
  | [`synth`](synth/synth.js) | minisynth audio pipeline — polynomial oscillator + ADSR + biquad per sample; loop-carried f64 |
57
59
  | [`blur`](blur/blur.js) | separable box blur on an RGBA8 image; integer stencil with edge clamp, the canonical image pipeline |
58
60
  | [`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 |
@@ -70,16 +72,28 @@ node bench/bench.mjs mat4 --targets=nat,v8,jz
70
72
  | [`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
73
  | [`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
74
  | [`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× |
75
+ | [`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 — then the only case JZ trailed V8; proving the return i32 measured 8.7 → 5.2 ms, jz now beats V8 1.3× |
76
+ | [`dispatch`](dispatch/dispatch.js) | data-driven dispatch through a table of 8 first-class functions — one call site fans out per an unpredictable opcode stream; the megamorphic call-site / virtual-dispatch kernel (strategy tables, event pipelines, effect chains), the classic call-IC deopt shape |
77
+ | [`shapes`](shapes/shapes.js) | per-variant measure summed over records of 8 heterogeneous object shapes — every access site sees 8 hidden classes; the megamorphic property-load / hidden-class kernel (JSON rows, AST nodes, ECS entities), the classic load-IC deopt shape — and JZ's widest gap (the schema union falls to dynamic-property probes) |
78
+ | [`strbuild`](strbuild/strbuild.js) | per-record string formatting (`id,name,value\n` per row: int→string + concat + char fold) — the serializer/logger inner loop, the string-allocation deopt shape (a giant `+=` accumulator is deliberately avoided: eager no-GC strings make it quadratic in allocation — jz exhausts memory on it) |
79
+ | [`wordcount`](wordcount/wordcount.js) | token-frequency counting into a plain object with 512 computed string keys over a skewed stream — the group-by/histogram kernel; a JIT drops the object to dictionary mode (the keyed-store deopt), while jz's string-keyed hash store + interning carry it |
80
+ | [`immutable`](immutable/immutable.js) | integer particle step in the immutable-update idiom — every step replaces each record with a fresh `{x,y,vx,vy}` (React/Redux-style state); value-semantics natives get it free, a JIT leans on young-gen GC + escape analysis, jz pays a bump allocation per object — the escape-analysis/SROA probe |
81
+ | [`colorconv`](colorconv/colorconv.js) | sRGB → Oklab over an image buffer — EOTF pow → 3×3 → cbrt → 3×3; lab probe for pow/cbrt intrinsic quality |
82
+ | [`colorlch`](colorlch/colorlch.js) | sRGB → OkLCh fused in one loop (pow → 3×3 → cbrt → 3×3 → sqrt + atan2); lab probe — the fused body must never lose to the split loops |
83
+ | [`colorlog`](colorlog/colorlog.js) | ARRI LogC4 → XYZ — runtime-base exp2 decode + 3×3; lab probe for runtime-exponent pow/exp2 |
84
+ | [`colorpq`](colorpq/colorpq.js) | sRGB → JzAzBz — 3×3 → ST 2084 PQ (≈12 signed-pow) → 3×3; lab probe for signed pow with non-constant exponent |
74
85
  | [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
75
86
  | [`jessie`](jessie/jessie.js) | the subscript/jessie JS parser over a realistic source corpus; branch-, allocation- and recursion-heavy front-end work |
76
87
  | [`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 |
77
88
 
78
- The `watr`, `jessie`, and `jz` rows are self-referential JZ (or its deps)
79
- compiling code. They stay runnable (`--cases=watr,jessie,jz`) and gated in
80
- `test/bench.js`, but are hidden from the bench page and the headline geomean SVG:
81
- they answer a different question (compiler throughput on itself) from the
82
- cross-language kernel comparison the page is about.
89
+ The **`lab` rows** the self-referential `watr`/`jessie`/`jz` (JZ or its deps
90
+ compiling code) and the JS-only intrinsic probes `colorconv`/`colorlch`/
91
+ `colorlog`/`colorpq` (pow/cbrt/exp2/atan2 gap trackers with no cross-language
92
+ ports; their transcendental checksums legitimately diverge per libm) answer
93
+ jz-internal questions, not the cross-language kernel comparison. They sit out
94
+ every aggregate (the geomean SVG, the page strip and geomeans, the aggregate
95
+ table below) but stay visible on the bench page under the `lab` chip and
96
+ runnable via `--cases=…`; the self-host rows stay gated in `test/bench.js`.
83
97
 
84
98
  Native rows for `json` are fixed-source references, not semantic equivalents
85
99
  of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
@@ -210,9 +224,9 @@ wasm-in-V8 would be a wrong-class single-case compare.
210
224
 
211
225
  | target | median | ×v8 | size | parity |
212
226
  | --- | ---: | ---: | ---: | --- |
213
- | **JZ → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
214
- | AssemblyScript (asc -O3 --runtime stub) | 6.51 ms | 1.38× | 1.9 kB | ok |
215
- | V8 (node) raw JS | 8.98 ms | 1.00× | 3.2 kB | ok |
227
+ | **JZ → V8 wasm** | **4.68 ms** | **1.94×** | **1.7 kB** | **ok** |
228
+ | AssemblyScript (asc -O3 --runtime stub) | 6.59 ms | 1.38× | 1.8 kB | ok |
229
+ | V8 (node) raw JS | 9.09 ms | 1.00× | 3.2 kB | ok |
216
230
  | hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
217
231
 
218
232
  JZ beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
@@ -223,9 +237,9 @@ that matches the hand-WAT floor.
223
237
 
224
238
  | target | median | ×v8 | size | parity |
225
239
  | --- | ---: | ---: | ---: | --- |
226
- | **JZ → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
227
- | AssemblyScript (asc -O3 --runtime stub) | 6.71 ms | 1.28× | 1.6 kB | ok |
228
- | V8 (node) raw JS | 8.57 ms | 1.00× | 1.2 kB | ok |
240
+ | **JZ → V8 wasm** | **0.77 ms** | **11.15×** | **1.5 kB** | **ok** |
241
+ | AssemblyScript (asc -O3 --runtime stub) | 6.72 ms | 1.28× | 1.4 kB | ok |
242
+ | V8 (node) raw JS | 8.60 ms | 1.00× | 1.2 kB | ok |
229
243
  | hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
230
244
 
231
245
  JZ is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
@@ -236,9 +250,9 @@ this from JS source.
236
250
 
237
251
  | target | median | ×v8 | size | parity |
238
252
  | --- | ---: | ---: | ---: | --- |
239
- | **JZ → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
240
- | AssemblyScript (asc -O3 --runtime stub) | 0.81 ms | 2.07× | 1.3 kB | ok |
241
- | V8 (node) raw JS | 1.67 ms | 1.00× | 1014 B | ok |
253
+ | **JZ → V8 wasm** | **0.13 ms** | **12.30×** | **1.0 kB** | **ok** |
254
+ | AssemblyScript (asc -O3 --runtime stub) | 0.79 ms | 2.06× | 1.3 kB | ok |
255
+ | V8 (node) raw JS | 1.64 ms | 1.00× | 1014 B | ok |
242
256
 
243
257
  JZ is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
244
258
  `sum` (called with both `Float64Array` and `Int32Array`) stays on typed
@@ -248,10 +262,10 @@ paths without falling back to generic dispatch.
248
262
 
249
263
  | target | median | ×v8 | size | parity |
250
264
  | --- | ---: | ---: | ---: | --- |
251
- | **JZ → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
252
- | V8 (node) raw JS | 3.87 ms | 1.00× | 1005 B | ok |
253
- | AssemblyScript (asc -O3 --runtime stub) | 9.15 ms | 0.42× | 1.5 kB | ok |
254
- | hand-WAT → V8 wasm | 3.59 ms | 1.11× | 355 B | ok |
265
+ | **JZ → V8 wasm** | **0.99 ms** | **3.81×** | **1.0 kB** | **ok** |
266
+ | V8 (node) raw JS | 3.77 ms | 1.00× | 1005 B | ok |
267
+ | AssemblyScript (asc -O3 --runtime stub) | 9.11 ms | 0.41× | 1.3 kB | ok |
268
+ | hand-WAT → V8 wasm | 3.49 ms | 1.08× | 355 B | ok |
255
269
 
256
270
  JZ is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
257
271
  (`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
@@ -261,9 +275,9 @@ overhead on every operation.
261
275
 
262
276
  | target | median | ×v8 | size | parity |
263
277
  | --- | ---: | ---: | ---: | --- |
264
- | **JZ → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
265
- | AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.15× | 1.6 kB | ok |
266
- | V8 (node) raw JS | 0.13 ms | 1.00× | 2.0 kB | ok |
278
+ | **JZ → V8 wasm** | **0.06 ms** | **2.16×** | **2.0 kB** | **ok** |
279
+ | AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.13× | 1.5 kB | ok |
280
+ | V8 (node) raw JS | 0.14 ms | 1.00× | 2.0 kB | ok |
267
281
 
268
282
  JZ is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
269
283
  `charCodeAt`-heavy scan. Both are well ahead of V8.
@@ -272,9 +286,9 @@ JZ is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
272
286
 
273
287
  | target | median | ×v8 | size | parity |
274
288
  | --- | ---: | ---: | ---: | --- |
275
- | **JZ → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
276
- | V8 (node) raw JS | 0.62 ms | 1.00× | 1.3 kB | ok |
277
- | AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.78× | 1.9 kB | ok |
289
+ | **JZ → V8 wasm** | **0.27 ms** | **2.25×** | **1.4 kB** | **ok** |
290
+ | V8 (node) raw JS | 0.60 ms | 1.00× | 1.3 kB | ok |
291
+ | AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.76× | 1.8 kB | ok |
278
292
 
279
293
  JZ is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
280
294
  `Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
@@ -284,9 +298,9 @@ V8's JIT does not inline the closure across the `map` boundary.
284
298
 
285
299
  | target | median | ×v8 | size | parity |
286
300
  | --- | ---: | ---: | ---: | --- |
287
- | **JZ → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
288
- | V8 (node) raw JS | 1.33 ms | 1.00× | 1.1 kB | ok |
289
- | AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.95× | 2.1 kB | ok |
301
+ | **JZ → V8 wasm** | **0.67 ms** | **2.00×** | **1.7 kB** | **ok** |
302
+ | V8 (node) raw JS | 1.34 ms | 1.00× | 1.1 kB | ok |
303
+ | AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.96× | 1.9 kB | ok |
290
304
 
291
305
  JZ is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
292
306
  reads are direct field offsets; the gap is small because the workload is
@@ -296,9 +310,9 @@ memory-bound.
296
310
 
297
311
  | target | median | ×v8 | size | parity |
298
312
  | --- | ---: | ---: | ---: | --- |
299
- | AssemblyScript (asc -O3 --runtime stub) | 8.90 ms | 1.11× | 1.3 kB | ok |
300
- | **JZ → V8 wasm** | **8.45 ms** | **1.17×** | **1.4 kB** | **ok** |
301
- | V8 (node) raw JS | 9.90 ms | 1.00× | 1.8 kB | ok |
313
+ | AssemblyScript (asc -O3 --runtime stub) | 8.85 ms | 1.11× | 1.3 kB | ok |
314
+ | **JZ → V8 wasm** | **8.45 ms** | **1.16×** | **1.1 kB** | **ok** |
315
+ | V8 (node) raw JS | 9.83 ms | 1.00× | 1.8 kB | ok |
302
316
 
303
317
  JZ is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
304
318
  conditional break compacts to 1.0 kB — the smallest wasm in the suite.
@@ -307,7 +321,7 @@ conditional break compacts to 1.0 kB — the smallest wasm in the suite.
307
321
 
308
322
  | target | median | ×v8 | size | parity |
309
323
  | --- | ---: | ---: | ---: | --- |
310
- | **JZ → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
324
+ | **JZ → V8 wasm** | **0.22 ms** | **1.22×** | **9.7 kB** | **ok** |
311
325
  | V8 (node) raw JS | 0.27 ms | 1.00× | 1.2 kB | ok |
312
326
 
313
327
  JZ is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
@@ -317,9 +331,9 @@ inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
317
331
 
318
332
  | target | median | ×v8 | size | parity |
319
333
  | --- | ---: | ---: | ---: | --- |
320
- | **JZ → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
321
- | AssemblyScript (asc -O3 --runtime stub) | 7.96 ms | 0.98× | 1.9 kB | ok |
322
- | V8 (node) raw JS | 7.79 ms | 1.00× | 1.6 kB | ok |
334
+ | **JZ → V8 wasm** | **5.41 ms** | **1.64×** | **1.6 kB** | **ok** |
335
+ | AssemblyScript (asc -O3 --runtime stub) | 7.93 ms | 1.12× | 1.8 kB | ok |
336
+ | V8 (node) raw JS | 8.87 ms | 1.00× | 1.6 kB | ok |
323
337
 
324
338
  JZ is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
325
339
  nested loops with typed-array index propagation stay on the i32 path.
@@ -328,9 +342,9 @@ nested loops with typed-array index propagation stay on the i32 path.
328
342
 
329
343
  | target | median | ×v8 | size | parity |
330
344
  | --- | ---: | ---: | ---: | --- |
331
- | **JZ → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
332
- | AssemblyScript (asc -O3 --runtime stub) | 8.76 ms | 1.11× | 1.4 kB | ok |
333
- | V8 (node) raw JS | 9.71 ms | 1.00× | 1.8 kB | ok |
345
+ | **JZ → V8 wasm** | **8.62 ms** | **1.12×** | **1.0 kB** | **ok** |
346
+ | AssemblyScript (asc -O3 --runtime stub) | 8.74 ms | 1.10× | 1.3 kB | ok |
347
+ | V8 (node) raw JS | 9.62 ms | 1.00× | 1.8 kB | ok |
334
348
 
335
349
  JZ is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
336
350
  typed-array parameter propagation keep the LUT lookup on raw i32.
@@ -419,12 +433,64 @@ dependent-gather / branchy-DP** kernels, not in dense scalar or dispatch loops.
419
433
  Like the batch above, these stay bench-only (out of `test/bench.js`) until the gap
420
434
  closes.
421
435
 
436
+ ### Deopt kernels — dispatch, shapes, strings, maps, allocation
437
+
438
+ Five cases aimed square at the *deoptimization* shapes — the dynamic patterns
439
+ that make a JIT bail out of optimized code, and that an AOT compiler must
440
+ instead resolve statically. All are bit-identical across every target and
441
+ data-shuffled, so no branch predictor, inline cache, or devirtualizer can
442
+ settle on a fast path. Together they cover the dynamic-JS surface jz commits to
443
+ compile well: **calls** (`dispatch` — one call site fans out to a table of 8
444
+ first-class functions, the megamorphic call-IC shape), **hidden classes**
445
+ (`shapes` — records of 8 heterogeneous object shapes at one access site, the
446
+ megamorphic load-IC shape), **string churn** (`strbuild` — per-record
447
+ int→string + concat, the serializer inner loop), **keyed maps** (`wordcount` —
448
+ counting into a plain object with 512 computed string keys, the
449
+ dictionary-mode keyed-store deopt), and **allocation** (`immutable` — a fresh
450
+ `{x,y,vx,vy}` per particle per step, the React/Redux-style immutable update).
451
+ The static rivals write what a static language writes: fn-pointer tables,
452
+ tagged unions, stack-buffer formatting, string hash maps, value-type structs.
453
+
454
+ | case | JZ | vs V8 | vs AS | vs native C | what it probes |
455
+ | --- | ---: | ---: | ---: | ---: | --- |
456
+ | **dispatch** — fn-table dispatch | **10.4 ms** | 0.84× | **1.23×** | 0.46× | `call_indirect` vs megamorphic call IC |
457
+ | **strbuild** — per-record formatting | 1.83 ms | 0.92× | 0.83× | 0.83× | int→string + concat temporaries |
458
+ | **wordcount** — string-keyed counting | 8.80 ms | 0.30× | 0.93× | 0.28× | dynamic keys vs dictionary-mode IC |
459
+ | **immutable** — fresh-object step | 1.32 ms | 0.33× | 0.52× | 0.06× | escape analysis / allocation churn |
460
+ | **shapes** — 8-shape record scan | 18.1 ms | 0.19× | 0.06× | 0.09× | schema-union access vs megamorphic load IC |
461
+
462
+ The probes split jz's dynamic story cleanly. On `dispatch` **jz leads the
463
+ systems-language wasm field** — AS 1.23×, Rust/C/Zig/Go→wasm 1.05–1.2× — its
464
+ data-selected `call_indirect` is already tighter than what the systems
465
+ languages ship through the same table (only MoonBit's moonrun row edges it);
466
+ the remaining field is JIT call machinery — V8 0.84×, and JavaScriptCore's
467
+ call ICs win the case outright at ~2.3 ms — with bounded-table devirt (guarded
468
+ direct calls over the 8 known closures) the lever to close it. `strbuild` is
469
+ near-parity with V8 but rust-wasm leads 1.3× (and zig-wasm's stack-buffer
470
+ formatting 4.5×) — per-temporary `__str_concat` allocation and generic
471
+ `__itoa` are the levers. (The giant `out +=` accumulator variant is
472
+ deliberately excluded: eager no-GC strings make it quadratic in allocation —
473
+ jz exhausts memory on it. A real landmine for jz users, documented in the
474
+ case header.) `wordcount` ties AS's Map while V8's dictionary mode runs 3.3×
475
+ ahead and a plain C strcmp table 10× — per-op string hashing is the gap,
476
+ interning the lever. `immutable` trails every rival — jz bump-allocates each
477
+ escaping object with no scalar replacement and no reclamation, where V8's
478
+ young-gen GC recycles and value-semantics natives never allocate at all.
479
+ `shapes` stays **the widest gap in the suite, by design**: the 8-schema union
480
+ defeats schema-slot inference, so every field read lowers through the
481
+ `__dyn_get` dynamic-property probe — ~5× behind V8's megamorphic IC and ~16×
482
+ behind AS's kind-tagged flat struct (which ties native C); the general fix is
483
+ shape-set devirt — a bounded schema union lowering to a tag-switch over direct
484
+ slot loads, the static mirror of a polymorphic IC. `dispatch` passes the
485
+ fastest-wasm gate; `shapes`, `wordcount`, `immutable`, and `strbuild` are
486
+ pinned red in `WASM_TODO` — the deopt work list, loudest first.
487
+
422
488
  ### watr — WAT-to-wasm compiler on small corpus
423
489
 
424
490
  | target | median | ×v8 | size | parity |
425
491
  | --- | ---: | ---: | ---: | --- |
426
- | V8 (node) raw JS | 1.38 ms | 1.00× | 2.6 kB | ok |
427
- | **JZ → V8 wasm** | **1.17 ms** | **1.17×** | **238.4 kB** | **ok** |
492
+ | V8 (node) raw JS | 0.97 ms | 1.00× | 2.6 kB | ok |
493
+ | **JZ → V8 wasm** | **0.96 ms** | **1.00×** | **244.3 kB** | **DIFF** |
428
494
 
429
495
  JZ is 1.07× slower than V8 raw JS on this large compiler bundle. The size
430
496
  (144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
@@ -436,19 +502,24 @@ Aggregate geomean (JZ / target):
436
502
 
437
503
  | target | speed | size |
438
504
  | --- | ---: | ---: |
439
- | V8 (node) | **0.40×** | — |
440
- | AssemblyScript | **0.36×** | **1.16×** |
505
+ | V8 (node) | **0.53×** | — |
506
+ | AssemblyScript | **0.58×** | **1.06×** |
441
507
 
442
- JZ wins or ties V8 on every kernel case; the only V8 losses are the
443
- self-hosting rows `watr` (1.07×) and `jessie` (1.28×). AS is beaten on
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.
508
+ JZ wins or ties V8 on every dense kernel case; the open V8 losses are the
509
+ self-host lab rows (`watr`, `jessie`) and the deliberate deopt probes above
510
+ (`dispatch`, `shapes`, `wordcount`, `immutable`, `strbuild` the dynamic-JS
511
+ work list). AS is beaten on speed across the shared cases except the tracked
512
+ gather/probe gaps (`dict`, `noise`, `levenshtein`) and the deopt probes
513
+ (`shapes`, `immutable`, `strbuild`; `wordcount` is a tie). On size JZ matches
514
+ AS (geomean ~1.0×) — JZ wins on speed and holds size parity.
446
515
 
447
516
  Against the systems languages compiled to the same target — **WebAssembly, run in
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.
517
+ V8** — JZ is **1.6× faster than C, 1.7× than Rust, and 3.4× than Go** (geomean,
518
+ deopt probes included). That apples-to-apples wasm field is the headline chart
519
+ above. Native `clang -O3` — the lone non-wasm reference row, the speed-of-light
520
+ ceiling, never a per-case beat-claim — now leads the corpus geomean 1.2×: the
521
+ deopt probes are exactly where native value semantics and mature runtimes pull
522
+ ahead, and closing them is the current work list.
452
523
 
453
524
  Case-by-case summary:
454
525
 
package/bench/bench.svg CHANGED
@@ -1,14 +1,14 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 424" width="720" height="424" style="color-scheme:light dark" role="img" aria-label="JZ benchmark — every rival compiled to WebAssembly, run in V8 · native C = reference; geometric mean across 38 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">
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 424" width="720" height="424" style="color-scheme:light dark" role="img" aria-label="JZ benchmark — every rival compiled to WebAssembly, run in V8 · native C = reference; geometric mean across 45 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
2
 
3
3
  <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
4
4
  <rect x="174" y="43.5" width="476" height="3" rx="1.5" fill="currentColor" fill-opacity="0.1"/>
5
5
  <line x1="174" y1="38" x2="174" y2="52" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
6
6
  <line x1="650" y1="38" x2="650" y2="52" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
7
- <text x="156" y="41" text-anchor="end" font-size="14" font-weight="700" fill="currentColor" fill-opacity="1">JZ</text>
8
- <text x="156" y="56" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">-O3</text>
9
- <text x="662" y="49" font-size="13" font-weight="700" fill="currentColor" fill-opacity="1">1.00×</text>
10
- <circle cx="182.0" cy="45" r="8" fill="currentColor" fill-opacity="1">
11
- <animate attributeName="cx" dur="1.6s" repeatCount="indefinite" calcMode="linear"
7
+ <text x="156" y="41" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">native C</text>
8
+ <text x="156" y="56" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">clang -O3 · ref</text>
9
+ <text x="662" y="49" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">0.82×</text>
10
+ <circle cx="182.0" cy="45" r="8" fill="currentColor" fill-opacity="0.4">
11
+ <animate attributeName="cx" dur="1.3125489894306868s" repeatCount="indefinite" calcMode="linear"
12
12
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.00s"/>
13
13
  </circle>
14
14
  </g>
@@ -16,11 +16,11 @@
16
16
  <rect x="174" y="93.5" width="476" height="3" rx="1.5" fill="currentColor" fill-opacity="0.1"/>
17
17
  <line x1="174" y1="88" x2="174" y2="102" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
18
18
  <line x1="650" y1="88" x2="650" y2="102" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
19
- <text x="156" y="91" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">native C</text>
20
- <text x="156" y="106" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">clang -O3 · ref</text>
21
- <text x="662" y="99" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">1.01×</text>
22
- <circle cx="182.0" cy="95" r="8" fill="currentColor" fill-opacity="0.4">
23
- <animate attributeName="cx" dur="1.6171275085641825s" repeatCount="indefinite" calcMode="linear"
19
+ <text x="156" y="91" text-anchor="end" font-size="14" font-weight="700" fill="currentColor" fill-opacity="1">JZ</text>
20
+ <text x="156" y="106" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">-O3</text>
21
+ <text x="662" y="99" font-size="13" font-weight="700" fill="currentColor" fill-opacity="1">1.00×</text>
22
+ <circle cx="182.0" cy="95" r="8" fill="currentColor" fill-opacity="1">
23
+ <animate attributeName="cx" dur="1.6s" repeatCount="indefinite" calcMode="linear"
24
24
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.41s"/>
25
25
  </circle>
26
26
  </g>
@@ -30,9 +30,9 @@
30
30
  <line x1="650" y1="138" x2="650" y2="152" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
31
31
  <text x="156" y="141" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">C</text>
32
32
  <text x="156" y="156" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">clang → wasm</text>
33
- <text x="662" y="149" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">2.01×</text>
33
+ <text x="662" y="149" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">1.64×</text>
34
34
  <circle cx="182.0" cy="145" r="8" fill="currentColor" fill-opacity="0.4">
35
- <animate attributeName="cx" dur="3.214848718379708s" repeatCount="indefinite" calcMode="linear"
35
+ <animate attributeName="cx" dur="2.624757929306874s" repeatCount="indefinite" calcMode="linear"
36
36
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-0.82s"/>
37
37
  </circle>
38
38
  </g>
@@ -40,11 +40,11 @@
40
40
  <rect x="174" y="193.5" width="476" height="3" rx="1.5" fill="currentColor" fill-opacity="0.1"/>
41
41
  <line x1="174" y1="188" x2="174" y2="202" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
42
42
  <line x1="650" y1="188" x2="650" y2="202" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
43
- <text x="156" y="191" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">Rust</text>
44
- <text x="156" y="206" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">rustc → wasm</text>
45
- <text x="662" y="199" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">2.05×</text>
43
+ <text x="156" y="191" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">AssemblyScript</text>
44
+ <text x="156" y="206" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">asc -O3</text>
45
+ <text x="662" y="199" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">1.73×</text>
46
46
  <circle cx="182.0" cy="195" r="8" fill="currentColor" fill-opacity="0.4">
47
- <animate attributeName="cx" dur="3.2852220400953436s" repeatCount="indefinite" calcMode="linear"
47
+ <animate attributeName="cx" dur="2.7633688737454363s" repeatCount="indefinite" calcMode="linear"
48
48
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-1.23s"/>
49
49
  </circle>
50
50
  </g>
@@ -52,11 +52,11 @@
52
52
  <rect x="174" y="243.5" width="476" height="3" rx="1.5" fill="currentColor" fill-opacity="0.1"/>
53
53
  <line x1="174" y1="238" x2="174" y2="252" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
54
54
  <line x1="650" y1="238" x2="650" y2="252" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
55
- <text x="156" y="241" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">AssemblyScript</text>
56
- <text x="156" y="256" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">asc -O3</text>
57
- <text x="662" y="249" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">2.10×</text>
55
+ <text x="156" y="241" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">Rust</text>
56
+ <text x="156" y="256" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">rustc → wasm</text>
57
+ <text x="662" y="249" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">1.73×</text>
58
58
  <circle cx="182.0" cy="245" r="8" fill="currentColor" fill-opacity="0.4">
59
- <animate attributeName="cx" dur="3.3536253223690404s" repeatCount="indefinite" calcMode="linear"
59
+ <animate attributeName="cx" dur="2.77333545752851s" repeatCount="indefinite" calcMode="linear"
60
60
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-1.64s"/>
61
61
  </circle>
62
62
  </g>
@@ -66,9 +66,9 @@
66
66
  <line x1="650" y1="288" x2="650" y2="302" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
67
67
  <text x="156" y="291" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">V8</text>
68
68
  <text x="156" y="306" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">Node (JS)</text>
69
- <text x="662" y="299" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">2.33×</text>
69
+ <text x="662" y="299" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">1.90×</text>
70
70
  <circle cx="182.0" cy="295" r="8" fill="currentColor" fill-opacity="0.4">
71
- <animate attributeName="cx" dur="3.72980153913452s" repeatCount="indefinite" calcMode="linear"
71
+ <animate attributeName="cx" dur="3.0407840012463723s" repeatCount="indefinite" calcMode="linear"
72
72
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-2.05s"/>
73
73
  </circle>
74
74
  </g>
@@ -77,13 +77,13 @@
77
77
  <line x1="174" y1="338" x2="174" y2="352" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
78
78
  <line x1="650" y1="338" x2="650" y2="352" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>
79
79
  <text x="156" y="341" text-anchor="end" font-size="14" font-weight="500" fill="currentColor" fill-opacity="0.82">Porffor</text>
80
- <text x="156" y="356" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">runs 4 / 38</text>
81
- <text x="662" y="349" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">3.76×</text>
80
+ <text x="156" y="356" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.46">runs 4 / 45</text>
81
+ <text x="662" y="349" font-size="13" font-weight="500" fill="currentColor" fill-opacity="0.6">3.27×</text>
82
82
  <circle cx="182.0" cy="345" r="8" fill="currentColor" fill-opacity="0.4">
83
- <animate attributeName="cx" dur="6.023170795143817s" repeatCount="indefinite" calcMode="linear"
83
+ <animate attributeName="cx" dur="5.225740832087161s" repeatCount="indefinite" calcMode="linear"
84
84
  keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-2.46s"/>
85
85
  </circle>
86
86
  </g>
87
87
  <text x="360" y="390" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="11" font-weight="600" fill="currentColor" fill-opacity="0.72">every rival compiled to WebAssembly, run in V8 · native C = reference</text>
88
- <text x="360" y="408" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="11" fill="currentColor" fill-opacity="0.5">geometric mean across 38 benchmark cases · lower is faster, JZ = 1.00× baseline</text>
88
+ <text x="360" y="408" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="11" fill="currentColor" fill-opacity="0.5">geometric mean across 45 benchmark cases · lower is faster, JZ = 1.00× baseline</text>
89
89
  </svg>
package/cli.js CHANGED
@@ -132,7 +132,9 @@ async function handleJzify(args) {
132
132
  if (!inputFile) throw new Error('No input file specified')
133
133
  if (!outputFile) outputFile = inputFile.replace(/\.js$/, '.jz')
134
134
  const code = readFileSync(inputFile, 'utf8')
135
- const out = transform(code) + '\n'
135
+ const warnings = { entries: [] }
136
+ const out = transform(code, { warnings }) + '\n'
137
+ for (const w of warnings.entries) console.warn(formatWarning(w))
136
138
  if (outputFile === '-') {
137
139
  process.stdout.write(out)
138
140
  } else {