jz 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- <img src="logo.svg" alt="jz logo" width="120"/>
1
+ <img src="jz.svg" alt="jz logo" width="120"/>
2
2
 
3
3
 
4
4
 
5
- ## ![stability](https://img.shields.io/badge/stability-experimental-black) [![test](https://github.com/dy/jz/actions/workflows/test.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/test.yml)
5
+ ## ![stability](https://img.shields.io/badge/stability-experimental-black) [![npm](https://img.shields.io/npm/v/jz?color=gray)](http://npmjs.org/jz) [![test](https://github.com/dy/jz/actions/workflows/test.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/test.yml)
6
6
 
7
7
 
8
8
  **JZ** (_javascript zero_) is **minimal modern functional JS subset**, compiling to WASM.<br/>
@@ -17,18 +17,14 @@ dist(3, 4) // 5
17
17
 
18
18
  ## Why?
19
19
 
20
- **Write plain JS, compile to WASM** – fast, portable and long-lasting. JZ distills the modern functional core – the "good parts" [Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk) – from legacy semantics, specs evolution and perf quirks.
20
+ **Write plain JS, compile to WASM** – fast, portable and long-lasting.<br>
21
+ JZ distills the modern functional core – the "good parts" ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) – from legacy semantics, features overhead and perf quirks.
21
22
 
22
- * **Static** – no runtime, no GC, no dynamic constructs.
23
+ * **Static AOT** – no runtime, no GC, no dynamic constructs.
23
24
  * **Valid jz = valid js** — test in browser, compile to wasm.
24
25
  * **Minimal** — output is close to hand-written WAT.
25
26
  <!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
26
27
 
