jz 0.1.0 → 0.2.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.
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,16 +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, features overhead 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
- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
28
- <!-- Used internally by: web-audio-api, color-space, audiojs -->
29
-
30
28
  | Good for | Not for |
31
29
  |-----------------------------|----------------------------|
32
30
  | Numeric / math compute | UI / frontend |
@@ -34,6 +32,9 @@ Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https:/
34
32
  | Parsing / transforms | Async / I/O-heavy logic |
35
33
  | WASM utilities | JavaScript runtime |
36
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
+
37
38
 
38
39
  ## Usage
39
40
 
@@ -48,8 +49,33 @@ add(2, 3) // 5
48
49
  const wasm = compile('export let f = (x) => x * 2')
49
50
  const mod = new WebAssembly.Module(wasm)
50
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
51
57
  ```
52
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. `memoryPages` remains as a legacy alias for the page count. |
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
+
53
79
  ## CLI
54
80
 
55
81
  `npm install -g jz`
@@ -78,7 +104,7 @@ flowchart TB
78
104
  subgraph JS[JS — not supported]
79
105
  subgraph JZify[JZ + jzify]
80
106
  subgraph JZ[JZ strict]
81
- j1["let/const, arrows, default/rest params, flow, break/continue, try/catch, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
107
+ j1["let/const, arrows, default/rest params, flow, break/continue, try/catch/finally, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
82
108
  end
83
109
  z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
84
110
  end
@@ -94,12 +120,12 @@ flowchart TB
94
120
 
95
121
  ```
96
122
  ┌────────────────────────────────────────────────────────────────────────┐
97
- │ JZify test262: 68%
123
+ │ JZify
98
124
  │ var function arguments switch new Foo() │
99
125
  │ == != instanceof undefined │
100
126
  │ │
101
127
  │ ┌────────────────────────────────────────────────────────────────────┐ │
102
- │ │ JZ test262: 54% │ │
128
+ │ │ JZ │ │
103
129
  │ │ let/const => ...xs destructuring import/export │ │
104
130
  │ │ if/else for/while/do-while/of/in break/continue │ │
105
131
  │ │ try/catch/finally throw │ │
@@ -109,29 +135,30 @@ flowchart TB
109
135
  │ │ console setTimeout/setInterval Date performance │ │
110
136
  │ └────────────────────────────────────────────────────────────────────┘ │
111
137
  └────────────────────────────────────────────────────────────────────────┘
112
-
113
138
  Not supported
114
139
  async/await Promise function* yield
115
140
  this class super extends delete labels
116
141
  eval Function with Proxy Reflect WeakMap WeakSet
117
142
  dynamic import DOM fetch Intl Node APIs
118
143
  ```
144
+ ## FAQ
119
145
 
146
+ <details>
147
+ <summary><strong>How to pass data between JS and WASM?</strong></summary>
120
148
 
149
+ <br>
121
150
 
122
- ## FAQ
123
-
124
- ### How to pass data between JS and WASM?
125
151
 
126
- 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:
127
153
 
128
154
  ```js
129
- const { exports, memory } = jz(\`
155
+ const { exports, memory } = jz`
130
156
  export let greet = (s) => s.length
131
157
  export let sum = (a) => a.reduce((s, x) => s + x, 0)
132
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]
133
160
  export let process = (buf) => buf.map(x => x * 2)
134
- \`)
161
+ `
135
162
 
136
163
  // JS → WASM (write)
137
164
  memory.String('hello') // → string pointer
@@ -152,14 +179,17 @@ exports.greet(memory.String('hello')) // 5
152
179
  exports.sum(memory.Array([1, 2, 3])) // 6
153
180
  exports.dist(memory.Object({ x: 3, y: 4 })) // 5
154
181
 
155
- // WASM JS (read)
182
+ // direct JS array return
183
+ exports.rgb(100) // [100, 50, 20]
184
+
185
+ // read pointer value
156
186
  memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
157
187
  ```
158
188
 
159
189
  Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
160
190
 
161
191
  ```js
162
- 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`
163
193
  ```
164
194
 
165
195
  <!--
@@ -190,7 +220,12 @@ All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. H
190
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.
191
221
  -->
192
222
 
193
- ### How does template interpolation work?
223
+ </details>
224
+
225
+ <details>
226
+ <summary><strong>How does template interpolation work?</strong></summary>
227
+
228
+ <br>
194
229
 
195
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:
196
231
 
@@ -205,7 +240,12 @@ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
205
240
 
206
241
  Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
