jz 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +281 -142
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +461 -185
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +591 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +600 -205
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/README.md CHANGED
@@ -1,29 +1,23 @@
1
1
  <img src="jz.svg" alt="jz logo" width="120"/>
2
2
 
3
+ ![stability](https://img.shields.io/badge/stability-experimental-black) [![npm](https://img.shields.io/npm/v/jz?color=black)](http://npmjs.org/package/jz) [![test](https://github.com/dy/jz/actions/workflows/test.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/test.yml) [![bench](https://github.com/dy/jz/actions/workflows/bench.yml/badge.svg)](https://github.com/dy/jz/actions/workflows/bench.yml)
3
4
 
5
+ **jz** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
4
6
 
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
-
7
-
8
- **JZ** (_javascript zero_) is a **minimal modern functional JS subset** that compiles to WASM.<br>
9
7
 
10
8
  ```js
11
9
  import jz from 'jz'
12
10
 
13
- // Distance between two points
14
11
  const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
15
12
  dist(3, 4) // 5
16
13
  ```
17
14
 
18
- ## Why?
15
+ **[repl](https://dy.github.io/jz/repl/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
19
16
 
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
17
 
23
- * **Static AOT** – no runtime, no GC, no dynamic constructs.
24
- * **Valid jz = valid js** — test in browser, compile to wasm.
25
- * **Minimal** output is close to hand-written WAT; CI gates it to stay at least as small and fast as AssemblyScript and Porffor on the bench corpus ([CONTRIBUTING](CONTRIBUTING.md#performance--size-invariant)).
26
- <!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
18
+ ## Why?
19
+
20
+ jz distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) and **compiles JS ahead-of-time to WASM**: no runtime, no GC, no legacy, no spec creep, near-native perf with unlocked SIMD. **Valid jz is valid JS** run and test as JS, compile to portable WASM ([known divergences](#faq)).
27
21
 
28
22
  | Good for | Not for |
29
23
  |-----------------------------|----------------------------|
@@ -32,9 +26,6 @@ JZ distills modern JS to its functional core — the "good parts" ([Crockford](h
32
26
  | Parsing / transforms | Async / I/O-heavy logic |
33
27
  | WASM utilities | JavaScript runtime |
34
28
 
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
-
38
29
 
39
30
  ## Usage
40
31
 
@@ -43,16 +34,14 @@ JZ distills modern JS to its functional core — the "good parts" ([Crockford](h
43
34
  ```js
44
35
  import jz, { compile } from 'jz'
45
36
 
46
- // Compile, instantiate
37
+ // Compile + instantiate
47
38
  const { exports: { add } } = jz('export let add = (a, b) => a + b')
48
39
  add(2, 3) // 5
49
40
 
50
- // Compile only — returns raw WASM binary (no JS adaptation)
41
+ // Compile only — raw WASM binary
51
42
  const wasm = compile('export let f = (x) => x * 2')
52
- const mod = new WebAssembly.Module(wasm)
53
- const inst = new WebAssembly.Instance(mod)
54
43
 
55
- // Async WASM startup — jz source compilation is still synchronous
44
+ // Async startup
56
45
  const asyncMod = await WebAssembly.compile(wasm)
57
46
  const asyncInst = await WebAssembly.instantiate(asyncMod)
58
47
  asyncInst.exports.f(21) // 42
@@ -65,17 +54,16 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
65
54
 
66
55
  | Option | Use |
67
56
  |---|---|
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. |
69
- | `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
70
- | `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
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. |
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. |
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). |
74
- | `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
75
- | `alloc: false` | Omit raw allocator exports like `_alloc`/`_clear` when compiling standalone WASM that never marshals heap values across the host boundary. |
76
- | `wat: true` | `compile()` returns WAT text instead of a WASM binary. |
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. |
78
-
57
+ | `modules: { specifier: source }` | Static ES imports to bundle. CLI import resolution does this from files automatically. |
58
+ | `imports: { mod: host }` | Host imports `import { fn } from "mod"`. |
59
+ | `memory` | Pass `memory: N` for owned memory with `N` initial pages, or `memory: jz.memory()` / `WebAssembly.Memory` to share across modules. |
60
+ | `host: 'js' \| 'wasi'` | Runtime-service lowering. Default `js`; `wasi` for standalone runtimes. |
61
+ | `optimize` | `false`/`0` off, `1` size-only, `true`/`2` default (all stable passes), `3` trades size for speed. String aliases: `'size'`, `'balanced'` (= default), `'speed'`. Object form overrides individual passes. |
62
+ | `strict: true` | Enforce the pure canonical subset: skip jzify lowering (so `var`/`function`/`class`/`==`/… are rejected, not accepted) **and** reject dynamic fallbacks (`obj[k]`, `for-in`, unknown receiver methods). Off by default broader JS is lowered automatically. |
63
+ | `alloc: false` | Omit allocator exports (`_alloc`/`_clear`) for standalone modules that never marshal heap values. |
64
+ | `randomSeed` | `Math.random` seeding default draws from host entropy (non-reproducible); a number fixes it for a reproducible sequence, `true` forces entropy explicitly. |
65
+ | `wat: true` | `compile()` returns WAT text instead of WASM binary. |
66
+ | `profile` | Mutable sink for compile-stage timings; set `profile.names = true` for a WASM `name` section. |
79
67
  </details>
80
68
 
81
69
  ## CLI
@@ -83,81 +71,110 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
83
71
  `npm install -g jz`
84
72
 
85
73
  ```sh
86
- # Compile
87
74
  jz program.js # → program.wasm
88
75
  jz program.js --wat # → program.wat
89
76
  jz program.js -o out.wasm # custom output (- for stdout)
77
+ jz program.js -O3 # optimization: -O0 off, -O1 size, -O2 balanced, -O3 speed
78
+ jz program.js --host wasi # standalone WASI output
79
+ jz --strict program.js # pure canonical subset (also implied by .jz extension)
80
+ jz -e "1 + 2" # eval → 3
81
+ ```
90
82
 
91
- # Optimization level: -O0 off, -O1 size, -O2 balanced, -O3 speed
92
- jz program.js -O3
93
-
94
- # Runtime-service lowering: js (default) or wasi
95
- jz program.js --host wasi
96
-
97
- # Evaluate
98
- jz -e "1 + 2" # 3
83
+ <details>
84
+ <summary><code>jz --help</code></summary>
99
85
 
100
- # Show help
101
- jz --help
102
86
  ```
87
+ jz v0.5.1 - min JS → WASM compiler
88
+
89
+ Usage:
90
+ jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
91
+ jz --strict <file.js> Strict mode — pure canonical subset, no lowering
92
+ jz --jzify <file.js> Transform JS → jz source (auto-derives output file)
93
+ jz -e <expression> Evaluate expression
94
+ jz --help Show this help
95
+
96
+ Examples:
97
+ jz program.js # → program.wasm
98
+ jz program.js --wat # → program.wat
99
+ jz program.js -o out.wasm # custom output name
100
+ jz program.js -o - # write to stdout
101
+ jz program.js -O3 # aggressive optimization
102
+ jz program.js -Os # optimize for size
103
+ jz program.js --host wasi # emit WASI Preview 1 imports
104
+ jz --strict program.js # strict mode
105
+ jz --jzify lib.js # → lib.jz
106
+ jz -e "1 + 2"
107
+
108
+ Options:
109
+ --output, -o <file> Output file (.wat, .wasm, or - for stdout)
110
+ -O<n>, --optimize <n> Optimization level: 0 off, 1 size-only, 2 default,
111
+ 3 aggressive. Aliases: -Os/size, -Ob/balanced, -Of/speed.
112
+ --host <js|wasi> Runtime-service lowering (default js)
113
+ --no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
114
+ --names Emit wasm name section for profilers/debuggers
115
+ --strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
116
+ --jzify Transform JS to jz source (no compilation)
117
+ --eval, -e Evaluate expression or file
118
+ --wat Output WAT text instead of binary
119
+ --resolve Resolve bare specifiers via Node.js module resolution
120
+ --imports <file> JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
121
+ --version, -v Show version number
122
+ ```
123
+ </details>
103
124
 
104
125
 
105
126
  ## Language
106
127
 
107
- JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
128
+ jz is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
108
129
 
109
130
  ```
110
131
  ┌────────────────────────────────────────────────────────────────────────┐
111
- │ JZify │
112
- │ var function arguments switch new Foo() │
113
- | class new this extends super static #private │
114
- │ == != instanceof undefined |
115
- │ │
116
132
  │ ┌────────────────────────────────────────────────────────────────────┐ │
117
- │ │ JZ │ │
133
+ │ │ jz strict │ │
118
134
  │ │ let/const => ...xs destructuring import/export │ │
119
135
  │ │ if/else for/while/do-while/of/in break/continue │ │
120
136
  │ │ try/catch/finally throw │ │
121
137
  │ │ operators strings booleans numbers arrays objects `${}` │ │
122
138
  │ │ Math Number String Array Object JSON RegExp Symbol null │ │
123
139
  │ │ ArrayBuffer DataView TypedArray Map Set │ │
140
+ │ │ parseInt parseFloat encodeURIComponent Error BigInt │ │
124
141
  │ │ console setTimeout/setInterval Date performance │ │
125
142
  │ └────────────────────────────────────────────────────────────────────┘ │
143
+ │ jz default (jzify) │
144
+ │ var function arguments switch new Foo() │
145
+ │ class new this extends super static #private │
146
+ │ == != instanceof undefined WeakMap WeakSet │
147
+ │ │
126
148
  └────────────────────────────────────────────────────────────────────────┘
127
149
  Not supported
128
150
  async/await Promise function* yield
129
- delete labels eval Function with
130
- Proxy Reflect WeakMap WeakSet
151
+ delete getters/setters eval Function with
152
+ Proxy Reflect
131
153
  import() DOM fetch Intl Node APIs
132
154
  ```
133
155
 
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
156
 
136
157
  ## FAQ
137
158
 
138
159
  <details>
139
- <summary><strong>Where does jz differ from JavaScript?</strong></summary>
140
-
141
- <br>
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).)
144
-
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.
150
-
151
- For full TC39 conformance use [porffor](https://github.com/CanadaHonk/porffor); jz trades completeness for low-level numeric performance by design.
152
-
160
+ <summary><strong>What are the differences with JS?</strong></summary>
161
+
162
+ - **Numbers are f64**; integer-proven values (loop counters, array idx, `| 0`) are `i32` and **wrap at ±2³¹**.
163
+ - **Strings are UTF-8 bytes** — `.length`, `charCodeAt`, indexing, `slice`, `indexOf`, regex count bytes (`"中".length` is `3`); `toUpperCase`/`toLowerCase`/`trim` are ASCII-only. UTF-8 skips UTF-16's 2× and a multi-KB Unicode case table.
164
+ - **Objects are fixed-shape structs** literal keys sit in fixed slots; computed writes (`o[k] = v`) fall back to a per-object hash and enumerate normally, but a dot-key added after the literal (`o.b = 2`) stays readable without enumerating (`Object.keys`/`for…in`). Prefer `Map` for heavy dynamic keys.
165
+ - **Typed arrays are fixed-size** — `arr.length = n` won't compile, and out-of-bounds reads give `0`.
166
+ - **No GC** call `memory.reset()` between batches; `WeakMap`/`WeakSet` wired to `Map`/`Set`.
167
+ - **`String(number)` keeps ~9 significant digits** (`String(Math.PI)` `"3.14159265"`), so it may not round-trip; `NaN`/`Infinity`/integers are exact. Exact shortest-form needs a multi-KB Ryū/Grisu formatter.
168
+ - **Errors are just their message** — a caught error is the value you threw (no `.message`, not `instanceof Error`), and `null.x` yields `undefined` instead of throwing. It keeps `throw` and member reads free of object machinery and per-access checks.
169
+ - **`Date` getters return UTC** (`getHours` `getUTCHours`) – the IANA timezone database is hundreds of KB.
170
+ - **`Math.random` is seedable** — default draws host entropy; pass `randomSeed: n` for a reproducible stream.
153
171
  </details>
154
172
 
155
- <details>
156
- <summary><strong>Can I use npm packages or existing JS libraries?</strong></summary>
157
173
 
158
- <br>
174
+ <details>
175
+ <summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
159
176
 
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.
177
+ Only the ones that fit the jz subset. There's no runtime, so packages touching the DOM, `async`/`Promise`, the network, or Node APIs won't compile — but pure numeric/algorithmic source does.
161
178
 
162
179
  - **Relative imports** (`./dep.js`) bundle at compile time.
163
180
  - **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.
@@ -167,384 +184,341 @@ jz is for compiling *your* numeric/DSP/parser code, not for running the npm ecos
167
184
  </details>
168
185
 
169
186
  <details>
170
- <summary><strong>Can I use import/export to split code?</strong></summary>
171
-
172
- <br>
187
+ <summary><strong>Can I use import/export?</strong></summary>
173
188
 
174
- Yes. Standard `import`/`export` syntax is bundled at compile time into a single WASM — no runtime module resolution.
189
+ Standard `import`/`export` syntax is bundled at compile time into a single WASM — no runtime module resolution.
175
190
 
176
191
  ```js
177
192
  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' } }
193
+ 'import { add } from "./math.js"; export let f = (a, b) => add(a, b)',
194
+ { modules: { './math.js': 'export let add = (a, b) => a + b' } }
180
195
  )
181
196
  ```
182
197
 
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.
198
+ Transitive imports work (main → math → utils → …); circular imports error at compile time. The **CLI** resolves filesystem imports automatically. In the **browser**, fetch sources yourself and pass them via `{ modules }` — the compiler stays synchronous and pure, no I/O.
184
199
 
185
200
  </details>
186
201
 
187
202
  <details>
188
- <summary><strong>How does memory work? How do I reset it?</strong></summary>
203
+ <summary><strong>Can I call into the host (functions, objects)?</strong></summary>
189
204
 
190
- <br>
205
+ `import … from 'host'` with the `{ imports }` option **wires a runtime binding** — a JS function, constant, or whole namespace. Numbers pass directly; strings, arrays, and objects cross via `memory.*`.
191
206
 
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.
207
+ ```js
208
+ // Custom function
209
+ jz('import { log } from "host"; export let f = (x) => { log(x); return x }',
210
+ { imports: { host: { log: console.log } } })
193
211
 
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:
212
+ // Whole namespacesin, cos, PI, all auto-wired (functions as imports, numeric constants folded)
213
+ jz('import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
214
+ { imports: { math: Math } })
195
215
 
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
- }
216
+ // globalThis works too
217
+ jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
218
+ { imports: { window: globalThis } })
201
219
  ```
202
220
 
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).
204
-
205
221
  </details>
206
222
 
207
223
  <details>
208
- <summary><strong>How do I see and control inferred types?</strong></summary>
224
+ <summary><strong>Can I interpolate values (template literals)?</strong></summary>
209
225
 
210
- <br>
226
+ `jz` is a tagged template — interpolated values are baked into the source at compile time. Numbers and booleans inline directly; strings, arrays, and objects compile as jz literals:
211
227
 
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.
228
+ ```js
229
+ jz`export let f = () => ${'hello'}.length` // 5
230
+ jz`export let f = () => ${[10, 20, 30]}[1]` // 20
231
+ jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3
213
232
 
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.
233
+ const scale = (x) => x * 10
234
+ jz`export let f = (n) => ${scale}(n) + 1` // f(2) → 21, host-called
235
+ ```
236
+
237
+ Interpolated functions become host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
215
238
 
216
239
  </details>
217
240
 
218
241
  <details>
219
- <summary><strong>Is it production-ready?</strong></summary>
220
-
221
- <br>
242
+ <summary><strong>How to pass numbers, strings, arrays, objects JS ↔ WASM?</strong></summary>
222
243
 
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.
244
+ **Numbers cross natively** as `f64`/`i32`. **Heap values** strings, arrays, objects, typed arrays cross as NaN-boxed `f64` pointers into linear memory, allocated through the module's `_alloc`/`_clear` exports. That pointer-plus-allocator convention *is* the whole ABI (a few hundred bytes, documented in [`layout.js`](layout.js) with a worked example in [`test/abi.js`](test/abi.js)). The one shortcut: arrays of 8 elements come back as plain JS arrays via WASM multi-value.
224
245
 
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.
246
+ The `memory` codec returned by `jz()` and by `jz/interop`'s `instantiate()`handles both directions: it marshals arguments in, decodes pointer returns out, and turns a wasm `throw` into a real `Error`:
226
247
 
227
- </details>
248
+ ```js
249
+ const { exports, memory } = jz`
250
+ export let greet = (s) => s.length
251
+ export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
252
+ export let rgb = (c) => [c, c * 0.5, c * 0.2]
253
+ export let process = (buf) => buf.map(x => x * 2)
254
+ `
228
255
 
229
- <details>
230
- <summary><strong>Why jz over Porffor or AssemblyScript?</strong></summary>
256
+ // Pass in
257
+ exports.greet(memory.String('hello')) // 5
258
+ exports.dist(memory.Object({ x: 3, y: 4 })) // 5
231
259
 
232
- <br>
260
+ // Get back
261
+ exports.rgb(100) // [100, 50, 20] — auto-decoded JS array
262
+ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
263
+ ```
233
264
 
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).
265
+ `memory.String` / `.Array` / `.Float64Array`/etc / `.Object` allocate on the heap and return a pointer; `memory.read(ptr)` decodes one back. `memory.Object()` is fixed-layout its keys must match a compiled schema's key set (order is free, fields place by name).
235
266
 
236
267
  </details>
237
268
 
238
269
  <details>
239
- <summary><strong>Can I compile jz to C?</strong></summary>
270
+ <summary><strong>Do I need jz at runtime?</strong></summary>
240
271
 
241
- <br>
272
+ The compiler runs at build time. At runtime you ship the `.wasm` and, at most, a small bridge — never the compiler, the parser, or a language runtime.
242
273
 
243
- Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
274
+ - **Pure-number modules — nothing but the `.wasm`.** Instantiate with raw `WebAssembly`, zero jz dependency: `(await WebAssembly.instantiate(wasmBytes)).instance.exports.dist(3, 4)`. Compile with `{ alloc: false }` to drop the `_alloc`/`_clear` exports too.
275
+ - **Heap values (strings, arrays, objects) — the `.wasm` plus `jz/interop`.** `import { instantiate } from 'jz/interop'` adds a ~6 KB-gzipped bridge (no compiler, no parser) that builds the same `Module`+`Instance` you'd build by hand and wires the allocator and the `memory` codec from the previous question (plus WASI / `wasm:js-string` imports if the module uses them).
276
+ - **No JavaScript host at all** — compile with `host: 'wasi'`; see the next question.
244
277
 
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
250
- ```
251
-
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).
278
+ For contrast, Rust (`wasm-bindgen`), Go (TinyGo), and C/Zig (Emscripten/WASI-libc) emit per-build generated glue and usually bundle a language runtime. jz keeps the ABI fixed and the optional bridge ~6 KB gzipped.
253
279
 
254
280
  </details>
255
281
 
256
282
  <details>
257
- <summary><strong>Can I add my own operators or stdlib methods?</strong></summary>
258
-
259
- <br>
283
+ <summary><strong>Can I run the `.wasm` without a JavaScript host (WASI)?</strong></summary>
260
284
 
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:
285
+ There's two possible `host` targets:
262
286
 
263
- ```js
264
- import { emitter } from './src/emit.js'
265
- import { typed } from './src/ir.js'
287
+ - **`js`** (default) — runs inside a JavaScript host (browser, Node, Deno, Bun). `jz()` and `jz/interop` wire the needed `env.*` services automatically (overridable via `opts.imports.env`), and you get full value marshaling across the boundary.
288
+ - **`wasi`** runs on a standalone WASM engine with no JavaScript (wasmtime, wasmer, deno run). jz emits WASI Preview 1, so the module needs no host shims — but there's no host-side marshaler, so heap values must be passed by hand.
266
289
 
267
- // my.double(x) x * 2
268
- emitter['my.double'] = (x) => ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
269
- ```
290
+ Either way the `.wasm` carries at most one import namespace (none, `env`, or `wasi_snapshot_preview1`). The difference is only in how a few runtime services are serviced:
270
291
 
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.
292
+ | What your code does | `js` (default) | `wasi` |
293
+ |---|---|---|
294
+ | `console.log()` | `env.print` — host stringifies | WASI `fd_write` |
295
+ | `Date.now()` / `performance.now()` | `env.now` → f64 | WASI `clock_time_get` |
296
+ | `setTimeout` / `setInterval` | `env.setTimeout` — host schedules | WASM timer queue + `__timer_tick` |
297
+ | dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
272
298
 
273
299
  </details>
274
300
 
301
+ <details>
302
+ <summary><strong>How does memory work?</strong></summary>
275
303
 
276
- ## Benchmark
277
-
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 | — | — | — | — | — | — | — |
292
-
304
+ 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 — the first 1 KB holds static data (string/array literals laid out from offset 0, plus the bump pointer itself at byte 1020 when memory is shared across threads). It grows the WASM memory automatically when full, and if the literals overflow that 1 KB the heap simply starts past them.
305
+ Memory is never reclaimed implicitly — a long-running program that allocates per call grows without bound. Reset between independent batches:
293
306
 
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`._
307
+ ```js
308
+ for (let i = 0; i < 1000; i++) {
309
+ const sum = exports.process(100) // allocates an array each call
310
+ memory.reset() // drop everything; heap ptr → 1024
311
+ }
312
+ ```
295
313
 
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.
314
+ 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.
297
315
 
316
+ </details>
298
317
 
299
318
  <details>
300
- <summary><strong>Optimizations</strong></summary>
301
-
302
- <br>
303
- High-impact summary behind the benchmark table, not an exhaustive list.
319
+ <summary><strong>Can modules share memory?</strong></summary>
304
320
 
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. |
318
-
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.
321
+ `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate, so objects created in one module are readable by another:
320
322
 
321
- #### Making array loops vectorize
323
+ ```js
324
+ const memory = jz.memory()
325
+ const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
326
+ const b = jz('export let read = (o) => o.x + o.y', { memory })
322
327
 
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).
328
+ b.exports.read(a.exports.make()) // 30 same memory, merged schemas
329
+ memory.read(a.exports.make()) // {x: 10, y: 20}
330
+ ```
324
331
 
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]`, `|`, `&`).
332
+ Pass an existing `WebAssembly.Memory` to wrap it: `jz.memory(new WebAssembly.Memory({ initial: 4 }))`.
326
333
 
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.
334
+ Each compiled module exposes two call surfaces:
328
335
 
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.
336
+ - **`.exports`** the JS-wrapped surface: it marshals JS arguments into the heap and decodes pointer return values back to JS values (and turns a wasm `throw` into an `Error`). Use it by default — it's also how you hand a value from one module to another, as in the example above (the value is re-marshaled through the shared memory).
337
+ - **`.instance.exports`** — the raw `WebAssembly.Instance` exports: numbers pass through untouched, and a pointer return comes back as a raw NaN-boxed handle. Decode it on the host with `memory.read(ptr)`. Don't pass a raw pointer back *in* as an argument, though — the JS↔wasm `f64` boundary canonicalizes its NaN payload and the pointer is lost; let `.exports` marshal across instead.
330
338
 
331
339
  </details>
332
340
 
341
+ <details>
342
+ <summary><strong>How big is the output?</strong></summary>
333
343
 
334
- ## Interop
335
-
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.
344
+ No runtime, no GC — a module is your code plus a small bump allocator. The geomean across the bench corpus is on par with AssemblyScript and smaller than Porffor; most modules are single-digit kB — the [ZzFX synth](examples/zzfx) is ~10 kB, [mandelbrot](examples/mandelbrot) ~7 kB. Shrink it further:
337
345
 
338
- ### Passing data in and out
346
+ - **`optimize: 'size'`** keeps every size pass, drops loop unrolling and SIMD.
347
+ - **`alloc: false`** — omit the allocator for pure-numeric modules that never marshal heap values.
348
+ - **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `profile.names`).
339
349
 
340
- Arrays of 8 elements come back as plain JS arrays (WASM multi-value); everything else stays heap-resident behind a pointer.
350
+ Hand-written WAT is still ~3–8× smaller on tight kernels jz carries generic allocator and stdlib helpers a specialist omits; closing that gap is ongoing. Size budgets are gated in CI alongside speed ([full table](bench/README.md)).
341
351
 
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
- `
352
+ </details>
349
353
 
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
354
 
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]
361
- ```
355
+ <details>
356
+ <summary><strong>Which optimizations are applied?</strong></summary>
362
357
 
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.
358
+ Ordinary JS is already fast — jz infers the right machine type for your numbers, so you write plain JS. What it does, all on at the default `optimize: 2` (each line is also the habit that triggers it):
365
359
 
366
- ### Template interpolation
360
+ - **Type narrowing** — parameters/results pinned to `i32`/`f64`/bool/typed-array elements from their call sites, off the boxed path. A `Float64Array`/`Int32Array` is direct memory access; a plain `[]` works too, with a little more overhead.
361
+ - **Escape analysis & arena rewind** — fixed-shape arrays/objects/typed-arrays become WASM locals; scratch a function doesn't return is freed on exit (no manual cleanup).
362
+ - **Loops** — invariant hoisting, CSE, typed-array address reuse, induction-variable strength reduction, small fixed-count unrolling (mat4, biquad).
363
+ - **SIMD-128** — independent iterations (`a[i] = a[i]*2 + b[i]`) run several lanes at once: lane-pure maps, reductions (sum/product/min·max), conditional maps (`bitselect`), byte scans (`memchr` via `i8x16`). Loops that look back (`a[i-1]`) or carry a running total stay sequential.
364
+ - **Smaller encoding** — tree-shaking, copy-propagation + dead-store elimination, local/string-pool reordering for 1-byte indices, pointer-call specialization, constant pooling; JS strings you only read aren't copied.
367
365
 
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:
366
+ Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny `env.*` imports, a constant `JSON.parse` folds to a literal, JS strings stay zero-copy. Levels `0`–`3` or `'size'`/`'balanced'`/`'speed'` (or a per-pass object): `'balanced'` (= `2`) is the default; `'speed'` trades size for inlined constants and larger buffers; `'size'` drops unrolling and SIMD.
369
367
 
370
- ```js
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
374
- ```
368
+ </details>
375
369
 
376
- Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
370
+ <details>
371
+ <summary><strong>How do I inspect or debug the output?</strong></summary>
377
372
 
378
- ### Calling host functions
373
+ - **Semantics** valid jz is valid JS: run the same source under Node and diff results (mind the [documented divergences](#faq)); `console.log` works inside compiled modules too.
374
+ - **Codegen** — `jz program.js --wat` (API: `compile(src, { wat: true })`) shows the emitted WAT: grep `v128` to confirm a loop vectorized, `__dyn_get`/`__ext_call` to spot dynamic fallbacks inference couldn't narrow.
375
+ - **Dynamic fallbacks** — compile with `strict: true` to turn every fallback (`obj[k]`, `for-in`, unknown receiver method) into a compile error pointing at the site.
376
+ - **Profiling** — `--names` (API: `profile.names = true`) emits a wasm `name` section so DevTools profilers and disassemblers show real function names; the `profile` option collects per-stage compile timings.
377
+ - **Slow kernel checklist** — a stray float literal pins a counter to f64; a plain `[]` where a typed array would do; a loop-carried dependency (`a[i-1]`, running sum) blocks SIMD. The signals in *Why no type annotations?* below are the levers.
379
378
 
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:
379
+ </details>
381
380
 
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 } } })
381
+ <details>
382
+ <summary><strong>How does jz work?</strong></summary>
386
383
 
387
- // Whole namespacesin, cos, PI, all auto-wired
388
- jz('import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
389
- { imports: { math: Math } })
384
+ A source string flows through six stages into wasm bytes no IR leaves the process, the whole thing is one pass per `compile()`:
390
385
 
391
- // globalThis works too
392
- jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
393
- { imports: { window: globalThis } })
386
+ ```
387
+ your .js
388
+ parse jessie parser (subscript) AST
389
+ │ jzify lower legacy JS to the canonical subset (var/function/class/==/…)
390
+ │ prepare resolve & bundle imports, normalize the AST
391
+ │ compile type inference (i32 vs f64) + emit WAT IR; module/ handlers lower operators
392
+ │ optimize WAT-level passes — CSE, DCE, const-fold, inline, peephole
393
+ │ encode watr: WAT → WASM binary
394
+
395
+ .wasm
394
396
  ```
