jz 0.5.1 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,319 @@
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>.py optional scalar CPython baseline
14
+ bench/<case>/<case>.npy.py optional NumPy baseline
15
+ bench/<case>/<case>.wat optional hand-written WAT baseline
16
+ ```
17
+
18
+ Every case prints the same line:
19
+
20
+ ```txt
21
+ median_us=<int> checksum=<u32> samples=<int> stages=<int> runs=<int>
22
+ ```
23
+
24
+ The orchestrator runs selected cases against selected targets and flags checksum
25
+ drift as `DIFF`.
26
+
27
+ ## Run
28
+
29
+ ```sh
30
+ npm run bench
31
+ node bench/bench.mjs --targets=nat,rust,go,numpy,v8,jz
32
+ node bench/bench.mjs --targets=jz --cases=biquad,mat4,poly,bitwise
33
+ node bench/bench.mjs --targets=v8,deno,bun,spidermonkey,hermes,graaljs,qjs
34
+ node bench/bench.mjs --cases=biquad,mat4,tokenizer,json,sort,crc32
35
+ node bench/bench.mjs biquad
36
+ node bench/bench.mjs mat4 --targets=nat,v8,jz
37
+ ```
38
+
39
+ ## Cases
40
+
41
+ | id | purpose |
42
+ | --- | --- |
43
+ | [`biquad`](biquad/biquad.js) | DSP filter cascade; dense f64 typed-array loop and offset-fusion baseline |
44
+ | [`mat4`](mat4/mat4.js) | fixed-size typed-array loops; exposes loop unrolling and offset fusion gaps |
45
+ | [`poly`](poly/poly.js) | same `sum` called with `Float64Array` and `Int32Array`; exposes bimorphic typed-array dispatch |
46
+ | [`bitwise`](bitwise/bitwise.js) | long `i32` narrowing chains with `Math.imul`, shifts, and unsigned conversion |
47
+ | [`tokenizer`](tokenizer/tokenizer.js) | string-heavy scan with `charCodeAt`, branches, and integer token accumulation |
48
+ | [`callback`](callback/callback.js) | `Array.map` callback path; exposes closure/call-indirect and array allocation cost |
49
+ | [`aos`](aos/aos.js) | array-of-object rows copied into typed arrays; exposes schema-slot read cost |
50
+ | [`mandelbrot`](mandelbrot/mandelbrot.js) | 256×256 escape-time iteration; dense f64 hot loop with conditional break and i32 counter |
51
+ | [`json`](json/json.js) | runtime `JSON.parse` of one module-local source plus heterogeneous object/array walk with a stable inferred JSON shape |
52
+ | [`sort`](sort/sort.js) | in-place heapsort over a typed array; exposes call-heavy nested loops and typed-array index propagation |
53
+ | [`crc32`](crc32/crc32.js) | table-driven CRC-32 over a mutable byte buffer; exposes integer narrowing and typed-array parameter propagation |
54
+ | [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
55
+ | [`jessie`](jessie/jessie.js) | the subscript/jessie JS parser over a realistic source corpus; branch-, allocation- and recursion-heavy front-end work |
56
+ | [`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 |
57
+
58
+ Native rows for `json` are fixed-source references, not semantic equivalents
59
+ of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
60
+ compile-time string, and Zig may constant-fold the whole parse+walk under
61
+ ReleaseFast; Go uses `encoding/json` but still unmarshals the same compile-time
62
+ string. The jz row parses a `let` source at runtime so `JSON.parse` is not
63
+ compile-time folded, while the compiler can still specialize the stable literal
64
+ shape. External unknown-shape JSON still uses the generic runtime parser.
65
+
66
+ Native-language rows are intentionally per case. NumPy rows are used only
67
+ where a vectorized array implementation is a meaningful Python convention;
68
+ scalar CPython is kept to the tokenizer row to avoid turning the suite into
69
+ a Python loop-overhead benchmark.
70
+
71
+ ### Parity classes
72
+
73
+ The `parity` column is `ok` when the run's checksum matches the most common
74
+ checksum across all targets, `DIFF` when it diverges in a way that suggests
75
+ a bug, and `fma` when the divergence is the documented FMA-fusion class.
76
+ The Go arm64 backend auto-fuses `a*b + c` to `FMADDD` (mandatory in ARMv8,
77
+ no compiler flag to disable it), which alters bit-level rounding on
78
+ recurrence-style loops like `biquad`. Result is still IEEE-754
79
+ correctly-rounded; cascade is the same algorithm.
80
+
81
+ ## Targets
82
+
83
+ | id | what it measures |
84
+ | --- | --- |
85
+ | `nat` | clang `-O3` native C baseline, when a matching C workload exists |
86
+ | `natgcc` | gcc `-O3`, when real gcc is installed |
87
+ | `rust` | Rust `rustc -C opt-level=3 -C target-cpu=native`, when a matching `.rs` exists |
88
+ | `go` | Go native compiler, when a matching `.go` exists |
89
+ | `zig` | Zig `build-exe -O ReleaseFast`, when a matching `.zig` exists |
90
+ | `python` | scalar CPython, when a matching `.py` exists |
91
+ | `numpy` | vectorized NumPy, when a matching `.npy.py` exists |
92
+ | `v8` | raw JavaScript on Node/V8 |
93
+ | `deno` | raw JavaScript on Deno/V8 |
94
+ | `bun` | raw JavaScript on Bun/JavaScriptCore |
95
+ | `spidermonkey` | raw JavaScript on SpiderMonkey shell (`spidermonkey`, `sm`, `js128`, `js115`, `js102`, or `js`) |
96
+ | `hermes` | raw JavaScript on Hermes |
97
+ | `shermes` | Static Hermes — JS AOT-compiled to a native binary (`shermes -O`), when installed |
98
+ | `graaljs` | raw JavaScript on GraalJS |
99
+ | `jz` | jz output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
100
+ | `as` | AssemblyScript `asc -O3 --runtime stub`, when a matching `.as.ts` exists |
101
+ | `jz-wasmtime` | jz output on wasmtime |
102
+ | `jz-w2c` | jz wasm translated by wabt `wasm2c`, then clang `-O3` |
103
+ | `wat` | hand-written WAT baseline when a case provides `run-wat.mjs` |
104
+ | `qjs` | QuickJS when installed |
105
+ | `porf` | Porffor (`porf run`) when installed |
106
+ | `jawsm` | jawsm when installed |
107
+
108
+ The `size` column reports the artifact size each target measures: the
109
+ compiled native binary for `nat`/`rust`/`go`/`zig`, the produced
110
+ `.wasm` for `jz`/`as`/hand-WAT/jawsm/`jz-w2c` (the C-translated
111
+ executable), or the source file for raw-JS interpreters where there is no
112
+ compile step. For source files with imports, raw-JS size is only the entry file;
113
+ jz size is the bundled wasm artifact.
114
+
115
+ Note the preset trade: these speed tables build jz at the default
116
+ (`optimize: 2`, speed-leaning — loop unroll + SIMD lift), while the dedicated
117
+ size comparison (`npm run bench:size`) builds with `optimize: 'size'` —
118
+ typically ~2× smaller on the same kernel (biquad: 3.5 kB default vs 1.6 kB
119
+ size-tuned). Pick the preset for what you ship; the two tables are not the
120
+ same artifact.
121
+
122
+ Runtime command overrides:
123
+
124
+ `watr` is intentionally compiled by jz with a size-oriented pass config
125
+ (`watr:false`, `smallConstForUnroll:false`): on a large compiler bundle, the
126
+ default WAT-level optimizer and small-loop unroll grow code more than they help.
127
+ This keeps the target measuring the best current jz artifact for that workload.
128
+
129
+ ```sh
130
+ BUN_BIN=/path/to/bun \
131
+ DENO_BIN=/path/to/deno \
132
+ SPIDERMONKEY_BIN=/path/to/js \
133
+ HERMES_BIN=/path/to/hermes \
134
+ SHERMES_BIN=/path/to/shermes \
135
+ GRAALJS_BIN=/path/to/graaljs \
136
+ PORF_BIN=/path/to/porf \
137
+ node bench/bench.mjs --targets=bun,deno,spidermonkey,hermes,shermes,graaljs,porf
138
+ ```
139
+
140
+ ## Reading the numbers (darwin/arm64, M-class)
141
+
142
+ Snapshots from `node bench/bench.mjs --targets=v8,jz,as`.
143
+ Where jz lands relative to **V8 raw JS** (`v8/node`) and **AssemblyScript**
144
+ (`as`) is the headline comparison. Native and hand-WAT rows are shown where
145
+ available from earlier runs.
146
+
147
+ ### biquad — f64 typed-array DSP cascade
148
+
149
+ | target | median | ×v8 | size | parity |
150
+ | --- | ---: | ---: | ---: | --- |
151
+ | **jz → V8 wasm** | **6.50 ms** | **1.90×** | **3.4 kB** | **ok** |
152
+ | AssemblyScript (asc -O3 --runtime stub) | 9.03 ms | 1.38× | 1.9 kB | ok |
153
+ | V8 (node) raw JS | 12.35 ms | 1.00× | 3.2 kB | ok |
154
+ | native C (clang -O3) | 5.32 ms | 2.32× | 32.7 kB | ok |
155
+ | hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
156
+
157
+ jz beats V8 raw JS by 1.9× and AS by 1.4×. The typed-array scalarization,
158
+ offset-fusion, and base-hoisting pipeline delivers dense-f64 loop codegen
159
+ that matches the hand-WAT floor.
160
+
161
+ ### mat4 — fixed-size Float64Array multiply
162
+
163
+ | target | median | ×v8 | size | parity |
164
+ | --- | ---: | ---: | ---: | --- |
165
+ | **jz → V8 wasm** | **2.74 ms** | **4.37×** | **3.3 kB** | **ok** |
166
+ | AssemblyScript (asc -O3 --runtime stub) | 9.32 ms | 1.28× | 1.6 kB | ok |
167
+ | V8 (node) raw JS | 11.96 ms | 1.00× | 1.2 kB | ok |
168
+ | native C (clang -O3) | 2.76 ms | 4.33× | 32.8 kB | ok |
169
+ | hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
170
+
171
+ jz is 4.4× faster than V8 raw JS and 3.4× faster than AS. The scalarized
172
+ SIMD hot path (unrolled 4×4 multiply) is the win; V8's JIT doesn't vectorize
173
+ this from JS source.
174
+
175
+ ### poly — bimorphic typed-array reduce
176
+
177
+ | target | median | ×v8 | size | parity |
178
+ | --- | ---: | ---: | ---: | --- |
179
+ | **jz → V8 wasm** | **0.37 ms** | **6.22×** | **1.2 kB** | **ok** |
180
+ | AssemblyScript (asc -O3 --runtime stub) | 1.15 ms | 2.02× | 1.3 kB | ok |
181
+ | V8 (node) raw JS | 2.32 ms | 1.00× | 1014 B | ok |
182
+
183
+ jz is 6.2× faster than V8 raw JS and 3.1× faster than AS. The bimorphic
184
+ `sum` (called with both `Float64Array` and `Int32Array`) stays on typed
185
+ paths without falling back to generic dispatch.
186
+
187
+ ### bitwise — i32 narrowing chains (`Math.imul`, shifts, FNV-1a)
188
+
189
+ | target | median | ×v8 | size | parity |
190
+ | --- | ---: | ---: | ---: | --- |
191
+ | **jz → V8 wasm** | **1.40 ms** | **3.81×** | **1.2 kB** | **ok** |
192
+ | V8 (node) raw JS | 5.32 ms | 1.00× | 1005 B | ok |
193
+ | AssemblyScript (asc -O3 --runtime stub) | 12.13 ms | 0.44× | 1.5 kB | ok |
194
+ | native C (clang -O3) | 1.31 ms | 4.06× | 32.9 kB | ok |
195
+ | hand-WAT → V8 wasm | 4.88 ms | 1.09× | 355 B | ok |
196
+
197
+ jz is 3.8× faster than V8 raw JS and 8.7× faster than AS. The i32 hot path
198
+ (`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
199
+ overhead on every operation.
200
+
201
+ ### tokenizer — string scan with `charCodeAt` and integer accumulation
202
+
203
+ | target | median | ×v8 | size | parity |
204
+ | --- | ---: | ---: | ---: | --- |
205
+ | AssemblyScript (asc -O3 --runtime stub) | 0.08 ms | 2.63× | 1.6 kB | ok |
206
+ | **jz → V8 wasm** | **0.10 ms** | **2.03×** | **1.7 kB** | **ok** |
207
+ | V8 (node) raw JS | 0.21 ms | 1.00× | 2.0 kB | ok |
208
+
209
+ jz is 2.0× faster than V8 raw JS. AS wins here by 1.3× because its
210
+ `String#charCodeAt` is a direct memory load without NaN-box decode, while
211
+ jz still boxes the string pointer. The gap is narrow (0.02 ms) and both
212
+ are well ahead of V8.
213
+
214
+ ### callback — `Array.map` closure + i32 fold
215
+
216
+ | target | median | ×v8 | size | parity |
217
+ | --- | ---: | ---: | ---: | --- |
218
+ | **jz → V8 wasm** | **0.03 ms** | **27.56×** | **1.4 kB** | **ok** |
219
+ | AssemblyScript (asc -O3 --runtime stub) | 1.49 ms | 0.59× | 1.9 kB | ok |
220
+ | V8 (node) raw JS | 0.88 ms | 1.00× | 828 B | ok |
221
+
222
+ jz is 27.6× faster than V8 raw JS and 49.7× faster than AS. Closure +
223
+ `Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
224
+ V8's JIT does not inline the closure across the `map` boundary.
225
+
226
+ ### aos — array-of-object rows to typed arrays
227
+
228
+ | target | median | ×v8 | size | parity |
229
+ | --- | ---: | ---: | ---: | --- |
230
+ | **jz → V8 wasm** | **1.62 ms** | **1.12×** | **1.8 kB** | **ok** |
231
+ | V8 (node) raw JS | 1.82 ms | 1.00× | 1.1 kB | ok |
232
+ | AssemblyScript (asc -O3 --runtime stub) | 1.91 ms | 0.95× | 2.2 kB | ok |
233
+
234
+ jz is 1.1× faster than V8 raw JS and 1.2× faster than AS. Schema-slot
235
+ reads are direct field offsets; the gap is small because the workload is
236
+ memory-bound.
237
+
238
+ ### mandelbrot — 256×256 escape-time iteration
239
+
240
+ | target | median | ×v8 | size | parity |
241
+ | --- | ---: | ---: | ---: | --- |
242
+ | AssemblyScript (asc -O3 --runtime stub) | 12.42 ms | 1.11× | 1.3 kB | ok |
243
+ | **jz → V8 wasm** | **12.55 ms** | **1.10×** | **1.0 kB** | **ok** |
244
+ | V8 (node) raw JS | 13.80 ms | 1.00× | 1.8 kB | ok |
245
+
246
+ jz is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
247
+ conditional break compacts to 1.0 kB — the smallest wasm in the suite.
248
+
249
+ ### json — runtime `JSON.parse` plus stable-shape walk
250
+
251
+ | target | median | ×v8 | size | parity |
252
+ | --- | ---: | ---: | ---: | --- |
253
+ | **jz → V8 wasm** | **0.23 ms** | **1.65×** | **7.7 kB** | **ok** |
254
+ | V8 (node) raw JS | 0.38 ms | 1.00× | 1.2 kB | ok |
255
+
256
+ jz is 1.7× faster than V8 raw JS. The runtime parser is specialized to the
257
+ inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
258
+
259
+ ### sort — in-place heapsort over typed array
260
+
261
+ | target | median | ×v8 | size | parity |
262
+ | --- | ---: | ---: | ---: | --- |
263
+ | **jz → V8 wasm** | **5.96 ms** | **1.87×** | **1.6 kB** | **ok** |
264
+ | AssemblyScript (asc -O3 --runtime stub) | 10.22 ms | 1.09× | 1.9 kB | ok |
265
+ | V8 (node) raw JS | 11.13 ms | 1.00× | 1.6 kB | ok |
266
+
267
+ jz is 1.9× faster than V8 raw JS and 1.7× faster than AS. Call-heavy
268
+ nested loops with typed-array index propagation stay on the i32 path.
269
+
270
+ ### crc32 — table-driven CRC-32 over byte buffer
271
+
272
+ | target | median | ×v8 | size | parity |
273
+ | --- | ---: | ---: | ---: | --- |
274
+ | **jz → V8 wasm** | **12.12 ms** | **1.11×** | **1.2 kB** | **ok** |
275
+ | AssemblyScript (asc -O3 --runtime stub) | 12.19 ms | 1.10× | 1.4 kB | ok |
276
+ | V8 (node) raw JS | 13.43 ms | 1.00× | 1.8 kB | ok |
277
+
278
+ jz is 1.1× faster than V8 raw JS and ties AS. Integer narrowing and
279
+ typed-array parameter propagation keep the LUT lookup on raw i32.
280
+
281
+ ### watr — WAT-to-wasm compiler on small corpus
282
+
283
+ | target | median | ×v8 | size | parity |
284
+ | --- | ---: | ---: | ---: | --- |
285
+ | V8 (node) raw JS | 1.45 ms | 1.00× | 2.6 kB | ok |
286
+ | **jz → V8 wasm** | **1.56 ms** | **1.07×** | **144.4 kB** | **ok** |
287
+
288
+ jz is 1.07× slower than V8 raw JS on this large compiler bundle. The size
289
+ (144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
290
+ has the advantage of profile-guided tiering on a long-running compiler.
291
+
292
+ ### Where the gaps live
293
+
294
+ Aggregate geomean (jz / target):
295
+
296
+ | target | speed | size |
297
+ | --- | ---: | ---: |
298
+ | V8 (node) | **0.41×** | — |
299
+ | AssemblyScript | **0.40×** | **0.85×** |
300
+ | Porffor | **0.32×** | **0.04×** |
301
+ | wasm-opt slack | — | **0.91×** |
302
+
303
+ jz wins or ties V8 on every case except `watr` (1.07×). AS is beaten on
304
+ all cases except `tokenizer` (AS 0.08 ms vs jz 0.10 ms). The size geomean
305
+ is 0.85× AS — jz output is smaller on average despite beating AS on speed.
306
+
307
+ Case-by-case summary:
308
+
309
+ * **biquad, mat4, poly, bitwise, callback: large wins.** jz beats V8 by
310
+ 1.9–27.6× and AS by 1.4–49.7×. Typed-array scalarization, i32 narrowing,
311
+ and closure lowering are the drivers.
312
+ * **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** jz beats V8 by
313
+ 1.1–2.0× and ties or beats AS. These are memory-bound or branch-heavy
314
+ workloads where codegen quality matters less than data layout.
315
+ * **json: solid win.** jz beats V8 by 1.7× on runtime JSON parsing; AS
316
+ cannot run this case.
317
+ * **watr: near parity.** jz is 1.07× slower than V8 on a 144 kB compiler
318
+ bundle. This is the only case where V8's profile-guided JIT tiers beat
319
+ jz's AOT wasm.
@@ -0,0 +1,112 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 500" width="720" height="500" role="img" aria-label="jz benchmark — each ball runs back and forth at a rate proportional to that engine's speed; jz is the 1.00× baseline, lower is faster">
2
+ <rect width="720" height="500" 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">→ wasm</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">Rust</text>
21
+ <text x="156" y="106" text-anchor="end" font-size="10" fill="#aeb4ba">rustc -O3</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.7111717105322697s" 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">Zig</text>
33
+ <text x="156" y="156" text-anchor="end" font-size="10" fill="#aeb4ba">ReleaseFast</text>
34
+ <text x="662" y="149" font-size="13" font-weight="500" fill="#868e96">1.31×</text>
35
+ <circle cx="182.0" cy="145" r="8" fill="#adb5bd">
36
+ <animate attributeName="cx" dur="2.097403438402887s" 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">Bun</text>
45
+ <text x="156" y="206" text-anchor="end" font-size="10" fill="#aeb4ba">JSC</text>
46
+ <text x="662" y="199" font-size="13" font-weight="500" fill="#868e96">1.51×</text>
47
+ <circle cx="182.0" cy="195" r="8" fill="#adb5bd">
48
+ <animate attributeName="cx" dur="2.4126551201460966s" 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">Go</text>
57
+ <text x="156" y="256" text-anchor="end" font-size="10" fill="#aeb4ba">gc</text>
58
+ <text x="662" y="249" font-size="13" font-weight="500" fill="#868e96">1.62×</text>
59
+ <circle cx="182.0" cy="245" r="8" fill="#adb5bd">
60
+ <animate attributeName="cx" dur="2.599256056991375s" 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">V8</text>
69
+ <text x="156" y="306" text-anchor="end" font-size="10" fill="#aeb4ba">Node</text>
70
+ <text x="662" y="299" font-size="13" font-weight="500" fill="#868e96">1.98×</text>
71
+ <circle cx="182.0" cy="295" r="8" fill="#adb5bd">
72
+ <animate attributeName="cx" dur="3.161748945741268s" 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">AssemblyScript</text>
81
+ <text x="156" y="356" text-anchor="end" font-size="10" fill="#aeb4ba">asc -O3</text>
82
+ <text x="662" y="349" font-size="13" font-weight="500" fill="#868e96">2.02×</text>
83
+ <circle cx="182.0" cy="345" r="8" fill="#adb5bd">
84
+ <animate attributeName="cx" dur="3.234040602137033s" 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">Porffor</text>
93
+ <text x="156" y="406" text-anchor="end" font-size="10" fill="#aeb4ba">runs 3 / 13</text>
94
+ <text x="662" y="399" font-size="13" font-weight="500" fill="#868e96">3.08×</text>
95
+ <circle cx="182.0" cy="395" r="8" fill="#adb5bd">
96
+ <animate attributeName="cx" dur="4.926330541139543s" 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
+ <g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
101
+ <rect x="174" y="443.5" width="476" height="3" rx="1.5" fill="#edf0f2"/>
102
+ <line x1="174" y1="438" x2="174" y2="452" stroke="#ced4da" stroke-width="2"/>
103
+ <line x1="650" y1="438" x2="650" y2="452" stroke="#ced4da" stroke-width="2"/>
104
+ <text x="156" y="441" text-anchor="end" font-size="14" font-weight="500" fill="#343a40">NumPy</text>
105
+ <text x="156" y="456" text-anchor="end" font-size="10" fill="#aeb4ba">Python</text>
106
+ <text x="662" y="449" font-size="13" font-weight="500" fill="#868e96">8.31×</text>
107
+ <circle cx="182.0" cy="445" r="8" fill="#adb5bd">
108
+ <animate attributeName="cx" dur="10s" repeatCount="indefinite" calcMode="linear"
109
+ keyTimes="0;0.5;1" values="182.0;642.0;182.0" begin="-3.28s"/>
110
+ </circle>
111
+ </g>
112
+ </svg>
package/cli.js CHANGED
@@ -7,24 +7,27 @@
7
7
  import { readFileSync, writeFileSync } from 'fs'
8
8
  import { resolve } from 'path'
9
9
  import { pathToFileURL } from 'url'
10
- import { parse } from 'subscript/feature/jessie'
11
10
  import jz, { compile } from './index.js'
12
- import jzifyFn from './src/jzify.js'
13
- import { codegen } from './src/codegen.js'
11
+ import transform from './transform.js'
14
12
  import { resolveModuleGraph } from './src/resolve.js'
15
13
  import { createRequire } from 'module'
16
14
 
17
15
  const jzRequire = createRequire(import.meta.url)
18
16
  const PKG = jzRequire('./package.json')
19
17
 
18
+ function formatWarning(w) {
19
+ const where = w.line != null ? ` (${w.line}:${w.column})` : ''
20
+ return `warning[${w.code}]${where}: ${w.message}`
21
+ }
22
+
20
23
  function showHelp() {
21
24
  console.log(`
22
25
  jz v${PKG.version} - min JS → WASM compiler
23
26
 
24
27
  Usage:
25
- jz <file.js> Compile JS to WASM (auto-jzify)
26
- jz --strict <file.js> Strict mode (no auto-transform)
27
- jz --jzify <file.js> Transform JS → jz (auto-derives output file)
28
+ jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
29
+ jz --strict <file.js> Strict mode — pure canonical subset, no lowering
30
+ jz --jzify <file.js> Transform JS → jz source (auto-derives output file)
28
31
  jz -e <expression> Evaluate expression
29
32
  jz --help Show this help
30
33
 
@@ -47,8 +50,8 @@ Options:
47
50
  --host <js|wasi> Runtime-service lowering (default js)
48
51
  --no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
49
52
  --names Emit wasm name section for profilers/debuggers
50
- --strict Strict jz mode (no auto-transform), reject dynamic fallbacks
51
- --jzify Transform JS to jz (no compilation)
53
+ --strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
54
+ --jzify Transform JS to jz source (no compilation)
52
55
  --eval, -e Evaluate expression or file
53
56
  --wat Output WAT text instead of binary
54
57
  --resolve Resolve bare specifiers via Node.js module resolution
@@ -84,18 +87,20 @@ async function main() {
84
87
 
85
88
  async function handleEvaluate(args) {
86
89
  const input = args.join(' ')
87
- let code
88
-
89
- if (args.length === 1 && (args[0].endsWith('.js') || args[0].endsWith('.jz')))
90
- code = readFileSync(args[0], 'utf8')
91
- else
92
- code = `export let _ = () => ${input}`
90
+ const isFile = args.length === 1 && (args[0].endsWith('.js') || args[0].endsWith('.jz'))
91
+ // A bare expression ("1 + 2") is wrapped in an arrow so its value can be printed. A file, or
92
+ // `-e` text that is already module-level (top-level export/import), compiles as-is — wrapping it
93
+ // would splice `export let _ = () => export …` and crash with a garbled SyntaxError.
94
+ const isModule = isFile || /^\s*(export|import)\b/m.test(input)
95
+ const code = isFile ? readFileSync(args[0], 'utf8')
96
+ : isModule ? input
97
+ : `export let _ = () => ${input}`
93
98
 
94
99
  const { exports } = jz(code)
95
100
 
96
- // If there's an exported _ (expression eval), call it
97
- if (exports._) console.log(exports._())
98
- else console.log(exports)
101
+ if (exports._ !== undefined) console.log(exports._()) // expression eval print value
102
+ else if (typeof exports.main === 'function') console.log(exports.main()) // module with a main() entry
103
+ else console.log(`compiled; exports: ${Object.keys(exports).join(', ') || '(none)'}`)
99
104
  }
100
105
 
101
106
  async function handleJzify(args) {
@@ -107,9 +112,7 @@ async function handleJzify(args) {
107
112
  if (!inputFile) throw new Error('No input file specified')
108
113
  if (!outputFile) outputFile = inputFile.replace(/\.js$/, '.jz')
109
114
  const code = readFileSync(inputFile, 'utf8')
110
- const ast = parse(code)
111
- const transformed = jzifyFn(ast)
112
- const out = codegen(transformed) + '\n'
115
+ const out = transform(code) + '\n'
113
116
  if (outputFile === '-') {
114
117
  process.stdout.write(out)
115
118
  } else {
@@ -154,17 +157,19 @@ async function handleCompile(args) {
154
157
  const { code: codeRewritten, modules } = resolveModuleGraph(inputFile, { resolveNode })
155
158
  if (process.env.JZ_DEBUG_MODULES === '1') console.error('modules:', Object.keys(modules))
156
159
 
157
- // .jz = strict (no auto-transform), .js = auto-jzify
158
- // --strict forces strict for any extension
160
+ // jzify is default-on; strict (pure canonical subset) is opt-in.
161
+ // `.jz` files are treated as strict; `--strict` forces it for any extension.
162
+ if (inputFile.endsWith('.jz')) strict = true
163
+ const warnings = { entries: [] }
159
164
  const opts = {
160
165
  wat,
161
- jzify: !strict && !inputFile.endsWith('.jz'),
166
+ warnings,
162
167
  strict,
163
168
  importMetaUrl: pathToFileURL(resolve(inputFile)).href,
164
169
  ...(optimize !== undefined && { optimize }),
165
170
  ...(host && { host }),
166
171
  ...(alloc === false && { alloc: false }),
167
- ...(names && { profileNames: true }),
172
+ ...(names && { profile: { names: true } }),
168
173
  ...(Object.keys(modules).length && { modules }),
169
174
  }
170
175
 
@@ -175,6 +180,9 @@ async function handleCompile(args) {
175
180
 
176
181
  const result = compile(codeRewritten, opts)
177
182
 
183
+ for (const w of warnings.entries)
184
+ console.warn(formatWarning(w))
185
+
178
186
  if (outputFile === '-') {
179
187
  process.stdout.write(result)
180
188
  } else if (wat) {