207
242
 
208
- ### Does it support ES module imports?
243
+ </details>
244
+
245
+ <details>
246
+ <summary><strong>Does it support ES module imports?</strong></summary>
247
+
248
+ <br>
209
249
 
210
250
  Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
211
251
 
@@ -235,40 +275,49 @@ const { exports } = jz(mainSrc, { modules: {
235
275
  } })
236
276
  ```
237
277
 
238
- ### Can I call JS/host functions from jz?
278
+ </details>
239
279
 
240
- Yes — JS functions are wired at instantiation via the `imports` option:
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:
241
286
 
242
287
  ```js
288
+ // Custom function
243
289
  const { exports } = jz(
244
290
  'import { log } from "host"; export let f = (x) => { log(x); return x }',
245
291
  { imports: { host: { log: console.log } } }
246
292
  )
247
- ```
248
-
249
- 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:
250
293
 
251
- ```js
252
- // Pass the entire Math namespace — sin, cos, sqrt, PI, etc. auto-wired
294
+ // Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
253
295
  const { exports } = jz(
254
296
  'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
255
297
  { imports: { math: Math } }
256
298
  )
257
299
 
258
- // Pass Date static methods
300
+ // Date static methods
259
301
  const { exports } = jz(
260
302
  'import { now } from "date"; export let f = () => now()',
261
303
  { imports: { date: Date } }
262
304
  )
263
305
 
264
- // Pass window / globalThis
306
+ // window / globalThis
265
307
  const { exports } = jz(
266
308
  'import { parseInt } from "window"; export let f = () => parseInt("42")',
267
309
  { imports: { window: globalThis } }
268
310
  )
269
311
  ```
270
312
 
271
- ### Can two modules share data?
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.
314
+
315
+ </details>
316
+
317
+ <details>
318
+ <summary><strong>Can two modules share data?</strong></summary>
319
+
320
+ <br>
272
321
 
273
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:
274
323
 
@@ -291,10 +340,54 @@ memory.Array([1, 2, 3]) // → NaN-boxed pointer
291
340
 
292
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.
293
342
 
294
- 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>
295
350
 
351
+ <br>
296
352
 
297
- ### How do I run compiled WASM outside the browser?
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>
386
+
387
+ <details>
388
+ <summary><strong>How do I run compiled WASM outside the browser?</strong></summary>
389
+
390
+ <br>
298
391
 
299
392
  ```sh
300
393
  jz program.js -o program.wasm
@@ -305,40 +398,79 @@ wasmer run program.wasm
305
398
  deno run program.wasm
306
399
  ```
307
400
 
308
- `console.log` compiles to WASI `fd_write` works natively on wasmtime/wasmer/deno without polyfills.
401
+ Pure numeric modules have no imports and instantiate with standard
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.
309
403
 
404
+ Two host modes select how runtime services lower:
310
405
 
311
- ### What host features are supported?
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.
412
+
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'`.
415
+
416
+ </details>
417
+
418
+ <details>
419
+ <summary><strong>What host features are supported?</strong></summary>
420
+
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 |
312
432
 
313
- The compiled `.wasm` uses one import namespace:
433
+ The compiled `.wasm` uses at most one import namespace:
314
434
 
315
- - `wasi_snapshot_preview1`standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
435
+ - nonepure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
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.
316
438
 
317
- | JS API | Maps to | Notes |
318
- |--------|---------|-------|
319
- | `console.log()` | WASI `fd_write` (fd=1) | Multiple args space-separated, newline appended |
320
- | `console.warn()`, `console.error()` | WASI `fd_write` (fd=2) | Writes to stderr |
321
- | `Date.now()` | WASI `clock_time_get` (realtime) | Returns ms since epoch |
322
- | `performance.now()` | WASI `clock_time_get` (monotonic) | Returns ms, high-resolution |
323
- | `setTimeout`, `clearTimeout` | WASM timer queue + `__timer_tick` | JS runtime drives tick via `setInterval`; wasmtime uses blocking `__timer_loop` |
324
- | `setInterval`, `clearInterval` | WASM timer queue + `__timer_tick` | Same — native WASM implementation, no host imports |
439
+ </details>
325
440
 
326
- ### How do I add custom operators / extend the stdlib?
441
+ <details>
442
+ <summary><strong>How do I add custom operators / extend the stdlib?</strong></summary>
443
+
444
+ <br>
327
445
 
328
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:
329
447
 