395
397
 
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.
398
+ Each stage lives in its own place: parsing in [`subscript`](https://github.com/dy/subscript)'s jessie grammar, [`jzify/`](jzify/) for the legacy-JS lowering, [`src/prepare/`](src/prepare/) for module bundling, [`src/compile/`](src/compile/) for inference + codegen (with built-ins in [`module/`](module/) and heap layout in [`src/abi/`](src/abi/)), [`src/optimize/`](src/optimize/) + [`src/wat/`](src/wat/) for the WAT passes, and [`watr`](https://github.com/dy/watr) for the final encode. Shared compile state is one `ctx` object ([`src/ctx.js`](src/ctx.js)).
399
399
 
400
- | JS API | `host: 'js'` (default) | `host: 'wasi'` |
401
- |---|---|---|
402
- | `console.log()` | `env.print(val, fd, sep)` — host stringifies | WASI `fd_write` (fd=1), space-separated, newline |
403
- | `console.warn`/`error` | same, fd=2 | WASI `fd_write` (fd=2) |
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` |
407
- | dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
400
+ </details>
408
401
 
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.
410
402
 
411
- ### Sharing memory across modules
403
+ <details>
404
+ <summary><strong>Why no type annotations?</strong></summary>
412
405
 
413
- `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate, so objects created in one module are readable by another:
406
+ Because `let x: i32` isn't valid JS annotations would break the promise that valid jz runs and tests as plain JS. So jz reads the types from signals you already write:
414
407
 
415
408
  ```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 })
419
-
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
409
+ export let bits = (a, b) => a | b // i32 — a bitwise op pins both operands
410
+ export let half = (n) => n * 0.5 // f64 0.5 isn't an integer
422
411
  ```
423
412
 
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.
413
+ Literals (`0` vs `0.5`), operators (`|` `<<` `&` ⇒ i32), and how a value is used pin it to `i32`, `f64`, string, object, or typed array. Anything still ambiguous stays **dynamic** always correct, just type-checked at runtime (a little slower).
425
414
 
426
- ### Shipping & running the `.wasm`
415
+ </details>
427
416
 
428
- Compile once, then run the binary anywhere.
429
417
 
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:
418
+ <details>
419
+ <summary><strong>Is jz production-ready?</strong></summary>
431
420
 
432
- ```js
433
- import { instantiate } from 'jz/interop'
434
- import wasmBytes from './program.wasm' // bundler-specific; or fetch(...)
421
+ It's **experimental** (pre-1.0) — the supported subset and the wasm ABI may still change, so pin a version and re-test on upgrade. What's solid: every push runs the full test suite, the test262 conformance subset, the benchmark gate, and the self-host build in CI, so regressions surface immediately.
435
422
 
436
- const { exports, memory } = instantiate(wasmBytes)
437
- exports.greet(memory.String('hello')) // marshal works exactly as at compile time
438
- ```
423
+ </details>
439
424
 
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.
441
425
 
442
- **Native runtimes.** Compile with `host: 'wasi'` and run on any WASM runtime:
426
+ <details>
427
+ <summary><strong>Can I compile in the browser or a Worker?</strong></summary>
443
428
 
444
- ```sh
445
- jz program.js --host wasi -o program.wasm
446
- wasmtime program.wasm # also `wasmer run` / `deno run`
447
- ```
429
+ Yes. The compiler is pure and synchronous (no I/O — you hand it the sources), so it runs anywhere JavaScript does — main thread, a Web Worker, or a build step — and compiling a kernel takes single-digit-to-tens of milliseconds, fast enough to do on the fly. The `.wasm` it produces is just a module: instantiate it in any WebAssembly host — browser main thread, Web/Service Worker, Node/Deno/Bun, or a standalone engine.
448
430
 
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.
431
+ </details>
450
432
 
451
- **Memory ABI (non-JS hosts).** The allocator is exposed as two exports:
452
433
 
453
- ```
454
- (func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
455
- (func $_clear) ;; rewinds heap pointer to 1024
456
- ```
434
+ <details>
435
+ <summary><strong>Can jz compile itself?</strong></summary>
457
436
 
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.
437
+ Yes fully. jz compiles its own **entire** source to `dist/jz.wasm`: the whole pipeline (parse jzify prepare compile encode) runs inside WASM, taking a source string and returning wasm bytes with no host help. In other words, `dist/jz.wasm` is jz compiled by jz.
459
438
 
460
- <details>
461
- <summary><strong>Zero-copy strings</strong></summary>
439
+ `npm run test:self` is the CI gate — it builds `dist/jz.wasm`, then round-trips real programs through the in-wasm compiler and runs their output, proving the wasm-hosted compiler produces working modules.
462
440
 
463
- <br>
441
+ </details>
464
442
 
465
- Strings have two boundary carriers; the compiler picks per export-param:
466
443
 
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 |
444
+ <details>
445
+ <summary><strong>Can I compile jz to C?</strong></summary>
471
446
 
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
447
+ Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
448
+
449
+ ```sh
450
+ jz program.js -o program.wasm
451
+ wasm-opt -O3 program.wasm -o program.opt.wasm
452
+ wasm2c program.opt.wasm -o program.c
453
+ cc -O3 program.c -o program
484
454
  ```
485
455
 
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.
456
+ The full native pipeline (jz `wasm-opt -O3` `wasm2c` `clang -O3 -flto` + PGO) lowers to standalone native code that beats V8 on the watr example corpus (19/21 wins, 2 ties, M4 Max). Details and the regression gate live in [`scripts/native/README.md`](scripts/native/README.md).
487
457
 
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`.
458
+ [Static Hermes](https://github.com/facebook/hermes) reaches native the same way from the other end full JS through C/LLVM, with sound type annotations for speed; jz keeps the source plain JS and gets its types by inference.
489
459
 
490
460
  </details>
491
461
 
492
- <details>
493
- <summary><strong>Custom sections</strong></summary>
494
462
 
495
- <br>
496
463
 
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
464
 
499
- | Section | Purpose |
500
- |---|---|
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. |
505
465
 
506
- Names are stable; binary layouts are not — re-derive from the latest `interop.js` if you parse them yourself.
507
466
 
508
- </details>
467
+ ## Performance
509
468
 
469
+ Geomean speed across the [bench corpus →](bench/README.md).
510
470
 
511
- ## Examples
471
+ <img src="bench/bench.svg?v=2" alt="jz vs alternatives — geomean speed across the bench corpus" width="720">
512
472
 
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)):
473
+ <sub>Local snapshot (M4 Max, darwin/arm64). Bun/Zig/Rust/Go/NumPy rows are hand-run reference points.</sub>
514
474
 
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
475
 
476
+ ## Examples
521
477
 
522
- ## Alternatives
478
+ <table>
479
+ <tr>
480
+ <td width="33%"><a href="https://dy.github.io/jz/examples/game-of-life/"><img src="examples/thumbs/game-of-life.webp" width="100%" alt="Game of Life"></a><br><b>game-of-life</b> — Conway's Life straight into shared pixel memory.</td>
481
+ <td width="33%"><a href="https://dy.github.io/jz/examples/lenia/"><img src="examples/thumbs/lenia.webp" width="100%" alt="Lenia"></a><br><b>lenia</b> — continuous cellular automaton; smooth-kernel "digital life".</td>
482
+ <td width="33%"><a href="https://dy.github.io/jz/examples/diffusion/"><img src="examples/thumbs/diffusion.webp" width="100%" alt="Diffusion"></a><br><b>diffusion</b> — Gray-Scott; organic coral / labyrinths.</td>
483
+ </tr>
484
+ <tr>
485
+ <td><a href="https://dy.github.io/jz/examples/interference/"><img src="examples/thumbs/interference.webp" width="100%" alt="Wave interference"></a><br><b>interference</b> — two-source wave field, recomputed every frame.</td>
486
+ <td><a href="https://dy.github.io/jz/examples/plasma/"><img src="examples/thumbs/plasma.webp" width="100%" alt="Plasma"></a><br><b>plasma</b> — FBM domain-warp; the classic flowing shader plasma.</td>
487
+ <td><a href="https://dy.github.io/jz/examples/chladni/"><img src="examples/thumbs/chladni.webp" width="100%" alt="Chladni plate"></a><br><b>chladni</b> — Camerata-style plate; frequency sweeps the nodal figure.</td>
488
+ </tr>
489
+ <tr>
490
+ <td><a href="https://dy.github.io/jz/examples/mandelbrot/"><img src="examples/thumbs/mandelbrot.webp" width="100%" alt="Mandelbrot set"></a><br><b>mandelbrot</b> — escape-time fractal with smooth coloring.</td>
491
+ <td><a href="https://dy.github.io/jz/examples/attractors/"><img src="examples/thumbs/attractors.webp" width="100%" alt="Strange attractor"></a><br><b>attractors</b> — de Jong map, millions of iters → luminous curves.</td>
492
+ <td><a href="https://dy.github.io/jz/examples/raymarcher/"><img src="examples/thumbs/raymarcher.webp" width="100%" alt="SDF raymarcher"></a><br><b>raymarcher</b> — an SDF sphere field; Shadertoy on the CPU.</td>
493
+ </tr>
494
+ <tr>
495
+ <td><a href="https://dy.github.io/jz/examples/rfft/"><img src="examples/thumbs/rfft.webp" width="100%" alt="Live spectrogram"></a><br><b>rfft</b> — live log/mel spectrogram from a jz real FFT.</td>
496
+ <td><a href="https://dy.github.io/jz/examples/zzfx/"><img src="examples/thumbs/zzfx.webp" width="100%" alt="ZzFX sound synth"></a><br><b>zzfx</b> — the unmodified <a href="https://github.com/KilledByAPixel/ZzFX">ZzFX</a> sfx synth, compiled as-is.</td>
497
+ <td><a href="https://dy.github.io/jz/examples/jukebox/"><img src="examples/thumbs/jukebox.webp" width="100%" alt="Floatbeat jukebox"></a><br><b>jukebox</b> — looping procedural-jazz arpeggio floatbeat; tap to play/pause.</td>
498
+ </tr>
499
+ </table>
500
+
501
+ [**Browse the gallery →**](https://dy.github.io/jz/examples/)
523
502
 
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.
525
- * [assemblyscript](https://github.com/AssemblyScript/assemblyscript) — TypeScript-subset compiling to WASM — small, performant output, but requires type annotations.
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.
527
503
 
528
- <details>
529
- <summary><strong>Which one to choose?</strong></summary>
530
504
 
531
- <br>
505
+ ## Alternatives
532
506
 
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. |
507
+ From small, fast JS subset to full JS spec, bundled engine:
539
508
 
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.
509
+ * [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) TS-like dialect WASM; small, fast output, but needs type annotations (not JS).
510
+ * [awasm-compiler](https://github.com/paulmillr/awasm-compiler) — reproducible WASM assembled through a typed *builder API*.
511
+ * [Porffor](https://github.com/CanadaHonk/porffor) — AOT JS→WASM (and C) targeting the full spec, grown against test262.
512
+ * [Static Hermes](https://github.com/facebook/hermes) — Meta's AOT JS → native via C/LLVM (no WASM target); full speed needs sound type annotations, untyped JS stays dynamic.
513
+ * [jawsm](https://github.com/drogus/jawsm) — JS→WASM in Rust on WasmGC; no interpreter, but leans on the engine's GC.
514
+ * [Javy](https://github.com/bytecodealliance/javy) — embeds QuickJS; runs almost any JS, but ships a full interpreter (large, interpreter-speed).
515
+ * [ComponentizeJS / jco](https://github.com/bytecodealliance/ComponentizeJS) — WASM Component via embedded SpiderMonkey; standards-complete, but bundles a JS engine.
541
516
 
542
- </details>
543
517
 
544
- ## Build with
518
+ ## Built with
545
519
 
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.
547
- * [watr](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Handles binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into a valid `.wasm` binary.
520
+ * [**subscript**](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST jz needs. Jessie subset keeps the grammar small and deterministic.
521
+ * [**watr**](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into valid `.wasm`.
548
522
 
549
523
 
550
524
  <p align=center>MIT • <a href="https://github.com/krishnized/license/">ॐ</a></p>