27
- **Used by**: [web-audio-api](https://github.com/audiojs/web-audio-api), [color-space](https://github.com/colorjs/color-space), [audiojs](https://github.com/colorjs/audiojs). Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
28
- <!-- * [audio-filter](https://github.com/audiojs/audio-filter)
29
- * [digital-filter](https://github.com/audiojs/digital-filter)
30
- * [time-stretch](https://github.com/audiojs/time-stretch) -->
31
-
32
28
  | Good for | Not for |
33
29
  |-----------------------------|----------------------------|
34
30
  | Numeric / math compute | UI / frontend |
@@ -36,6 +32,9 @@ dist(3, 4) // 5
36
32
  | Parsing / transforms | Async / I/O-heavy logic |
37
33
  | WASM utilities | JavaScript runtime |
38
34
 
35
+ <!-- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo). -->
36
+ <!-- Used internally by: web-audio-api, color-space, audiojs -->
37
+
39
38
 
40
39
  ## Usage
41
40
 
@@ -50,8 +49,33 @@ add(2, 3) // 5
50
49
  const wasm = compile('export let f = (x) => x * 2')
51
50
  const mod = new WebAssembly.Module(wasm)
52
51
  const inst = new WebAssembly.Instance(mod)
52
+
53
+ // Async WASM startup — jz source compilation is still synchronous
54
+ const asyncMod = await WebAssembly.compile(wasm)
55
+ const asyncInst = await WebAssembly.instantiate(asyncMod)
56
+ asyncInst.exports.f(21) // 42
53
57
  ```
54
58
 
59
+ <details>
60
+ <summary><strong>Options</strong></summary><br>
61
+
62
+ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones:
63
+
64
+ | Option | Use |
65
+ |---|---|
66
+ | `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, and `undefined` by lowering them to the JZ subset. |
67
+ | `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
68
+ | `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
69
+ | `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. |
70
+ | `host: 'js' \| 'wasi'` | Select runtime-service lowering. Default `js` uses small `env.*` imports auto-wired by `jz()`; `wasi` emits WASI Preview 1 imports for wasmtime/wasmer/deno. |
71
+ | `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default, `3` enables aggressive experimental passes, object form overrides individual passes. |
72
+ | `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
73
+ | `alloc: false` | Omit raw allocator exports like `_alloc`/`_clear` when compiling standalone WASM that never marshals heap values across the host boundary. |
74
+ | `wat: true` | `compile()` returns WAT text instead of a WASM binary. |
75
+ | `profile` | Pass a mutable sink to collect compile-stage timings; set `profile.names = true` to also emit a WASM `name` section for profiler/debugger symbolication. `profileNames` remains as a legacy alias. |
76
+
77
+ </details>
78
+
55
79
  ## CLI
56
80
 
57
81
  `npm install -g jz`
@@ -102,56 +126,39 @@ flowchart TB
102
126
  │ │
103
127
  │ ┌────────────────────────────────────────────────────────────────────┐ │
104
128
  │ │ JZ │ │
105
- │ │ let/const => ...xs destructuring import/export `${}` │ │
129
+ │ │ let/const => ...xs destructuring import/export │ │
106
130
  │ │ if/else for/while/do-while/of/in break/continue │ │
107
131
  │ │ try/catch/finally throw │ │
108
- │ │ operators strings booleans numbers arrays objects null │ │
109
- │ │ Math Number String Array Object JSON RegExp Symbol │ │
132
+ │ │ operators strings booleans numbers arrays objects `${}` │ │
133
+ │ │ Math Number String Array Object JSON RegExp Symbol null │ │
110
134
  │ │ ArrayBuffer DataView TypedArray Map Set │ │
111
135
  │ │ console setTimeout/setInterval Date performance │ │
112
136
  │ └────────────────────────────────────────────────────────────────────┘ │
113
137
  └────────────────────────────────────────────────────────────────────────┘
114
-
115
138
  Not supported
116
139
  async/await Promise function* yield
117
140
  this class super extends delete labels
118
141
  eval Function with Proxy Reflect WeakMap WeakSet
119
142
  dynamic import DOM fetch Intl Node APIs
120
143
  ```
144
+ ## FAQ
121
145
 
146
+ <details>
147
+ <summary><strong>How to pass data between JS and WASM?</strong></summary>
122
148
 
123
- ## Benchmark
124
-
125
- | | **jz** | [Node](https://nodejs.org/) | [AS](https://github.com/AssemblyScript/assemblyscript) | WAT | C | [Go](https://go.dev/) | [Rust](https://www.rust-lang.org/) |
126
- |---|---|---|---|---|---|---|---|
127
- | [**biquad**](bench/biquad/biquad.js) | **6.44 ms**<br>**3.4 kB** | 12.30 ms<br>3.2 kB | 9.04 ms<br>1.9 kB | 6.48 ms<br>767 B | 5.43 ms<br>32.7 kB | 9.03 ms<br>1.60 MB<br>fma | 5.33 ms<br>380.7 kB |
128
- | [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10 ms**<br>**1.6 kB** | 0.18 ms<br>1.4 kB | 0.08 ms<br>1.5 kB | — | 0.13 ms<br>32.9 kB | 0.07 ms<br>1.60 MB | 0.12 ms<br>380.7 kB |
129
- | [**mat4**](bench/mat4/mat4.js) | **4.00 ms**<br>**1.7 kB** | 11.64 ms<br>1.1 kB | 9.18 ms<br>1.5 kB | 7.99 ms<br>353 B | 2.62 ms<br>32.8 kB | 11.93 ms<br>1.60 MB | 0.80 ms<br>380.7 kB |
130
- | [**aos**](bench/aos/aos.js) | **1.50 ms**<br>**2.3 kB** | 1.81 ms<br>1.1 kB | 1.91 ms<br>2.2 kB | — | 1.22 ms<br>32.9 kB | 0.90 ms<br>1.60 MB | 1.20 ms<br>380.7 kB |
131
- | [**bitwise**](bench/bitwise/bitwise.js) | **4.93 ms**<br>**1.2 kB** | 5.31 ms<br>1005 B | 12.36 ms<br>1.5 kB | 4.96 ms<br>355 B | 1.31 ms<br>32.9 kB | 5.24 ms<br>1.60 MB | 1.30 ms<br>380.7 kB |
132
- | [**poly**](bench/poly/poly.js) | **1.13 ms**<br>**1.3 kB** | 2.31 ms<br>1014 B | 1.14 ms<br>1.3 kB | — | 0.52 ms<br>32.9 kB | 0.80 ms<br>1.60 MB | 0.52 ms<br>380.7 kB |
133
- | [**callback**](bench/callback/callback.js) | **0.01 ms**<br>**1.5 kB** | 1.03 ms<br>828 B | 1.48 ms<br>1.9 kB | — | 0.09 ms<br>32.9 kB | 0.20 ms<br>1.60 MB | 0.08 ms<br>380.7 kB |
134
- | [**json**](bench/json/json.js) | **0.20 ms**<br>**2.8 kB** | 0.38 ms<br>923 B | — | — | 0.02 ms<br>32.8 kB | 1.06 ms<br>1.97 MB | 0.03 ms<br>380.7 kB |
135
- | [**watr**](bench/watr/watr.js) | **1.82 ms**<br>**137.1 kB** | 1.50 ms<br>2.6 kB | — | — | — | — | — |
136
-
137
- _Numbers from `node bench/bench.mjs` on Apple Silicon, May 2026. `jz` uses host imports for benchmark timing/logging (measures wasm without WASI console/perf bloat). Raw-JS size is the entry file; jz size is the bundled wasm artifact. `fma` is the documented Go arm64 fused-multiply-add checksum class. See [benchmark](./bench/)._
138
-
139
-
140
-
141
-
142
- ## FAQ
149
+ <br>
143
150
 
144
- ### How to pass data between JS and WASM?
145
151
 
146
- Numbers pass directly as f64. Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
152
+ Numbers pass directly as f64, arrays of ≤ 8 elements return as plain JS arrays (multi-value). Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
147
153
 
148
154
  ```js
149
- const { exports, memory } = jz(\`
155
+ const { exports, memory } = jz`
150
156
  export let greet = (s) => s.length
151
157
  export let sum = (a) => a.reduce((s, x) => s + x, 0)
152
158
  export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
159
+ export let rgb = (c) => [c, c * 0.5, c * 0.2]
153
160
  export let process = (buf) => buf.map(x => x * 2)
154
- \`)
161
+ `
155
162
 
156
163
  // JS → WASM (write)
157
164
  memory.String('hello') // → string pointer
@@ -159,8 +166,9 @@ memory.Array([1, 2, 3]) // → array pointer
159
166
  memory.Float64Array([1.0, 2.0]) // → typed array pointer
160
167
  memory.Int32Array([10, 20, 30]) // all typed array constructors available
161
168
 
162
- // Objects: keys and order must match the jz source declaration.
169
+ // Objects: keys and order must match the jz source declaration.
163
170
  // jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
171
+ // If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
164
172
  memory.Object({ x: 3, y: 4 }) // → object pointer
165
173
 
166
174
  // Strings/arrays inside objects are auto-wrapped to pointers:
@@ -171,14 +179,17 @@ exports.greet(memory.String('hello')) // 5
171
179
  exports.sum(memory.Array([1, 2, 3])) // 6
172
180
  exports.dist(memory.Object({ x: 3, y: 4 })) // 5
173
181
 
174
- // WASM JS (read)
182
+ // direct JS array return
183
+ exports.rgb(100) // [100, 50, 20]
184
+
185
+ // read pointer value
175
186
  memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
176
187
  ```
177
188
 
178
189
  Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
179
190
 
180
191
  ```js
181
- jz\`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x\`
192
+ jz`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x`
182
193
  ```
183
194
 
184
195
  <!--
@@ -209,7 +220,12 @@ All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. H
209
220
  **NaN preservation**: IEEE 754 defines 2^52 − 1 distinct NaN bit patterns. WASM preserves NaN payload bits through arithmetic (spec requires `nondeterministic_nan`), and JS engines canonicalize only on certain operations (`Math.fround`, structured clone). jz uses quiet NaNs (`0x7FF8` prefix) which survive all standard paths. The 51 payload bits encode type (4), aux metadata (15), and heap offset (32) — enough for 4GB addressable memory and 12 type codes.
210
221
  -->
211
222
 
212
- ### How does template interpolation work?
223
+ </details>
224
+
225
+ <details>
226
+ <summary><strong>How does template interpolation work?</strong></summary>
227
+
228
+ <br>
213
229
 
214
230
  Numbers and booleans inline directly into source. Strings, arrays, and objects are serialized as jz source literals and compiled at compile time — no post-instantiation allocation, no getter overhead:
215
231
 
@@ -224,66 +240,84 @@ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
224
240
 
225
241
  Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
226
242
 
227
- ### Does it support imports?
243
+ </details>
244
+
245
+ <details>
246
+ <summary><strong>Does it support ES module imports?</strong></summary>
228
247
 
229
- Yes — standard ES `import` syntax, bundled at compile-time into one WASM.
248
+ <br>
249
+
250
+ Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
230
251
 
231
252
  ```js
232
- // modules: jz source bundled at compile time
233
253
  const { exports } = jz(
234
254
  'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
235
255
  { modules: { './math.jz': 'export let add = (a, b) => a + b' } }
236
256
  )
237
-
238
- // imports: JS functions wired at instantiation
239
- const { exports } = jz(
240
- 'import { log } from "host"; export let f = (x) => { log(x); return x }',
241
- { imports: { host: { log: console.log } } }
242
- )
243
257
  ```
244
258
 
245
259
  Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
246
260
 
247
- You can also pass whole host environment objects — `Math`, `Date`, `window`, `console`, or any custom namespace object. jz extracts the functions it needs via `Object.getOwnPropertyNames`, so non-enumerable built-ins (like `Math.sin`) work automatically:
261
+ **CLI** resolves filesystem imports automatically.
262
+
263
+ ```sh
264
+ jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
265
+ ```
266
+
267
+ **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
248
268
 
249
269
  ```js
250
- // Pass the entire Math namespace sin, cos, sqrt, PI, etc. auto-wired
270
+ // Transitive bundlingall merged into one WASM
271
+ const { exports } = jz(mainSrc, { modules: {
272
+ './math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
273
+ // Fetch sources yourself, pass them in
274
+ './utils.jz': await fetch('./util.jz').then(r => r.text())
275
+ } })
276
+ ```
277
+
278
+ </details>
279
+
280
+ <details>
281
+ <summary><strong>How do I pass values from the host to jz?</strong></summary>
282
+
283
+ <br>
284
+
285
+ Any host namespace — functions, constants, custom objects — wires in via the `imports` option. jz extracts what's needed via `Object.getOwnPropertyNames`, so non-enumerable built-ins (`Math.sin`, `Date.now`) work automatically:
286
+
287
+ ```js
288
+ // Custom function
289
+ const { exports } = jz(
290
+ 'import { log } from "host"; export let f = (x) => { log(x); return x }',
291
+ { imports: { host: { log: console.log } } }
292
+ )
293
+
294
+ // Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
251
295
  const { exports } = jz(
252
296
  'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
253
297
  { imports: { math: Math } }
254
298
  )
255
299
 
256
- // Pass Date static methods
300
+ // Date static methods
257
301
  const { exports } = jz(
258
302
  'import { now } from "date"; export let f = () => now()',
259
303
  { imports: { date: Date } }
260
304
  )
261
305
 
262
- // Pass window / globalThis
306
+ // window / globalThis
263
307
  const { exports } = jz(
264
308
  'import { parseInt } from "window"; export let f = () => parseInt("42")',
265
309
  { imports: { window: globalThis } }
266
310
  )
267
311
  ```
268
312
 
269
- **CLI** resolves filesystem imports automatically.
270
-
271
- ```sh
272
- jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
273
- ```
313
+ For per-call data (numbers, strings, arrays, objects, typed arrays), see *How to pass data between JS and WASM?* above — pointers via `memory.String`/`memory.Array`/`memory.Object` or template interpolation.
274
314
 
275
- **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
315
+ </details>
276
316
 
277
- ```js
278
- // Transitive bundling all merged into one WASM
279
- const { exports } = jz(mainSrc, { modules: {
280
- './math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
281
- // Fetch sources yourself, pass them in
282
- './utils.jz': await fetch('./util.jz').then(r => r.text())
283
- } })
284
- ```
317
+ <details>
318
+ <summary><strong>Can two modules share data?</strong></summary>
285
319
 
286
- ### Can two modules share data?
320
+ <br>
287
321
 
288
322
  Yes — `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate automatically, so objects created in one module are readable by another:
289
323
 
@@ -306,10 +340,54 @@ memory.Array([1, 2, 3]) // → NaN-boxed pointer
306
340
 
307
341
  `jz.memory()` returns an actual `WebAssembly.Memory` (monkey-patched with `.read()`, `.String()`, `.Array()`, `.Object()`, `.write()`, etc). You can also pass an existing memory: `jz.memory(new WebAssembly.Memory({ initial: 4 }))` patches and returns the same object. Passing raw `WebAssembly.Memory` to `{ memory }` auto-wraps it.
308
342
 
309
- All modules sharing a memory use a single bump allocator (heap pointer at byte 1020). Use `.instance.exports` for raw pointers, `.exports` for the JS-wrapped surface.
343
+ Modules sharing a memory share a single bump allocator see *How does memory work?* below. Use `.instance.exports` for raw pointers, `.exports` for the JS-wrapped surface.
344
+
345
+
346
+ </details>
347
+
348
+ <details>
349
+ <summary><strong>How does memory work? How do I reset it?</strong></summary>
350
+
351
+ <br>
352
+
353
+ jz uses a **bump allocator**: every heap value (string, array, object, typed array) bumps a single pointer forward. No free list, no GC, no per-object header overhead beyond `[len][cap]`. Bytes 0–1023 are reserved (data segment + heap-pointer slot at byte 1020); the heap starts at byte 1024 and grows the WASM memory automatically when full.
354
+
355
+ This means **memory is never reclaimed implicitly** — long-running programs that allocate per call will grow without bound. The fix is to reset the heap pointer between independent batches:
356
+
357
+ ```js
358
+ const { exports, memory } = jz`
359
+ export let process = (n) => {
360
+ let xs = []
361
+ for (let i = 0; i < n; i++) xs.push(i * 2)
362
+ return xs.reduce((s, x) => s + x, 0)
363
+ }
364
+ `
365
+
366
+ for (let i = 0; i < 1000; i++) {
367
+ const sum = exports.process(100) // allocates an array each call
368
+ memory.reset() // drop everything; heap ptr → 1024
369
+ }
370
+ ```
371
+
372
+ After `memory.reset()` all previously returned pointers are invalid — read what you need first, then reset.
373
+
374
+ For finer control, allocate manually: `memory.alloc(bytes)` returns a raw offset using the same bump pointer. Pure scalar modules (no strings/arrays/objects) are compiled without the allocator at all — no `_alloc`, no `_clear`, no memory section.
375
+
376
+ **Non-JS hosts** (wasmtime, wasmer, deno, EdgeJS, embedded WASM) get the same allocator via two exports:
377
+
378
+ ```
379
+ (func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
380
+ (func $_clear) ;; rewinds heap pointer to 1024
381
+ ```
382
+
383
+ `memory.reset()` and `memory.alloc()` are JS-side aliases for these. Headers vary by type: strings store `[len:i32]` + utf8 bytes (offset = `_alloc(4+n) + 4`); arrays / typed arrays / objects store `[len:i32, cap:i32]` + payload (offset = `_alloc(8+bytes) + 8`). The pointer crossing the WASM boundary is the f64 NaN-box `0x7FF8 << 48 | type << 47 | aux << 32 | offset` — see [`src/host.js`](src/host.js) for type codes and the canonical encoders. Call `_clear()` between batches to reclaim. Strip both with `compile(code, { alloc: false })` if you only call functions and never marshal heap values across the boundary.
384
+
385
+ </details>
310
386
 
387
+ <details>
388
+ <summary><strong>How do I run compiled WASM outside the browser?</strong></summary>
311
389
 
312
- ### How do I run compiled WASM outside the browser?
390
+ <br>
313
391
 
314
392
  ```sh
315
393
  jz program.js -o program.wasm
@@ -321,33 +399,78 @@ deno run program.wasm
321
399
  ```
322
400
 
323
401
  Pure numeric modules have no imports and instantiate with standard
324
- `WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS
325
- hosts such as EdgeJS. Compile once at startup or build time, then reuse the
326
- module; do not compile JZ source per request.
402
+ `WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS hosts such as EdgeJS. Compile once at startup or build time, then reuse the module; do not compile JZ source per request.
403
+
404
+ Two host modes select how runtime services lower:
405
+
406
+ ```js
407
+ jz.compile(code) // host: 'js' (default) — env.* imports
408
+ jz.compile(code, { host: 'wasi' }) // wasi_snapshot_preview1.* imports
409
+ ```
410
+
411
+ `host: 'js'` (default) — `console.log`/`Date.now`/`performance.now` import from `env.*` and the JS host (`jz()` runtime) wires them automatically. Host-side stringification means jz drops `__ftoa`/`__write_*`/`__to_str` from the binary.
327
412
 
328
- `console.log` compiles to WASI `fd_write` by default — works natively on
329
- wasmtime/wasmer/deno without polyfills. In JS hosts, `jz()` auto-applies the
330
- small `jz/wasi` polyfill; pass `{ write(fd, text) { ... } }` to capture or route
331
- stdout/stderr without depending on `process.stdout`.
413
+ `host: 'wasi'` — `console.log` compiles to WASI `fd_write`, clocks to
414
+ `clock_time_get`. Output runs natively on wasmtime/wasmer/deno. In JS hosts, the small `jz/wasi` polyfill is auto-applied; pass `{ write(fd, text) {…} }` to capture stdout/stderr. `host: 'wasi'` errors at compile time if a program would emit `env.__ext_*` (dynamic dispatch into the JS host) — annotate the receiver or stay on `host: 'js'`.
332
415
 
416
+ </details>
333
417
 
334
- ### What host features are supported?
418
+ <details>
419
+ <summary><strong>What host features are supported?</strong></summary>
335
420
 
336
- The compiled `.wasm` uses one import namespace:
421
+ <br>
422
+
423
+ | JS API | `host: 'js'` (default) | `host: 'wasi'` |
424
+ |---|---|---|
425
+ | `console.log()` | `env.print(val: i64, fd: i32, sep: i32)` — host stringifies | WASI `fd_write` (fd=1), space-separated, newline appended |
426
+ | `console.warn`/`error` | same, fd=2 | WASI `fd_write` (fd=2) |
427
+ | `Date.now()` | `env.now(0) -> f64` (epoch ms) | `clock_time_get` (realtime) |
428
+ | `performance.now()` | `env.now(1) -> f64` (monotonic ms) | `clock_time_get` (monotonic) |
429
+ | `setTimeout`/`clearTimeout` | `env.setTimeout(cb, delay, repeat) -> f64` / `env.clearTimeout(id) -> f64` — host schedules; fires via exported `__invoke_closure` | WASM timer queue + `__timer_tick` (or blocking `__timer_loop` on wasmtime) |
430
+ | `setInterval`/`clearInterval` | same `env.setTimeout` (repeat=1) / `env.clearTimeout` | WASM timer queue + `__timer_tick` |
431
+ | dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
432
+
433
+ The compiled `.wasm` uses at most one import namespace:
337
434
 
338
435
  - none — pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
339
- - `wasi_snapshot_preview1` — standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node/EdgeJS-like hosts, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
436
+ - `env` — JS-host services (default). Auto-wired by the `jz()` runtime.
437
+ - `wasi_snapshot_preview1` — standard WASI Preview 1. Run natively on wasmtime/wasmer/deno.
438
+
439
+ </details>
440
+
441
+ <details>
442
+ <summary><strong>How do I add custom operators / extend the stdlib?</strong></summary>
340
443
 
341
- | JS API | Maps to | Notes |
342
- |--------|---------|-------|
343
- | `console.log()` | WASI `fd_write` (fd=1) | Multiple args space-separated, newline appended |
344
- | `console.warn()`, `console.error()` | WASI `fd_write` (fd=2) | Writes to stderr |
345
- | `Date.now()` | WASI `clock_time_get` (realtime) | Returns ms since epoch |
346
- | `performance.now()` | WASI `clock_time_get` (monotonic) | Returns ms, high-resolution |
347
- | `setTimeout`, `clearTimeout` | WASM timer queue + `__timer_tick` | JS runtime drives tick via `setInterval`; wasmtime uses blocking `__timer_loop` |
348
- | `setInterval`, `clearInterval` | WASM timer queue + `__timer_tick` | Same — native WASM implementation, no host imports |
444
+ <br>
349
445
 
350
- ### Can I compile jz to C?
446
+ jz's emitter table (`ctx.core.emit`) maps AST operators → WASM IR generators. Module files in `module/` register handlers on it. To add your own:
447
+
448
+ ```js
449
+ import { emitter } from './src/emit.js'
450
+ import { typed } from './src/ir.js'
451
+
452
+ // Register a custom operator: my.double(x) → x * 2
453
+ emitter['my.double'] = (x) => {
454
+ return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
455
+ }
456
+ ```
457
+
458
+ The naming convention follows the AST path: `Math.sin` → `math.sin`, `arr.push` → `.push`, typed variants like `.f64:push`. See any file in `module/` for the full pattern — each exports a function that receives `ctx` and registers emitters, stdlib, globals, or helpers.
459
+
460
+ Inside a runtime module, import directly from the layer you need:
461
+
462
+ ```js
463
+ import { emit } from '../src/emit.js'
464
+ import { asF64, temp } from '../src/ir.js'
465
+ import { valTypeOf, VAL } from '../src/analyze.js'
466
+ ```
467
+
468
+ </details>
469
+
470
+ <details>
471
+ <summary><strong>Can I compile jz to C?</strong></summary>
472
+
473
+ <br>
351
474
 
352
475
  Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
353
476
 
@@ -356,6 +479,49 @@ jz program.js -o program.wasm
356
479
  wasm2c program.wasm -o program.c
357
480
  cc program.c -o program
358
481
  ```
482
+ </details>
483
+
484
+
485
+ ## Benchmark
486
+
487
+ | | jz | [Node](https://nodejs.org/) | [Porffor](https://github.com/CanadaHonk/porffor) | [AS](https://github.com/AssemblyScript/assemblyscript) | WAT | C | [Go](https://go.dev/) | [Zig](https://ziglang.org/) | [Rust](https://www.rust-lang.org/) | [NumPy](https://numpy.org/) |
488
+ |---|---|---|---|---|---|---|---|---|---|---|
489
+ | [biquad](bench/biquad/biquad.js) | 4.63ms<br>4.0kB | 8.68ms<br>3.2kB | fails | 6.59ms<br>1.9kB | 6.45ms<br>767 B | 5.30ms | 8.91ms<br>fma | 5.06ms | 5.28ms | 3.12s |
490
+ | [tokenizer](bench/tokenizer/tokenizer.js) | 0.07ms<br>1.8kB | 0.12ms<br>1.4kB | 0.34ms<br>2.6kB | 0.05ms<br>1.5kB | 0.08ms<br>344 B | 0.14ms | 0.07ms | 0.12ms | 0.12ms | 5.15ms |
491
+ | [mat4](bench/mat4/mat4.js) | 2.12ms<br>3.7kB | 11.80ms<br>1.2kB | 88.54ms<br>2.4kB<br>diff | 9.21ms<br>1.6kB | 8.06ms<br>414 B | 2.73ms | 11.93ms | 2.73ms | 1.77ms | 387.60ms |
492
+ | [aos](bench/aos/aos.js) | 1.11ms<br>2.3kB | 1.26ms<br>1.1kB | fails | 1.33ms<br>2.2kB | 1.07ms<br>481 B | 1.20ms | 0.91ms | 0.91ms | 1.20ms | 2.57ms |
493
+ | [mandelbrot](bench/mandelbrot/mandelbrot.js) | 8.02ms<br>1.2kB | 9.06ms<br>1.8kB | 9.71ms<br>3.0kB | 8.00ms<br>1.3kB | — | 8.31ms | 8.80ms | 7.83ms | 8.52ms | — |
494
+ | [bitwise](bench/bitwise/bitwise.js) | 0.98ms<br>1.3kB | 3.76ms<br>1005 B | fails | 8.79ms<br>1.5kB | 4.86ms<br>355 B | 1.30ms | 5.20ms | 4.15ms | 1.30ms | 14.72ms |
495
+ | [poly](bench/poly/poly.js) | 0.27ms<br>1.4kB | 1.62ms<br>1014 B | fails | 0.73ms<br>1.3kB | 0.81ms<br>359 B | 0.57ms | 0.79ms | 0.89ms | 0.63ms | 0.60ms |
496
+ | [callback](bench/callback/callback.js) | 0.03ms<br>1.6kB | 0.69ms<br>828 B | fails | 1.04ms<br>1.9kB | 0.24ms<br>267 B | 0.08ms | 0.23ms | 0.01ms | 0.12ms | 1.78ms |
497
+ | [json](bench/json/json.js) | 0.24ms<br>11.0kB | 0.37ms<br>1.2kB | fails | — | — | 0.03ms | 1.05ms | <0.01ms | 0.03ms | 1.21ms |
498
+ | [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
499
+
500
+ _Numbers from `node bench/bench.mjs` on Apple Silicon. Porffor cells were refreshed with `porf` 0.61.13; `fails` means the latest Porffor compiler/runtime did not complete that benchmark._
501
+
502
+ <details>
503
+ <summary><strong>Optimizations</strong></summary>
504
+
505
+ <br>
506
+ High-impact summary behind the benchmark table, not an exhaustive list.
507
+
508
+ | Optimization | Effect |
509
+ |---|---|
510
+ | Escape scalar replacement | Removes short-lived object/array literals before allocation. |
511
+ | Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
512
+ | Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
513
+ | Host-service import lowering | `host: 'js'` lowers console, clocks, and timers to small `env.*` imports instead of pulling WASI/string formatting into normal JS-host builds. |
514
+ | Static and shaped runtime JSON specialization | Constant `JSON.parse` sources fold to fresh slot trees; stable `let` JSON sources use a generated runtime parser for the inferred shape. |
515
+ | Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
516
+ | Integer/value-type narrowing | Keeps bitwise, `Math.imul`, `charCodeAt`, loop counters, and internal narrowed returns on raw i32/f64 paths instead of generic boxed-value helpers. |
517
+ | SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
518
+ | Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
519
+ | OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
520
+ | Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
521
+
522
+ `npm run test:bench-pin` pins every claimed V8 win, AssemblyScript win/tie, and wasm size budget. Mandelbrot is pinned as a V8 win and AssemblyScript tie, not an AS win. Unclaimed rows stay visible as todo gaps without weakening the asserted wins.
523
+
524
+ </details>
359
525
 
360
526
 
361
527
  ## Alternatives
package/cli.js CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  import { readFileSync, writeFileSync, existsSync } from 'fs'
8
8
  import { dirname, resolve, join } from 'path'
9
- import { parse } from 'subscript/jessie'
9
+ import { pathToFileURL } from 'url'
10
+ import { execFileSync } from 'child_process'
11
+ import { parse } from 'subscript/feature/jessie'
10
12
  import jz, { compile } from './index.js'
11
13
  import jzifyFn, { codegen } from './src/jzify.js'
12
14
 
@@ -36,6 +38,7 @@ Options:
36
38
  --jzify Transform JS to jz (no compilation)
37
39
  --eval, -e Evaluate expression or file
38
40
  --wat Output WAT text instead of binary
41
+ --resolve Resolve bare specifiers via Node.js module resolution
39
42
  `)
40
43
  }
41
44
 
@@ -96,12 +99,13 @@ async function handleJzify(args) {
96
99
  }
97
100
 
98
101
  async function handleCompile(args) {
99
- let inputFile = null, outputFile = null, wat = false, strict = false
102
+ let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false
100
103
 
101
104
  for (let i = 0; i < args.length; i++) {
102
105
  if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
103
106
  else if (args[i] === '--wat') wat = true
104
107
  else if (args[i] === '--strict') strict = true
108
+ else if (args[i] === '--resolve') resolveNode = true
105
109
  else if (!inputFile) inputFile = args[i]
106
110
  }
107
111
 
@@ -126,21 +130,43 @@ async function handleCompile(args) {
126
130
  } catch {}
127
131
  }
128
132
 
133
+ // Recursively resolve relative imports from entry file and all discovered modules
129
134
  const importRe = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g
130
- let m; while ((m = importRe.exec(code)) !== null) {
131
- const spec = m[1]
132
- if (!modules[spec] && (spec.startsWith('./') || spec.startsWith('../'))) {
133
- const full = resolve(dir, spec)
134
- try { modules[spec] = readFileSync(full, 'utf8') }
135
- catch { try { modules[spec] = readFileSync(full + '.js', 'utf8') } catch {} }
135
+ const resolveBareModule = (specifier, fromDir) => execFileSync(
136
+ process.execPath,
137
+ ['--input-type=module', '-e', 'process.stdout.write(import.meta.resolve(process.argv[1]))', specifier],
138
+ { cwd: fromDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
139
+ ).trim()
140
+ const resolveModule = (specifier, fromDir) => {
141
+ if (modules[specifier]) return
142
+ // Relative imports: resolve from filesystem
143
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
144
+ const full = resolve(fromDir, specifier)
145
+ let src
146
+ try { src = readFileSync(full, 'utf8') }
147
+ catch { try { src = readFileSync(full + '.js', 'utf8') } catch { return } }
148
+ modules[specifier] = src
149
+ let m; importRe.lastIndex = 0
150
+ while ((m = importRe.exec(src)) !== null) resolveModule(m[1], dirname(full))
151
+ return
152
+ }
153
+ // Bare specifiers: opt-in Node.js resolution
154
+ if (resolveNode) {
155
+ try {
156
+ const resolved = resolveBareModule(specifier, fromDir)
157
+ if (resolved.startsWith('file:')) modules[specifier] = readFileSync(new URL(resolved), 'utf8')
158
+ } catch {}
136
159
  }
137
160
  }
161
+ let m; importRe.lastIndex = 0
162
+ while ((m = importRe.exec(code)) !== null) resolveModule(m[1], dir)
138
163
 
139
164
  // .jz = strict (no auto-transform), .js = auto-jzify
140
165
  // --strict forces strict for any extension
141
166
  const opts = {
142
167
  wat,
143
168
  jzify: !strict && !inputFile.endsWith('.jz'),
169
+ importMetaUrl: pathToFileURL(resolve(inputFile)).href,
144
170
  ...(Object.keys(modules).length && { modules }),
145
171
  }
146
172