330
448
  ```js
331
- import { emitter } from 'jz/src/compile.js'
449
+ import { emitter } from './src/emit.js'
450
+ import { typed } from './src/ir.js'
332
451
 
333
452
  // Register a custom operator: my.double(x) → x * 2
334
453
  emitter['my.double'] = (x) => {
335
- return ['f64.mul', ['f64.const', 2], x]
454
+ return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
336
455
  }
337
456
  ```
338
457
 
339
- 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 registers emitters and stdlib on `ctx`.
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>
340
472
 
341
- ### Can I compile jz to C?
473
+ <br>
342
474
 
343
475
  Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
344
476
 
@@ -347,23 +479,50 @@ jz program.js -o program.wasm
347
479
  wasm2c program.wasm -o program.c
348
480
  cc program.c -o program
349
481
  ```
482
+ </details>
350
483
 
351
484
 
352
485
  ## Benchmark
353
486
 
354
- | | **jz** | [Node](https://nodejs.org/) | [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/) | [Porffor](https://github.com/CanadaHonk/porffor) |
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/) |
355
488
  |---|---|---|---|---|---|---|---|---|---|---|
356
- | [**biquad**](bench/biquad/biquad.js) | **6.44ms**<br>**3.4kB** | 12.30ms<br>3.2kB | 9.04ms<br>1.9kB | 6.48ms<br>767 B | 5.43ms | 9.03ms<br>fma | 5.09ms | 5.33ms | 3.15s | — |
357
- | [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10ms**<br>**1.6kB** | 0.18ms<br>1.4kB | 0.08ms<br>1.5kB | | 0.13ms | 0.07ms | 0.12ms | 0.12ms | 5.21ms | 0.46ms<br>2.6kB |
358
- | [**mat4**](bench/mat4/mat4.js) | **4.00ms**<br>**1.7kB** | 11.64ms<br>1.1kB | 9.18ms<br>1.5kB | 7.99ms<br>353 B | 2.62ms | 11.93ms | 2.60ms | 0.80ms | 323.69ms | 87.65ms<br>2.3kB |
359
- | [**aos**](bench/aos/aos.js) | **1.50ms**<br>**2.3kB** | 1.81ms<br>1.1kB | 1.91ms<br>2.2kB | | 1.22ms | 0.90ms | 0.99ms | 1.20ms | 2.23ms | — |
360
- | [**bitwise**](bench/bitwise/bitwise.js) | **4.93ms**<br>**1.2kB** | 5.31ms<br>1005 B | 12.36ms<br>1.5kB | 4.96ms<br>355 B | 1.31ms | 5.24ms | 4.26ms | 1.30ms | 14.89ms | — |
361
- | [**poly**](bench/poly/poly.js) | **1.13ms**<br>**1.3kB** | 2.31ms<br>1014 B | 1.14ms<br>1.3kB | | 0.52ms | 0.80ms | | 0.52ms | 0.60ms | — |
362
- | [**callback**](bench/callback/callback.js) | **0.01ms**<br>**1.5kB** | 1.03ms<br>828 B | 1.48ms<br>1.9kB | | 0.09ms | 0.20ms | 0.01ms | 0.08ms | 1.84ms | — |
363
- | [**json**](bench/json/json.js) | **0.20ms**<br>**2.8kB** | 0.38ms<br>923 B | | | 0.02ms | 1.06ms | | 0.03ms | 1.19ms | |
364
- | [**watr**](bench/watr/watr.js) | **1.82ms**<br>**137.1kB** | 1.50ms<br>85.3kB | | — | — | | | | | |
365
-
366
- _Numbers from `node bench/bench.mjs` on Apple Silicon._
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
+ | [json-dynamic](bench/json-dynamic/json-dynamic.js) | 0.24ms<br>11.3kB | 0.39ms<br>1.2kB | — | — | — | — | — | — | — | — |
499
+ | [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
500
+
501
+ _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._
502
+
503
+ <details>
504
+ <summary><strong>Optimizations</strong></summary>
505
+
506
+ <br>
507
+ High-impact summary behind the benchmark table, not an exhaustive list.
508
+
509
+ | Optimization | Effect |
510
+ |---|---|
511
+ | Escape scalar replacement | Removes short-lived object/array literals before allocation. |
512
+ | Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
513
+ | Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
514
+ | 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. |
515
+ | 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. |
516
+ | Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
517
+ | 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. |
518
+ | SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
519
+ | Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
520
+ | OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
521
+ | Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
522
+
523
+ `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.
524
+
525
+ </details>
367
526
 
368
527
 
369
528
  ## 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