jz 0.4.0 → 0.5.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
@@ -2,10 +2,10 @@
2
2
 
3
3
 
4
4
 
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)
5
+ ## ![stability](https://img.shields.io/badge/stability-experimental-black) [![npm](https://img.shields.io/npm/v/jz?color=black)](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) [![test262](https://github.com/dy/jz/actions/workflows/test262.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/test262.yml) [![bench](https://github.com/dy/jz/actions/workflows/bench.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/bench.yml)
6
6
 
7
7
 
8
- **JZ** (_javascript zero_) is **minimal modern functional JS subset**, compiling to WASM.<br/>
8
+ **JZ** (_javascript zero_) is a **minimal modern functional JS subset** that compiles to WASM.<br>
9
9
 
10
10
  ```js
11
11
  import jz from 'jz'
@@ -17,8 +17,8 @@ dist(3, 4) // 5
17
17
 
18
18
  ## Why?
19
19
 
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.
20
+ **Write plain JS, compile to WASM** fast, portable and long-lasting.<br>
21
+ JZ distills modern JS to its functional core the "good parts" ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) without the legacy semantics, feature bloat, and perf quirks.
22
22
 
23
23
  * **Static AOT** – no runtime, no GC, no dynamic constructs.
24
24
  * **Valid jz = valid js** — test in browser, compile to wasm.
@@ -38,6 +38,8 @@ JZ distills the modern functional core – the "good parts" ([Crockford](https:/
38
38
 
39
39
  ## Usage
40
40
 
41
+ `npm install jz`
42
+
41
43
  ```js
42
44
  import jz, { compile } from 'jz'
43
45
 
@@ -63,16 +65,16 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
63
65
 
64
66
  | Option | Use |
65
67
  |---|---|
66
- | `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, and `undefined` by lowering them to the JZ subset. |
68
+ | `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, `undefined`, and `class` (see *Not supported* below for the class subset) by lowering them to the JZ subset. The CLI auto-enables this for `.js` files. |
67
69
  | `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
68
70
  | `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
69
71
  | `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
72
  | `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. String aliases `'size'` (unroll/vectorize off, tight scalar caps — smallest wasm), `'balanced'` (= default), `'speed'` (full unroll + SIMD). Object form overrides individual passes/knobs (and accepts `level:` as a number or alias base). |
73
+ | `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default (every stable jz pass + full watr), `3` adds larger array/hash initial caps and inlines `f64.const` over mutable globals (trades size for speed). String aliases `'size'` (unroll/vectorize off, tight scalar caps — smallest wasm), `'balanced'` (= default), `'speed'` (full unroll + SIMD). Object form overrides individual passes/knobs (and accepts `level:` as a number or alias base). |
72
74
  | `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
73
75
  | `alloc: false` | Omit raw allocator exports like `_alloc`/`_clear` when compiling standalone WASM that never marshals heap values across the host boundary. |
74
76
  | `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. |
77
+ | `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. |
76
78
 
77
79
  </details>
78
80
 
@@ -108,7 +110,8 @@ JZ is a strict functional JS subset. Built-in `jzify` transform extends support
108
110
  ┌────────────────────────────────────────────────────────────────────────┐
109
111
  │ JZify │
110
112
  │ var function arguments switch new Foo() │
111
- == != instanceof undefined
113
+ | class new this extends super static #private
114
+ │ == != instanceof undefined |
112
115
  │ │
113
116
  │ ┌────────────────────────────────────────────────────────────────────┐ │
114
117
  │ │ JZ │ │
@@ -123,404 +126,421 @@ JZ is a strict functional JS subset. Built-in `jzify` transform extends support
123
126
  └────────────────────────────────────────────────────────────────────────┘
124
127
  Not supported
125
128
  async/await Promise function* yield
126
- this class super extends delete labels
127
- eval Function with Proxy Reflect WeakMap WeakSet
128
- dynamic import DOM fetch Intl Node APIs
129
+ delete labels eval Function with
130
+ Proxy Reflect WeakMap WeakSet
131
+ import() DOM fetch Intl Node APIs
129
132
  ```
133
+
134
+ `jzify` covers most class syntax — fields, methods, `new`, `this`, `extends`, `super.method()`, `#private`, constant computed member names. Getters/setters, bare `super.x` reads, and dynamic computed names are rejected with a clear message.
135
+
130
136
  ## FAQ
131
137
 
132
138
  <details>
133
- <summary><strong>How to pass data between JS and WASM?</strong></summary>
139
+ <summary><strong>Where does jz differ from JavaScript?</strong></summary>
134
140
 
135
141
  <br>
136
142
 
143
+ `Valid jz = valid JS` means jz source always parses and runs as JS — but jz compiles to *static* WASM, so a handful of behaviors diverge from V8. These are deliberate trades, not unfinished corners: each one is what keeps the output close to hand-written WAT. `--wat` shows exactly what was emitted. (For what's out of scope entirely — `eval`, `async`, `Proxy`, … — see the *Not supported* box above; for moving values across the boundary, see [Interop](#interop).)
137
144
 
138
- 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:
139
-
140
- > [!WARNING] jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
141
- > `memory.Object({ x: 3, y: 4 })` expects the same key order as the jz source `{ x, y }`.
142
- > `{ y: 4, x: 3 }` with reversed keys will produce wrong values.
143
-
144
- ```js
145
- const { exports, memory } = jz`
146
- export let greet = (s) => s.length
147
- export let sum = (a) => a.reduce((s, x) => s + x, 0)
148
- export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
149
- export let rgb = (c) => [c, c * 0.5, c * 0.2]
150
- export let process = (buf) => buf.map(x => x * 2)
151
- `
152
-
153
- // JS → WASM (write)
154
- memory.String('hello') // → string pointer
155
- memory.Array([1, 2, 3]) // → array pointer
156
- memory.Float64Array([1.0, 2.0]) // → typed array pointer
157
- memory.Int32Array([10, 20, 30]) // all typed array constructors available
145
+ - **A boolean can surface as `1`/`0` at the host boundary.** `typeof`, `String`, `JSON.stringify`, and a directly-returned comparison all hand back a real boolean but a boolean produced by value-preserving `&&`/`||`, or read bare from an untyped container, crosses as the numeric carrier `1`/`0`.
146
+ - **Objects are fixed-layout schemas** — key set and order fixed at the literal; `delete` is rejected; `memory.Object({…})` must match the source key order.
147
+ - **Errors are untagged** `throw` carries a value, not a typed `Error`; `e instanceof TypeError` does not discriminate.
148
+ - **`Set`/`Map` iterate slot order**, not insertion order.
149
+ - **Memory is not reclaimed automatically** see *How does memory work?* below.
158
150
 
159
- // Objects: keys and order must match the jz source declaration.
160
- // jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
161
- // If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
162
- memory.Object({ x: 3, y: 4 }) // → object pointer
151
+ For full TC39 conformance use [porffor](https://github.com/CanadaHonk/porffor); jz trades completeness for low-level numeric performance by design.
163
152
 
164
- // Strings/arrays inside objects are auto-wrapped to pointers:
165
- memory.Object({ name: 'jz', count: 3 }) // name auto-wrapped via memory.String
153
+ </details>
166
154
 
167
- // Call with pointers
168
- exports.greet(memory.String('hello')) // 5
169
- exports.sum(memory.Array([1, 2, 3])) // 6
170
- exports.dist(memory.Object({ x: 3, y: 4 })) // 5
155
+ <details>
156
+ <summary><strong>Can I use npm packages or existing JS libraries?</strong></summary>
171
157
 
172
- // direct JS array return
173
- exports.rgb(100) // [100, 50, 20]
158
+ <br>
174
159
 
175
- // read pointer value
176
- memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
177
- ```
160
+ Only code that fits the jz subset. There's no runtime, so packages touching the DOM, `async`/`Promise`, the network, or Node APIs won't compile — but pure numeric/algorithmic source does.
178
161
 
179
- Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
162
+ - **Relative imports** (`./dep.js`) bundle at compile time.
163
+ - **Bare specifiers** (`import { x } from "pkg"`) resolve through Node module resolution only with the `--resolve` CLI flag, or by passing the source yourself via `{ modules }`. The package's source still has to be valid jz.
180
164
 
181
- ```js
182
- jz`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x`
183
- ```
165
+ jz is for compiling *your* numeric/DSP/parser code, not for running the npm ecosystem.
184
166
 
185
- <!--
186
- ### How does everything fit in f64?
167
+ </details>
187
168
 
188
- All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. Heap types use [NaN-boxing](https://nachtimwald.com/2019/11/06/nan-boxing/): quiet NaN (`0x7FF8`) + 51-bit payload `[type:4][aux:15][offset:32]`.
169
+ <details>
170
+ <summary><strong>Can I use import/export to split code?</strong></summary>
189
171
 
190
- | Type | Code | Payload | Example |
191
- |------|------|---------|---------|
192
- | Number | — | regular f64 | `3.14`, `42`, `NaN` |
193
- | Null | 0 | reserved pattern | `null` (distinct from `0` and `NaN`) |
194
- | Array | 1 | aux=length, offset=heap | `[1, 2, 3]` |
195
- | ArrayBuffer | 2 | offset=heap | `new ArrayBuffer(16)` |
196
- | TypedArray | 3 | aux=elemType, offset=heap | `new Float64Array(n)` |
197
- | String | 4 | offset=heap | `"hello world"` (>4 chars) |
198
- | SSO String | 5 | aux=packed chars | `"hi"` (<=4 ASCII chars, zero alloc) |
199
- | Object | 6 | aux=schemaId, offset=heap | `{x: 1, y: 2}` |
200
- | Hash | 7 | offset=heap | dynamic string-keyed objects |
201
- | Set | 8 | offset=heap | `new Set()` |
202
- | Map | 9 | offset=heap | `new Map()` |
203
- | Closure | 10 | aux=funcIdx, offset=env | `x => x + captured` |
204
- | External | 11 | offset=hostMap index | JS host object references |
172
+ <br>
205
173
 
206
- **Why NaN-boxing?** used by LuaJIT, JavaScriptCore, SpiderMonkey. The alternatives tagged unions (OCaml, Haskell), pointer tagging (V8 Smis), or separate type+value pairs all require branching at call boundaries or multi-word passing. NaN-boxing fits any value in one 64-bit word: one calling convention, one memory layout, one comparison instruction.
174
+ Yes. Standard `import`/`export` syntax is bundled at compile time into a single WASMno runtime module resolution.
207
175
 
208
- **The f64 tradeoff**: f64 arithmetic is ~1.2x slower than i32 for pure integer work on most architectures. jz mitigates this — `analyzeLocals` preserves i32 for loop counters, bitwise ops, and comparisons, so the penalty only applies to mixed-type parameters. The gain: zero interop cost at the JS↔WASM boundary (f64 is WASM's native JS-compatible type), no marshaling, no boxing/unboxing. For jz's target workloads (DSP, typed arrays, math), f64 is the natural type anyway.
176
+ ```js
177
+ const { exports } = jz(
178
+ 'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
179
+ { modules: { './math.jz': 'export let add = (a, b) => a + b' } }
180
+ )
181
+ ```
209
182
 
210
- **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.
211
- -->
183
+ Transitive imports work (main math utils …); circular imports error at compile time. The **CLI** resolves filesystem imports automatically (`jz main.jz -o main.wasm` reads `./math.jz` etc.). In the **browser**, fetch sources yourself and pass them via `{ modules }`the compiler stays synchronous and pure, no I/O.
212
184
 
213
185
  </details>
214
186
 
215
187
  <details>
216
- <summary><strong>How does template interpolation work?</strong></summary>
188
+ <summary><strong>How does memory work? How do I reset it?</strong></summary>
217
189
 
218
190
  <br>
219
191
 
220
- 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:
192
+ jz uses a **bump allocator**: every heap value (string, array, object, typed array) bumps a single pointer forward no free list, no GC. The heap starts at byte 1024 and grows the WASM memory automatically when full.
221
193
 
222
- ```js
223
- jz`export let f = () => ${'hello'}.length` // 5 — string compiled as literal
224
- jz`export let f = () => ${[10, 20, 30]}[1]` // 20 — array compiled as literal
225
- jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3 — object compiled as literal
194
+ So **memory is never reclaimed implicitly** — a long-running program that allocates per call grows without bound. Reset the heap pointer between independent batches:
226
195
 
227
- // Nested values work too
228
- jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
196
+ ```js
197
+ for (let i = 0; i < 1000; i++) {
198
+ const sum = exports.process(100) // allocates an array each call
199
+ memory.reset() // drop everything; heap ptr → 1024
200
+ }
229
201
  ```
230
202
 
231
- Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
203
+ After `memory.reset()` all previously returned pointers are invalid read what you need first, then reset. For finer control, `memory.alloc(bytes)` returns a raw offset on the same pointer. Pure scalar modules (no heap values) compile without the allocator at all. The low-level export/encoding contract is in [Interop](#interop).
232
204
 
233
205
  </details>
234
206
 
235
207
  <details>
236
- <summary><strong>Does it support ES module imports?</strong></summary>
208
+ <summary><strong>How do I see and control inferred types?</strong></summary>
237
209
 
238
210
  <br>
239
211
 
240
- Yesstandard ES `import` syntax is bundled at compile-time into a single WASM.
212
+ Inference is mechanical and visible not the hidden, fragile, coercive thing the "explicit > implicit" reflex assumes. It reads the same signals a human reader does: literals, operators (`x | 0` i32), member access (`s.length` → string), `typeof` guards, and assignment flow. The chosen types appear in `--wat`; ambiguous cases fall back to NaN-boxed **f64** a safe default, never a wrong type.
241
213
 
242
- ```js
243
- const { exports } = jz(
244
- 'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
245
- { modules: { './math.jz': 'export let add = (a, b) => a + b' } }
246
- )
247
- ```
214
+ So there's nothing to annotate. Type annotations bundle two jobs into one syntax: hinting storage to the compiler (`let x: number` — which only duplicates what `x | 0` already tells inference) and documenting contracts at boundaries (a *docs* concern, not a *language* one). jz keeps the split clean — inference handles storage, and **valid jz = valid JS** means no parallel type system to learn. To pin a type, write code that implies it: `x | 0` keeps `x` an i32; an `s = ''` default declares a string param. (JSDoc `@type` is planned as an advisory hint, not yet enforced.) Annotations never make code faster; they only sharpen what inference already sees.
248
215
 
249
- Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
216
+ </details>
250
217
 
251
- **CLI** resolves filesystem imports automatically.
218
+ <details>
219
+ <summary><strong>Is it production-ready?</strong></summary>
252
220
 
253
- ```sh
254
- jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
255
- ```
221
+ <br>
256
222
 
257
- **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure no I/O.
223
+ It's **experimental** (`0.4.0`) the compiler API and option names may still change. What's stable is the *output*: jz emits deterministic WASM, gated on every push by test262, a differential fuzzer (`test/differential.js` runs jz-compiled wasm against the same source as plain JS), and the size/speed bench.
258
224
 
259
- ```js
260
- // Transitive bundling — all merged into one WASM
261
- const { exports } = jz(mainSrc, { modules: {
262
- './math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
263
- // Fetch sources yourself, pass them in
264
- './utils.jz': await fetch('./util.jz').then(r => r.text())
265
- } })
266
- ```
225
+ The robust way to depend on jz today: compile to `.wasm` at build time, commit the binary, and load it through the dependency-free [`jz/interop`](#interop) bridge — your app then rides on the WASM, not on the compiler's evolving API.
267
226
 
268
227
  </details>
269
228
 
270
229
  <details>
271
- <summary><strong>How do I pass values from the host to jz?</strong></summary>
230
+ <summary><strong>Why jz over Porffor or AssemblyScript?</strong></summary>
272
231
 
273
232
  <br>
274
233
 
275
- 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:
234
+ Pick jz for plain JS that fits the subset and tiny, native-fast numeric/DSP WASM. For full TC39, a typed TypeScript dialect, or running standard JS unchanged, see the [Alternatives](#alternatives) decision table (porffor / AssemblyScript / jawsm).
276
235
 
277
- ```js
278
- // Custom function
279
- const { exports } = jz(
280
- 'import { log } from "host"; export let f = (x) => { log(x); return x }',
281
- { imports: { host: { log: console.log } } }
282
- )
236
+ </details>
283
237
 
284
- // Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
285
- const { exports } = jz(
286
- 'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
287
- { imports: { math: Math } }
288
- )
238
+ <details>
239
+ <summary><strong>Can I compile jz to C?</strong></summary>
289
240
 
290
- // Date static methods
291
- const { exports } = jz(
292
- 'import { now } from "date"; export let f = () => now()',
293
- { imports: { date: Date } }
294
- )
241
+ <br>
295
242
 
296
- // window / globalThis
297
- const { exports } = jz(
298
- 'import { parseInt } from "window"; export let f = () => parseInt("42")',
299
- { imports: { window: globalThis } }
300
- )
243
+ Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
244
+
245
+ ```sh
246
+ jz program.js -o program.wasm
247
+ wasm-opt -O3 program.wasm -o program.opt.wasm # trims redundant locals/loads first
248
+ wasm2c program.opt.wasm -o program.c
249
+ cc -O3 program.c -o program
301
250
  ```
302
251
 
303
- 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.
252
+ The full native pipeline (jz `wasm-opt -O3` `wasm2c` → `clang -O3 -flto` + PGO) lands within a few percent of hand-tuned C — beating V8 on 19 of 21 bench cases on an M4 Max. Details and the regression gate live in [`scripts/native/README.md`](scripts/native/README.md).
304
253
 
305
254
  </details>
306
255
 
307
256
  <details>
308
- <summary><strong>Can two modules share data?</strong></summary>
257
+ <summary><strong>Can I add my own operators or stdlib methods?</strong></summary>
309
258
 
310
259
  <br>
311
260
 
312
- Yes — `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate automatically, so objects created in one module are readable by another:
261
+ Yes — jz's emitter table (`ctx.core.emit`) maps AST operators to WASM IR generators, and the whole stdlib is just modules registering on it. Adding one is the same move the built-ins make:
313
262
 
314
263
  ```js
315
- const memory = jz.memory()
264
+ import { emitter } from './src/emit.js'
265
+ import { typed } from './src/ir.js'
316
266
 
317
- const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
318
- const b = jz('export let read = (o) => o.x + o.y', { memory })
267
+ // my.double(x) x * 2
268
+ emitter['my.double'] = (x) => ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
269
+ ```
270
+
271
+ Handler names follow the AST path: `Math.sin` → `math.sin`, `arr.push` → `.push`, typed variants like `.f64:push`. Any file in [`module/`](module/) is a worked template — each receives `ctx` and registers emitters, stdlib, globals, or helpers. See [CONTRIBUTING.md](CONTRIBUTING.md) for the pipeline.
319
272
 
320
- // Object from module a, processed by module b — same memory, merged schemas
321
- b.exports.read(a.exports.make()) // 30
273
+ </details>
322
274
 
323
- // Read from JS too — memory knows all schemas
324
- memory.read(a.exports.make()) // {x: 10, y: 20}
325
275
 
326
- // Write from JS before any compilation
327
- memory.String('hello') // → NaN-boxed pointer
328
- memory.Array([1, 2, 3]) // → NaN-boxed pointer
329
- ```
276
+ ## Benchmark
330
277
 
331
- `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.
278
+ | | 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/) |
279
+ |---|---|---|---|---|---|---|---|---|---|---|
280
+ | [biquad](bench/biquad/biquad.js) | 6.50ms<br>3.4kB | 12.35ms<br>3.2kB | fails | 9.03ms<br>1.9kB | 6.49ms<br>767 B | 5.30ms | 8.96ms<br>fma | 5.04ms | 5.27ms | 3.09s |
281
+ | [mat4](bench/mat4/mat4.js) | 2.74ms<br>3.3kB | 11.96ms<br>1.2kB | 88.68ms<br>2.4kB<br>diff | 9.32ms<br>1.6kB | 8.12ms<br>414 B | 2.76ms | 12.51ms | 2.74ms | 1.78ms | 389.44ms |
282
+ | [poly](bench/poly/poly.js) | 0.37ms<br>1.2kB | 2.32ms<br>1014 B | fails | 1.15ms<br>1.3kB | 0.81ms<br>359 B | 0.52ms | 0.80ms | 0.80ms | 0.57ms | 0.61ms |
283
+ | [bitwise](bench/bitwise/bitwise.js) | 1.40ms<br>1.2kB | 5.32ms<br>1005 B | fails | 12.13ms<br>1.5kB | 4.88ms<br>355 B | 1.30ms | 5.23ms | 4.16ms | 1.30ms | 14.77ms |
284
+ | [tokenizer](bench/tokenizer/tokenizer.js) | 0.10ms<br>1.7kB | 0.21ms<br>2.0kB | 0.41ms<br>3.2kB | 0.08ms<br>1.6kB | 0.10ms<br>344 B | 0.13ms | 0.08ms | 0.14ms | 0.12ms | 5.13ms |
285
+ | [callback](bench/callback/callback.js) | 0.03ms<br>1.4kB | 0.88ms<br>828 B | fails | 1.49ms<br>1.9kB | 0.25ms<br>267 B | 0.10ms | 0.20ms | 0.01ms | 0.09ms | 1.81ms |
286
+ | [aos](bench/aos/aos.js) | 1.62ms<br>1.8kB | 1.82ms<br>1.1kB | fails | 1.91ms<br>2.2kB | 1.07ms<br>481 B | 1.20ms | 0.90ms | 0.90ms | 1.20ms | 2.55ms |
287
+ | [mandelbrot](bench/mandelbrot/mandelbrot.js) | 12.55ms<br>1.0kB | 13.80ms<br>1.8kB | 13.47ms<br>3.0kB | 12.42ms<br>1.3kB | — | 12.26ms | 12.46ms | 12.31ms | 12.23ms | — |
288
+ | [json](bench/json/json.js) | 0.23ms<br>7.7kB | 0.38ms<br>1.2kB | fails | — | — | 0.21ms | 1.17ms | 0.69ms | 0.68ms | 1.20ms |
289
+ | [sort](bench/sort/sort.js) | 5.96ms<br>1.6kB | 11.13ms<br>1.6kB | fails | 10.22ms<br>1.9kB | — | 8.85ms | 10.36ms | 8.84ms | 9.37ms | 5.05ms |
290
+ | [crc32](bench/crc32/crc32.js) | 12.12ms<br>1.2kB | 13.43ms<br>1.8kB | 80.76ms<br>3.1kB | 12.19ms<br>1.4kB | — | 10.69ms | 9.30ms | 9.45ms | 9.38ms | 0.24ms |
291
+ | [watr](bench/watr/watr.js) | 1.56ms<br>144.4kB | 1.45ms<br>2.6kB | fails | — | — | — | — | — | — | — |
332
292
 
333
- 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.
334
293
 
294
+ _Per-case median speed / wasm size from `node bench/bench.mjs` on Apple Silicon (arm64); the **geomean** row is the gated cross-case jz/target ratio from `test/bench.js`._
295
+
296
+ Geomean size: jz **0.86× AS**. jz wasm runs at `clang -O3` speed — native-C parity at geomean 0.96× — and `test/bench.js` gates every figure so a regression fails CI.
335
297
 
336
- </details>
337
298
 
338
299
  <details>
339
- <summary><strong>How does memory work? How do I reset it?</strong></summary>
300
+ <summary><strong>Optimizations</strong></summary>
340
301
 
341
302
  <br>
303
+ High-impact summary behind the benchmark table, not an exhaustive list.
342
304
 
343
- 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.
305
+ | Optimization | Effect |
306
+ |---|---|
307
+ | Escape scalar replacement | Removes short-lived object/array literals before allocation. |
308
+ | Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
309
+ | Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
310
+ | 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. |
311
+ | 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. |
312
+ | Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
313
+ | 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. |
314
+ | SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
315
+ | Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
316
+ | OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
317
+ | Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
344
318
 
345
- 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:
319
+ `npm run test:bench` 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.
346
320
 
347
- ```js
348
- const { exports, memory } = jz`
349
- export let process = (n) => {
350
- let xs = []
351
- for (let i = 0; i < n; i++) xs.push(i * 2)
352
- return xs.reduce((s, x) => s + x, 0)
353
- }
354
- `
321
+ #### Making array loops vectorize
355
322
 
356
- for (let i = 0; i < 1000; i++) {
357
- const sum = exports.process(100) // allocates an array each call
358
- memory.reset() // drop everything; heap ptr → 1024
359
- }
360
- ```
323
+ The lane-local vectorizer (on at default `optimize: 2`) lifts inner loops of shape `for (let i=0;i<N;i++) arr[i] = f(arr[i], …)` to SIMD-128 when the body is lane-pure (the k-th output depends only on the k-th inputs).
361
324
 
362
- After `memory.reset()` all previously returned pointers are invalid read what you need first, then reset.
325
+ **Lifts:** in-place maps (`a[i] = a[i] * 2`), cross-array maps (`b[i] = a[i] * k + c`), **structure-of-arrays** (`zs[i] = xs[i]*a + ys[i]*b`, up to 4 base pointers), and reductions (`s += a[i]`, `h ^= a[i]`, `|`, `&`).
363
326
 
364
- 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.
327
+ **Doesn't lift:** **array-of-structures** (interleaved `a[i*3]`, `a[i*3+1]` stride exceeds lane width; split into one typed array per field), loop-carried scalars (`s ^= s << 13`), stencils (`a[i] = a[i] + a[i-1]`), unbounded loops, mixed lane types in one body.
365
328
 
366
- **Non-JS hosts** (wasmtime, wasmer, deno, EdgeJS, embedded WASM) get the same allocator via two exports:
329
+ Check with `--wat`: a successful lift adds a `$__simd_loop<N>` block ahead of the scalar tail. No block means the recognizer bailed — usually a loop-carried local or a non-`(base + i<<K)` address.
367
330
 
368
- ```
369
- (func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
370
- (func $_clear) ;; rewinds heap pointer to 1024
371
- ```
331
+ </details>
372
332
 
373
- `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.
374
333
 
375
- </details>
334
+ ## Interop
376
335
 
377
- <details>
378
- <summary><strong>How do I run compiled WASM outside the browser?</strong></summary>
336
+ How values cross the JS↔WASM boundary, and how to ship and run the compiled `.wasm`. The mental model is simple: numbers pass straight through, and anything heap-allocated — strings, arrays, objects — crosses as a pointer the `memory` helper reads and writes for you. Under the hood that pointer is a **NaN-boxed `f64`** into a bump-allocated heap, one boundary codec per binary, fixed at compile time.
379
337
 
380
- <br>
338
+ ### Passing data in and out
381
339
 
382
- ```sh
383
- jz program.js -o program.wasm
340
+ Arrays of ≤ 8 elements come back as plain JS arrays (WASM multi-value); everything else stays heap-resident behind a pointer.
384
341
 
385
- # Run with any WASM runtime
386
- wasmtime program.wasm # WASI support built in
387
- wasmer run program.wasm
388
- deno run program.wasm
342
+ ```js
343
+ const { exports, memory } = jz`
344
+ export let greet = (s) => s.length
345
+ export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
346
+ export let rgb = (c) => [c, c * 0.5, c * 0.2]
347
+ export let process = (buf) => buf.map(x => x * 2)
348
+ `
349
+
350
+ // JS → WASM (write)
351
+ memory.String('hello') // → string pointer
352
+ memory.Array([1, 2, 3]) // → array pointer
353
+ memory.Float64Array([1, 2]) // → typed array pointer (all TypedArray ctors available)
354
+ memory.Object({ x: 3, y: 4 }) // → object pointer (see warning)
355
+
356
+ // Call with pointers
357
+ exports.greet(memory.String('hello')) // 5
358
+ exports.dist(memory.Object({ x: 3, y: 4 })) // 5
359
+ exports.rgb(100) // [100, 50, 20] — direct JS array return
360
+ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
389
361
  ```
390
362
 
391
- Pure numeric modules have no imports and instantiate with standard
392
- `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.
363
+ > [!WARNING] jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
364
+ > `memory.Object({ x: 3, y: 4 })` must use the same key order as the jz source `{ x, y }` reversed keys produce wrong values. Strings/arrays inside objects are auto-wrapped to pointers.
365
+
366
+ ### Template interpolation
393
367
 
394
- Two host modes select how runtime services lower:
368
+ Interpolated values are baked into the source at compile time — no post-instantiation allocation, no getter overhead. Numbers and booleans inline directly; strings, arrays, and objects compile as jz literals:
395
369
 
396
370
  ```js
397
- jz.compile(code) // host: 'js' (default) env.* imports
398
- jz.compile(code, { host: 'wasi' }) // wasi_snapshot_preview1.* imports
371
+ jz`export let f = () => ${'hello'}.length` // 5
372
+ jz`export let f = () => ${[10, 20, 30]}[1]` // 20
373
+ jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3
399
374
  ```
400
375
 
401
- `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.
376
+ Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
402
377
 
403
- `host: 'wasi'` `console.log` compiles to WASI `fd_write`, clocks to
404
- `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'`.
378
+ ### Calling host functions
405
379
 
406
- </details>
380
+ Any host namespace — functions, constants, custom objects — wires in via the `imports` option. jz extracts names via `Object.getOwnPropertyNames`, so non-enumerable built-ins (`Math.sin`, `Date.now`) work automatically:
407
381
 
408
- <details>
409
- <summary><strong>What host features are supported?</strong></summary>
382
+ ```js
383
+ // Custom function
384
+ jz('import { log } from "host"; export let f = (x) => { log(x); return x }',
385
+ { imports: { host: { log: console.log } } })
410
386
 
411
- <br>
387
+ // Whole namespace — sin, cos, PI, … all auto-wired
388
+ jz('import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
389
+ { imports: { math: Math } })
390
+
391
+ // globalThis works too
392
+ jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
393
+ { imports: { window: globalThis } })
394
+ ```
395
+
396
+ ### Host features & runtime services
397
+
398
+ Two host modes select how runtime services lower. `host: 'js'` (default) imports small `env.*` services that `jz()` auto-wires; `host: 'wasi'` emits WASI Preview 1 for wasmtime/wasmer/deno.
412
399
 
413
400
  | JS API | `host: 'js'` (default) | `host: 'wasi'` |
414
401
  |---|---|---|
415
- | `console.log()` | `env.print(val: i64, fd: i32, sep: i32)` — host stringifies | WASI `fd_write` (fd=1), space-separated, newline appended |
402
+ | `console.log()` | `env.print(val, fd, sep)` — host stringifies | WASI `fd_write` (fd=1), space-separated, newline |
416
403
  | `console.warn`/`error` | same, fd=2 | WASI `fd_write` (fd=2) |
417
- | `Date.now()` | `env.now(0) -> f64` (epoch ms) | `clock_time_get` (realtime) |
418
- | `performance.now()` | `env.now(1) -> f64` (monotonic ms) | `clock_time_get` (monotonic) |
419
- | `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) |
420
- | `setInterval`/`clearInterval` | same `env.setTimeout` (repeat=1) / `env.clearTimeout` | WASM timer queue + `__timer_tick` |
404
+ | `Date.now()` | `env.now(0) f64` (epoch ms) | `clock_time_get` (realtime) |
405
+ | `performance.now()` | `env.now(1) f64` (monotonic ms) | `clock_time_get` (monotonic) |
406
+ | `setTimeout`/`setInterval` | `env.setTimeout(cb, delay, repeat)` — host schedules; fires via `__invoke_closure` | WASM timer queue + `__timer_tick` |
421
407
  | dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
422
408
 
423
- The compiled `.wasm` uses at most one import namespace:
409
+ The compiled `.wasm` carries at most one import namespace — none, `env`, or `wasi_snapshot_preview1` — matching the mode above. `host: 'gc'` is reserved for a planned wasm-gc backend and errors today; pair `host: 'wasi'` with `strict: true` to also fail dynamic `obj[k]`/unknown-receiver calls at compile time.
424
410
 
425
- - none pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
426
- - `env` — JS-host services (default). Auto-wired by the `jz()` runtime.
427
- - `wasi_snapshot_preview1` — standard WASI Preview 1. Run natively on wasmtime/wasmer/deno.
411
+ ### Sharing memory across modules
428
412
 
429
- </details>
413
+ `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate, so objects created in one module are readable by another:
430
414
 
431
- <details>
432
- <summary><strong>How do I add custom operators / extend the stdlib?</strong></summary>
415
+ ```js
416
+ const memory = jz.memory()
417
+ const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
418
+ const b = jz('export let read = (o) => o.x + o.y', { memory })
433
419
 
434
- <br>
420
+ b.exports.read(a.exports.make()) // 30 — same memory, merged schemas
421
+ memory.read(a.exports.make()) // {x: 10, y: 20} — JS reads it too
422
+ ```
423
+
424
+ `jz.memory()` returns a real `WebAssembly.Memory` patched with `.read()`/`.String()`/`.Array()`/`.Object()`/`.write()`. Pass an existing one to wrap it: `jz.memory(new WebAssembly.Memory({ initial: 4 }))`. Modules sharing a memory share one bump allocator. Use `.instance.exports` for raw pointers, `.exports` for the JS-wrapped surface.
425
+
426
+ ### Shipping & running the `.wasm`
435
427
 
436
- 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:
428
+ Compile once, then run the binary anywhere.
429
+
430
+ **JS host, no compiler.** `jz/interop` is a dependency-free bridge (only `wasi.js`) that knows the value encoding, so bundlers tree-shake the compiler, parser, and watr out entirely:
437
431
 
438
432
  ```js
439
- import { emitter } from './src/emit.js'
440
- import { typed } from './src/ir.js'
433
+ import { instantiate } from 'jz/interop'
434
+ import wasmBytes from './program.wasm' // bundler-specific; or fetch(...)
441
435
 
442
- // Register a custom operator: my.double(x) → x * 2
443
- emitter['my.double'] = (x) => {
444
- return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
445
- }
436
+ const { exports, memory } = instantiate(wasmBytes)
437
+ exports.greet(memory.String('hello')) // marshal works exactly as at compile time
446
438
  ```
447
439
 
448
- 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.
440
+ `instantiate(wasm, opts?)` accepts `Uint8Array`, `ArrayBuffer`, or a prebuilt `WebAssembly.Module` and returns the same `{ exports, memory, instance, module }` shape as the `jz(src)` tag same `memory.String/Array/Object/...` constructors, same `memory.read(ptr)` decoder.
449
441
 
450
- Inside a runtime module, import directly from the layer you need:
442
+ **Native runtimes.** Compile with `host: 'wasi'` and run on any WASM runtime:
451
443
 
452
- ```js
453
- import { emit } from '../src/emit.js'
454
- import { asF64, temp } from '../src/ir.js'
455
- import { valTypeOf, VAL } from '../src/analyze.js'
444
+ ```sh
445
+ jz program.js --host wasi -o program.wasm
446
+ wasmtime program.wasm # also `wasmer run` / `deno run`
456
447
  ```
457
448
 
458
- </details>
449
+ Pure numeric modules have no imports and instantiate with standard `WebAssembly.Module`/`Instance` — the right shape for JS hosts such as EdgeJS. Compile at startup or build time and reuse the module; don't compile jz source per request.
450
+
451
+ **Memory ABI (non-JS hosts).** The allocator is exposed as two exports:
452
+
453
+ ```
454
+ (func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
455
+ (func $_clear) ;; rewinds heap pointer to 1024
456
+ ```
457
+
458
+ `memory.alloc()`/`memory.reset()` are JS 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 boundary pointer 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. Strip both exports with `compile(code, { alloc: false })` if you only call functions and never marshal heap values across the boundary.
459
459
 
460
460
  <details>
461
- <summary><strong>Can I compile jz to C?</strong></summary>
461
+ <summary><strong>Zero-copy strings</strong></summary>
462
462
 
463
463
  <br>
464
464
 
465
- Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
465
+ Strings have two boundary carriers; the compiler picks per export-param:
466
466
 
467
- ```sh
468
- jz program.js -o program.wasm
469
- wasm2c program.wasm -o program.c
470
- cc program.c -o program
467
+ | carrier | when | what crosses | per-call cost |
468
+ |---|---|---|---|
469
+ | **f64 / SSO** (default) | every param unless the narrower can prove it is used purely as a string | a NaN-boxed `f64` → UTF-8 bytes in linear memory; ≤4 ASCII chars inline in the NaN payload (SSO) | one `_alloc` + memcpy |
470
+ | **externref / `wasm:js-string`** | param uses only `.length`/bounded `.charCodeAt(i)`, isn't reassigned/captured/escaped, *and* has either a `.charCodeAt` use, call-site STRING evidence, or a `s = ''` default | the JS string itself, by reference | **zero** — lowers to [`wasm:js-string`](https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md) builtins the engine inlines |
471
+
472
+ ```js
473
+ const { exports } = jz`
474
+ // Opt-in fires: .charCodeAt in a bounded loop discriminates string.
475
+ export let sum = (s) => { let n = 0; for (let i = 0; i < s.length; i++) n += s.charCodeAt(i); return n }
476
+ // Opt-in fires: 's = ""' default declares string intent.
477
+ export let len = (s = '') => s.length
478
+ // Opt-in declines: '+' isn't a builtin; param escapes into the f64 op.
479
+ export let label = (s) => s + ' (ok)'
480
+ `
481
+ exports.sum('hello') // 532 — JS string passed by reference
482
+ exports.len() // 0 — default substituted JS-side
483
+ exports.label('test') // 'test (ok)' — memory-backed string, as before
471
484
  ```
472
- </details>
473
485
 
486
+ **Why `.length`-only doesn't flip.** `.length` also reads arrays and typed arrays, so keeping it on f64 preserves that tolerant polymorphism — flipping would trap on non-strings. **Why bounded loops matter.** `wasm:js-string.charCodeAt` **traps** out of range where JS returns `NaN`, so the narrower proves `i < s.length` before flipping.
474
487
 
475
- ## Benchmark
488
+ Native `wasm:js-string` lands in V8 17+ (Chrome 134+, Node 25+ via the `{ builtins: ['js-string'] }` Module option), Safari 18.4+, Firefox behind a flag. `jz/interop` probes the engine and either passes the option for native inlining or attaches a JS polyfill — either way the boundary string-copy is saved. Opt out with `optimize: { jsstring: false }`. Bench: `node bench/jsstring/bench-jsstring.mjs`.
476
489
 
477
- | | 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/) |
478
- |---|---|---|---|---|---|---|---|---|---|---|
479
- | [biquad](bench/biquad/biquad.js) | 6.40ms<br>4.1kB | 12.02ms<br>3.2kB | fails | 8.88ms<br>1.9kB | 6.44ms<br>767 B | 5.30ms | 8.96ms<br>fma | 5.04ms | 5.27ms | 3.09s |
480
- | [tokenizer](bench/tokenizer/tokenizer.js) | 0.10ms<br>1.9kB | 0.20ms<br>2.0kB | 0.41ms<br>3.2kB | 0.08ms<br>1.6kB | 0.10ms<br>344 B | 0.13ms | 0.08ms | 0.14ms | 0.12ms | 5.13ms |
481
- | [mat4](bench/mat4/mat4.js) | 2.12ms<br>3.0kB | 11.90ms<br>1.2kB | 88.68ms<br>2.4kB<br>diff | 9.44ms<br>1.6kB | 8.10ms<br>414 B | 2.76ms | 12.51ms | 2.74ms | 1.78ms | 389.44ms |
482
- | [aos](bench/aos/aos.js) | 1.61ms<br>2.0kB | 1.82ms<br>1.1kB | fails | 1.91ms<br>2.2kB | 1.07ms<br>481 B | 1.20ms | 0.90ms | 0.90ms | 1.20ms | 2.55ms |
483
- | [mandelbrot](bench/mandelbrot/mandelbrot.js) | 12.24ms<br>1.2kB | 13.53ms<br>1.8kB | 13.47ms<br>3.0kB | 12.23ms<br>1.3kB | — | 12.26ms | 12.46ms | 12.31ms | 12.23ms | — |
484
- | [bitwise](bench/bitwise/bitwise.js) | 1.62ms<br>1.3kB | 5.25ms<br>1005 B | fails | 11.97ms<br>1.5kB | 4.95ms<br>355 B | 1.30ms | 5.23ms | 4.16ms | 1.30ms | 14.77ms |
485
- | [poly](bench/poly/poly.js) | 0.37ms<br>1.4kB | 2.29ms<br>1014 B | fails | 1.13ms<br>1.3kB | 0.81ms<br>359 B | 0.52ms | 0.80ms | 0.80ms | 0.57ms | 0.61ms |
486
- | [callback](bench/callback/callback.js) | 0.05ms<br>1.5kB | 0.86ms<br>828 B | fails | 1.48ms<br>1.9kB | 0.25ms<br>267 B | 0.10ms | 0.20ms | 0.01ms | 0.09ms | 1.81ms |
487
- | [json](bench/json/json.js) | 0.23ms<br>10.9kB | 0.37ms<br>1.2kB | fails | — | — | 0.21ms | 1.17ms | 0.69ms | 0.68ms | 1.20ms |
488
- | [sort](bench/sort/sort.js) | 8.44ms<br>1.9kB | 9.98ms<br>1.6kB | fails | 10.18ms<br>1.9kB | — | 8.85ms | 10.36ms | 8.84ms | 9.37ms | 5.05ms |
489
- | [crc32](bench/crc32/crc32.js) | 11.96ms<br>1.5kB | 13.35ms<br>1.8kB | 80.76ms<br>3.1kB | 12.04ms<br>1.4kB | — | 10.69ms | 9.30ms | 9.45ms | 9.38ms | 0.24ms |
490
-
491
- _Numbers from `node bench/bench.mjs` on Apple Silicon._ `optimize: 'size'|'speed'|'balanced'` provides a size/speed tradeoff leverage.
490
+ </details>
492
491
 
493
492
  <details>
494
- <summary><strong>Optimizations</strong></summary>
493
+ <summary><strong>Custom sections</strong></summary>
495
494
 
496
495
  <br>
497
- High-impact summary behind the benchmark table, not an exhaustive list.
498
496
 
499
- | Optimization | Effect |
497
+ jz embeds four small WebAssembly custom sections so the JS interop layer can wire boundary ABIs without re-parsing the source. They're inert for non-JS hosts (wasmtime/wasmer ignore unknown customs); `interop.js` reads them once at instantiate-time. You don't need to touch them — they're documented so external tools (linkers, custom loaders, devtools) can read them safely.
498
+
499
+ | Section | Purpose |
500
500
  |---|---|
501
- | Escape scalar replacement | Removes short-lived object/array literals before allocation. |
502
- | Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
503
- | Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
504
- | 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. |
505
- | 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. |
506
- | Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
507
- | 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. |
508
- | SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
509
- | Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
510
- | OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
511
- | Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
501
+ | `jz:schema` | Object schemas for exported records JS rehydrates plain objects from boundary writes without per-call shape inference. |
502
+ | `jz:rest` | Per-export rest-parameter info (`{ name, fixed }`) tells JS how many fixed args precede the rest array so the wrapper packs the tail correctly (covers aliased re-exports). |
503
+ | `jz:i64exp` | Per-export i64-ABI map marks slots where pointers cross as i64 (dodging V8's NaN canonicalization) instead of f64. |
504
+ | `jz:extparam` | Per-export externref-param positions args that skip NaN-boxing (the jsstring carrier writes here), with `d` carrying `= ''` defaults. |
512
505
 
513
- `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.
506
+ Names are stable; binary layouts are not re-derive from the latest `interop.js` if you parse them yourself.
514
507
 
515
508
  </details>
516
509
 
517
510
 
511
+ ## Examples
512
+
513
+ Runnable browser demos in [`examples/`](examples/) — each compiles a `.js` kernel to WASM and shares a typed array with a canvas (the memory-sharing pattern from [Interop](#interop)):
514
+
515
+ * [game-of-life](examples/game-of-life/) — Conway's Life writing the cell grid straight into shared pixel memory.
516
+ * [interference](examples/interference/) — two-source wave interference field rendered per frame.
517
+ * [mandelbrot](examples/mandelbrot/) — escape-time fractal with a precomputed color table.
518
+
519
+ Each folder has a `build.mjs` and an `index.html` — build, then open the page.
520
+
521
+
518
522
  ## Alternatives
519
523
 
520
524
  * [porffor](https://github.com/CanadaHonk/porffor) — ahead-of-time JS→WASM compiler targeting full TC39 semantics. Implements the spec progressively (test262). Where jz restricts the language for performance, porffor aims for completeness.
521
525
  * [assemblyscript](https://github.com/AssemblyScript/assemblyscript) — TypeScript-subset compiling to WASM — small, performant output, but requires type annotations.
522
526
  * [jawsm](https://github.com/drogus/jawsm) — JS→WASM compiler in Rust. Compiles standard JS with a runtime that provides GC and closures in WASM.
523
527
 
528
+ <details>
529
+ <summary><strong>Which one to choose?</strong></summary>
530
+
531
+ <br>
532
+
533
+ | Pick | When |
534
+ |---|---|
535
+ | **jz** | You write plain JS, want tiny WASM and native-class numeric/DSP speed, and your code fits the subset. |
536
+ | **porffor** | You need full TC39 / spec completeness. |
537
+ | **AssemblyScript** | You're comfortable writing a typed TypeScript dialect for explicit low-level control. |
538
+ | **jawsm** | You need to run standard JS *unchanged*, with GC and closures provided by a bundled WASM runtime. |
539
+
540
+ The axis is completeness vs. cost: jz restricts the language to emit a runtime-free, native-speed binary; the others spend size/runtime to cover more of JS.
541
+
542
+ </details>
543
+
524
544
  ## Build with
525
545
 
526
546
  * [subscript](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST jz needs without a full ES parser. Jessie subset keeps the grammar small and deterministic.