jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/README.md CHANGED
@@ -1,39 +1,33 @@
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
 
4
-
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>
5
+ **jz** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
9
6
 
10
7
  ```js
11
8
  import jz from 'jz'
12
9
 
13
- // Distance between two points
14
10
  const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
15
11
  dist(3, 4) // 5
16
12
  ```
17
13
 
18
- ## Why?
14
+ **[repl](https://dy.github.io/jz/repl/)** · **[floatbeat](https://dy.github.io/jz/floatbeat/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
15
+
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.
17
+ ## Why?
22
18
 
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. -->
19
+ 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 WASM.
27
20
 
28
- | Good for | Not for |
29
- |-----------------------------|----------------------------|
30
- | Numeric / math compute | UI / frontend |
31
- | DSP / audio / bytebeats | Backend / APIs |
32
- | Parsing / transforms | Async / I/O-heavy logic |
33
- | WASM utilities | JavaScript runtime |
21
+ | Good for | Not for |
22
+ |------------------------------|---------------------------|
23
+ | DSP, audio, synthesis | UI, DOM, the frontend |
24
+ | Image, video, pixels | Servers, APIs, I/O |
25
+ | Simulation, physics, games | Async, promises, events |
26
+ | Parsers, codecs, compression | Dynamic, polymorphic, OOP |
27
+ | Scientific, numeric, ML | Security crypto, big-ints |
28
+ | Hashing, checksums, RNG | Glue, plumbing, orchestration |
34
29
 
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 -->
30
+ Output `.wasm` is portable — run it in any host (browser, Node, Deno, edge, plugins), or take it native via [wasm2c](https://github.com/WebAssembly/wabt) (wasm → C → binary).
37
31
 
38
32
 
39
33
  ## Usage
@@ -41,18 +35,20 @@ JZ distills modern JS to its functional core — the "good parts" ([Crockford](h
41
35
  `npm install jz`
42
36
 
43
37
  ```js
44
- import jz, { compile } from 'jz'
38
+ import jz, { compile, compileModule, instantiate } from 'jz'
45
39
 
46
- // Compile, instantiate
40
+ // Compile + instantiate
47
41
  const { exports: { add } } = jz('export let add = (a, b) => a + b')
48
42
  add(2, 3) // 5
49
43
 
50
- // Compile only — returns raw WASM binary (no JS adaptation)
44
+ // Compile only — raw WASM binary
51
45
  const wasm = compile('export let f = (x) => x * 2')
52
- const mod = new WebAssembly.Module(wasm)
53
- const inst = new WebAssembly.Instance(mod)
54
46
 
55
- // Async WASM startup jz source compilation is still synchronous
47
+ // Compile once instantiate many (pays the AOT + validate cost once)
48
+ const mod = compileModule('export let f = (x) => x * 2')
49
+ instantiate(mod).exports.f(21) // 42 — repeat cheaply, no recompile
50
+
51
+ // Async startup
56
52
  const asyncMod = await WebAssembly.compile(wasm)
57
53
  const asyncInst = await WebAssembly.instantiate(asyncMod)
58
54
  asyncInst.exports.f(21) // 42
@@ -65,17 +61,22 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
65
61
 
66
62
  | Option | Use |
67
63
  |---|---|
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
-
64
+ | `modules: { specifier: source }` | Static ES imports to bundle. CLI import resolution does this from files automatically. |
65
+ | `imports: { mod: host }` | Host imports `import { fn } from "mod"`. |
66
+ | `memory` | Pass `memory: N` for owned memory with `N` initial pages, or `memory: jz.memory()` / `WebAssembly.Memory` to share across modules. `maxMemory: N` caps growth; `importMemory: true` imports `env.memory` instead of exporting own. |
67
+ | `host: 'js' \| 'wasi'` | Runtime-service lowering. Default `js`; `wasi` for standalone runtimes. |
68
+ | `optimize` | `false`/`0` off, `1` minimal, `true`/`2` default (all stable passes), `3`/`'speed'` trades size for speed, `'size'` for smallest wasm. (Object form for per-pass overrides is internal/unstable.) |
69
+ | `define` | Compile-time constants injected as top-level bindings, e.g. `{ DEBUG: false, PORT: 8080 }` (numbers, booleans, strings, null, or literal arrays/objects). |
70
+ | `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. |
71
+ | `alloc: false` | Omit allocator exports (`_alloc`/`_clear`) for standalone modules that never marshal heap values. |
72
+ | `noSimd: true` | Disable auto-vectorization (no jz-emitted `v128`) for engines without the SIMD proposal. Explicit `f32x4`/`i32x4` intrinsics still compile. |
73
+ | `whyNotSimd: true` | Diagnostic (CLI `--why-not-simd`): emit a `simd-why-not` warning per loop the auto-vectorizer declined, naming the first blocking op finds loops one op away from SIMD. Noisy; off by default. Surfaced via the `warnings` sink. |
74
+ | `experimentalStencil: true` | Opt-in (CLI `--experimental-stencil`): vectorize neighbour-load stencils — `b[i] = f(a[i-1], a[i], a[i+1])` and 2-D 5-point sweeps — to f64x2. Bit-exact vs scalar (a lane-parallel map reorders nothing within a lane). Unstable / off by default until proven across the corpus. |
75
+ | `experimentalOuterStrip: true` | Opt-in (CLI `--experimental-outer-strip`): strip-mine a pixel loop whose per-pixel value is an inner reduction (e.g. metaballs' field sum over blobs) into f64x2 — two adjacent pixels per step. Bit-exact vs scalar (each lane accumulates in scalar order). Unstable / off by default. |
76
+ | `randomSeed` | `Math.random` seeding — default draws from host entropy (non-reproducible); a number fixes it for a reproducible sequence, `true` forces entropy explicitly. |
77
+ | `wat: true` | `compile()` returns WAT text instead of WASM binary. |
78
+ | `names: true` | Emit a WASM `name` section (function symbols) for profilers/debuggers. |
79
+ | `profile` | Mutable sink for compile-stage timings (`entries`/`totals` per phase). |
79
80
  </details>
80
81
 
81
82
  ## CLI
@@ -83,81 +84,123 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
83
84
  `npm install -g jz`
84
85
 
85
86
  ```sh
86
- # Compile
87
87
  jz program.js # → program.wasm
88
88
  jz program.js --wat # → program.wat
89
89
  jz program.js -o out.wasm # custom output (- for stdout)
90
+ jz program.js -O3 # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
91
+ jz program.js --host wasi # standalone WASI output
92
+ jz --strict program.js # pure canonical subset (also implied by .jz extension)
93
+ jz -e "1 + 2" # eval → 3
94
+ ```
90
95
 
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
96
+ <details>
97
+ <summary><code>jz --help</code></summary>
99
98
 
100
- # Show help
101
- jz --help
102
99
  ```
100
+ jz - min JS → WASM compiler
101
+
102
+ Usage:
103
+ jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
104
+ jz --strict <file.js> Strict mode — pure canonical subset, no lowering
105
+ jz --jzify <file.js> Transform JS → jz source (auto-derives output file)
106
+ jz -e <expression> Evaluate expression
107
+ jz --help Show this help
108
+
109
+ Examples:
110
+ jz program.js # → program.wasm
111
+ jz program.js --wat # → program.wat
112
+ jz program.js -o out.wasm # custom output name
113
+ jz program.js -o - # write to stdout
114
+ jz program.js -O3 # optimize for speed
115
+ jz program.js -Os # optimize for size
116
+ jz program.js -D DEBUG=false # inject a compile-time constant
117
+ jz program.js --memory 64 # 64 initial pages (4 MB)
118
+ jz program.js --host wasi # emit WASI Preview 1 imports
119
+ jz --strict program.js # strict mode
120
+ jz --jzify lib.js # → lib.jz
121
+ jz -e "1 + 2"
122
+
123
+ Options:
124
+ --output, -o <file> Output file (.wat, .wasm, or - for stdout)
125
+ -O<n>, --optimize <n> Optimization level: 0 off, 1 minimal, 2 default (all
126
+ stable passes), 3 speed. -Os optimizes for size.
127
+ --define, -D <K=V> Inject a compile-time constant (VALUE parsed as JSON,
128
+ else string). Repeatable.
129
+ --host <js|wasi> Runtime-service lowering (default js)
130
+ --memory <pages> Initial memory size in 64 KiB pages
131
+ --max-memory <pages> Cap memory growth at this many pages (default unbounded)
132
+ --import-memory Import env.memory instead of exporting own memory
133
+ --no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
134
+ --no-simd Disable auto-vectorization (no v128) for non-SIMD engines
135
+ --why-not-simd Report, per loop, why the auto-vectorizer declined it
136
+ --experimental-stencil Vectorize neighbour-load stencils (a[i±1]); opt-in
137
+ --experimental-outer-strip Strip-mine pixel loops over an inner reduction to f64x2; opt-in
138
+ --no-tail-call Use ordinary call frames instead of return_call
139
+ --names Emit wasm name section for profilers/debuggers
140
+ --stats Print compile-phase timings to stderr
141
+ --strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
142
+ --jzify Transform JS to jz source (no compilation)
143
+ --eval, -e Evaluate expression or file
144
+ --wat Output WAT text instead of binary
145
+ --resolve Resolve bare specifiers via Node.js module resolution
146
+ --imports <file> JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
147
+ --version, -v Show version number
148
+ ```
149
+ </details>
103
150
 
104
151
 
105
152
  ## Language
106
153
 
107
- JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
154
+ jz is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
108
155
 
109
156
  ```
110
157
  ┌────────────────────────────────────────────────────────────────────────┐
111
- │ JZify │
112
- │ var function arguments switch new Foo() │
113
- | class new this extends super static #private │
114
- │ == != instanceof undefined |
115
- │ │
116
158
  │ ┌────────────────────────────────────────────────────────────────────┐ │
117
- │ │ JZ │ │
159
+ │ │ jz strict │ │
118
160
  │ │ let/const => ...xs destructuring import/export │ │
119
161
  │ │ if/else for/while/do-while/of/in break/continue │ │
120
162
  │ │ try/catch/finally throw │ │
121
163
  │ │ operators strings booleans numbers arrays objects `${}` │ │
122
164
  │ │ Math Number String Array Object JSON RegExp Symbol null │ │
123
165
  │ │ ArrayBuffer DataView TypedArray Map Set │ │
166
+ │ │ parseInt parseFloat encodeURIComponent Error BigInt │ │
124
167
  │ │ console setTimeout/setInterval Date performance │ │
125
168
  │ └────────────────────────────────────────────────────────────────────┘ │
169
+ │ jz default (jzify) │
170
+ │ var function arguments switch new Foo() │
171
+ │ class new this extends super static #private │
172
+ │ == != instanceof undefined WeakMap WeakSet │
173
+ │ │
126
174
  └────────────────────────────────────────────────────────────────────────┘
127
175
  Not supported
128
176
  async/await Promise function* yield
129
- delete labels eval Function with
130
- Proxy Reflect WeakMap WeakSet
177
+ delete getters/setters eval Function with
178
+ Proxy Reflect
131
179
  import() DOM fetch Intl Node APIs
132
180
  ```
133
181
 
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
182
 
136
183
  ## FAQ
137
184
 
138
185
  <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
-
186
+ <summary><strong>What are the differences with JS?</strong></summary>
187
+
188
+ - **Numbers are f64**; integer-proven values (loop counters, array idx, `| 0`) are `i32` and **wrap at ±2³¹**.
189
+ - **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.
190
+ - **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.
191
+ - **Typed arrays are fixed-size** — `arr.length = n` won't compile, and out-of-bounds reads give `0`.
192
+ - **No GC** call `memory.reset()` between batches; `WeakMap`/`WeakSet` wired to `Map`/`Set`.
193
+ - **`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.
194
+ - **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.
195
+ - **`Date` getters return UTC** (`getHours` `getUTCHours`) – the IANA timezone database is hundreds of KB.
196
+ - **`Math.random` is seedable** — default draws host entropy; pass `randomSeed: n` for a reproducible stream.
153
197
  </details>
154
198
 
155
- <details>
156
- <summary><strong>Can I use npm packages or existing JS libraries?</strong></summary>
157
199
 
158
- <br>
200
+ <details>
201
+ <summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
159
202
 
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.
203
+ 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
204
 
162
205
  - **Relative imports** (`./dep.js`) bundle at compile time.
163
206
  - **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 +210,338 @@ jz is for compiling *your* numeric/DSP/parser code, not for running the npm ecos
167
210
  </details>
168
211
 
169
212
  <details>
170
- <summary><strong>Can I use import/export to split code?</strong></summary>
213
+ <summary><strong>Can I use import/export?</strong></summary>
171
214
 
172
- <br>
173
-
174
- Yes. Standard `import`/`export` syntax is bundled at compile time into a single WASM — no runtime module resolution.
215
+ Standard `import`/`export` syntax is bundled at compile time into a single WASM — no runtime module resolution.
175
216
 
176
217
  ```js
177
218
  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' } }
219
+ 'import { add } from "./math.js"; export let f = (a, b) => add(a, b)',
220
+ { modules: { './math.js': 'export let add = (a, b) => a + b' } }
180
221
  )
181
222
  ```
182
223
 
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.
224
+ 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
225
 
185
226
  </details>
186
227
 
187
228
  <details>
188
- <summary><strong>How does memory work? How do I reset it?</strong></summary>
229
+ <summary><strong>Can I call into the host (functions, objects)?</strong></summary>
189
230
 
190
- <br>
231
+ `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
232
 
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.
233
+ ```js
234
+ // Custom function
235
+ jz('import { log } from "host"; export let f = (x) => { log(x); return x }',
236
+ { imports: { host: { log: console.log } } })
193
237
 
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:
238
+ // Whole namespacesin, cos, PI, all auto-wired (functions as imports, numeric constants folded)
239
+ jz('import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
240
+ { imports: { math: Math } })
195
241
 
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
- }
242
+ // globalThis works too
243
+ jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
244
+ { imports: { window: globalThis } })
201
245
  ```
202
246
 
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
247
  </details>
206
248
 
207
249
  <details>
208
- <summary><strong>How do I see and control inferred types?</strong></summary>
250
+ <summary><strong>Can I interpolate values (template literals)?</strong></summary>
209
251
 
210
- <br>
252
+ `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
253
 
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.
254
+ ```js
255
+ jz`export let f = () => ${'hello'}.length` // 5
256
+ jz`export let f = () => ${[10, 20, 30]}[1]` // 20
257
+ jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3
258
+
259
+ const scale = (x) => x * 10
260
+ jz`export let f = (n) => ${scale}(n) + 1` // f(2) → 21, host-called
261
+ ```
213
262
 
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.
263
+ Interpolated functions become host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
215
264
 
216
265
  </details>
217
266
 
218
267
  <details>
219
- <summary><strong>Is it production-ready?</strong></summary>
220
-
221
- <br>
268
+ <summary><strong>How to pass numbers, strings, arrays, objects JS ↔ WASM?</strong></summary>
222
269
 
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.
270
+ **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
271
 
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.
272
+ 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
273
 
227
- </details>
274
+ ```js
275
+ const { exports, memory } = jz`
276
+ export let greet = (s) => s.length
277
+ export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
278
+ export let rgb = (c) => [c, c * 0.5, c * 0.2]
279
+ export let process = (buf) => buf.map(x => x * 2)
280
+ `
228
281
 
229
- <details>
230
- <summary><strong>Why jz over Porffor or AssemblyScript?</strong></summary>
282
+ // Pass in
283
+ exports.greet(memory.String('hello')) // 5
284
+ exports.dist(memory.Object({ x: 3, y: 4 })) // 5
231
285
 
232
- <br>
286
+ // Get back
287
+ exports.rgb(100) // [100, 50, 20] — auto-decoded JS array
288
+ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
289
+ ```
233
290
 
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).
291
+ `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
292
 
236
293
  </details>
237
294
 
238
295
  <details>
239
- <summary><strong>Can I compile jz to C?</strong></summary>
296
+ <summary><strong>Do I need jz at runtime?</strong></summary>
240
297
 
241
- <br>
242
-
243
- Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
298
+ 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.
244
299
 
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
- ```
300
+ - **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.
301
+ - **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).
302
+ - **No JavaScript host at all** compile with `host: 'wasi'`; see the next question.
251
303
 
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
+ 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
305
 
254
306
  </details>
255
307
 
256
308
  <details>
257
- <summary><strong>Can I add my own operators or stdlib methods?</strong></summary>
309
+ <summary><strong>Can I run the `.wasm` without a JavaScript host (WASI)?</strong></summary>
258
310
 
259
- <br>
311
+ There's two possible `host` targets:
260
312
 
261
- Yesjz'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
+ - **`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.
314
+ - **`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.
262
315
 
263
- ```js
264
- import { emitter } from './src/emit.js'
265
- import { typed } from './src/ir.js'
316
+ 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:
266
317
 
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.
318
+ | What your code does | `js` (default) | `wasi` |
319
+ |---|---|---|
320
+ | `console.log()` | `env.print` — host stringifies | WASI `fd_write` |
321
+ | `Date.now()` / `performance.now()` | `env.now` → f64 | WASI `clock_time_get` |
322
+ | `setTimeout` / `setInterval` | `env.setTimeout` host schedules | WASM timer queue + `__timer_tick` |
323
+ | dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
272
324
 
273
325
  </details>
274
326
 
327
+ <details>
328
+ <summary><strong>How does memory work?</strong></summary>
275
329
 
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
-
330
+ 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.
331
+ Memory is never reclaimed implicitly — a long-running program that allocates per call grows without bound. Reset between independent batches:
293
332
 
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`._
333
+ ```js
334
+ for (let i = 0; i < 1000; i++) {
335
+ const sum = exports.process(100) // allocates an array each call
336
+ memory.reset() // drop everything; heap ptr → 1024
337
+ }
338
+ ```
295
339
 
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.
340
+ 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
341
 
342
+ </details>
298
343
 
299
344
  <details>
300
- <summary><strong>Optimizations</strong></summary>
301
-
302
- <br>
303
- High-impact summary behind the benchmark table, not an exhaustive list.
345
+ <summary><strong>Can modules share memory?</strong></summary>
304
346
 
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.
347
+ `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate, so objects created in one module are readable by another:
320
348
 
321
- #### Making array loops vectorize
349
+ ```js
350
+ const memory = jz.memory()
351
+ const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
352
+ const b = jz('export let read = (o) => o.x + o.y', { memory })
322
353
 
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).
354
+ b.exports.read(a.exports.make()) // 30 same memory, merged schemas
355
+ memory.read(a.exports.make()) // {x: 10, y: 20}
356
+ ```
324
357
 
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]`, `|`, `&`).
358
+ Pass an existing `WebAssembly.Memory` to wrap it: `jz.memory(new WebAssembly.Memory({ initial: 4 }))`.
326
359
 
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.
360
+ Each compiled module exposes two call surfaces:
328
361
 
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.
362
+ - **`.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).
363
+ - **`.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
364
 
331
365
  </details>
332
366
 
367
+ <details>
368
+ <summary><strong>How big is the output?</strong></summary>
333
369
 
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.
370
+ 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
371
 
338
- ### Passing data in and out
372
+ - **`optimize: 'size'`** keeps every size pass, drops loop unrolling and SIMD.
373
+ - **`alloc: false`** — omit the allocator for pure-numeric modules that never marshal heap values.
374
+ - **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `names: true`).
339
375
 
340
- Arrays of 8 elements come back as plain JS arrays (WASM multi-value); everything else stays heap-resident behind a pointer.
376
+ 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
377
 
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
- `
378
+ </details>
349
379
 
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
380
 
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
- ```
381
+ <details>
382
+ <summary><strong>Which optimizations are applied?</strong></summary>
362
383
 
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.
384
+ 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
385
 
366
- ### Template interpolation
386
+ - **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.
387
+ - **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).
388
+ - **Loops** — invariant hoisting, CSE, typed-array address reuse, induction-variable strength reduction, small fixed-count unrolling (mat4, biquad).
389
+ - **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.
390
+ - **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
391
 
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:
392
+ 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` (default `2`), or the named presets `'speed'` (= `3`, trades size for inlined constants and larger buffers) and `'size'` (drops unrolling and SIMD for the smallest wasm).
369
393
 
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
- ```
394
+ </details>
375
395
 
376
- Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
396
+ <details>
397
+ <summary><strong>How do I inspect or debug the output?</strong></summary>
377
398
 
378
- ### Calling host functions
399
+ - **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.
400
+ - **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. `--why-not-simd` (API: `whyNotSimd: true`) goes further — for each loop the auto-vectorizer declined it reports the first blocking op (e.g. `i32.rem_s: no lane-pure SIMD mapping`), so you don't have to grep the WAT to find what's one op away.
401
+ - **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.
402
+ - **Profiling** — `--names` (API: `names: true`) emits a wasm `name` section so DevTools profilers and disassemblers show real function names; `--stats` (API: the `profile` sink) collects per-stage compile timings.
403
+ - **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
404
 
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:
405
+ </details>
381
406
 
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 } } })
407
+ <details>
408
+ <summary><strong>How does jz work?</strong></summary>
386
409
 
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 } })
410
+ A source string flows through six stages into wasm bytes no IR leaves the process, the whole thing is one pass per `compile()`:
390
411
 
391
- // globalThis works too
392
- jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
393
- { imports: { window: globalThis } })
412
+ ```
413
+ your .js
414
+ parse jessie parser (subscript) AST
415
+ │ jzify lower legacy JS to the canonical subset (var/function/class/==/…)
416
+ │ prepare resolve & bundle imports, normalize the AST
417
+ │ compile type inference (i32 vs f64) + emit WAT IR; module/ handlers lower operators
418
+ │ optimize WAT-level passes — CSE, DCE, const-fold, inline, peephole
419
+ │ encode watr: WAT → WASM binary
420
+
421
+ .wasm
394
422
  ```
395
423
 
396
- ### Host features & runtime services
424
+ 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)).
397
425
 
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.
426
+ </details>
399
427
 
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 |
408
428
 
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.
429
+ <details>
430
+ <summary><strong>Why no type annotations?</strong></summary>
410
431
 
411
- ### Sharing memory across modules
412
-
413
- `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate, so objects created in one module are readable by another:
432
+ 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
433
 
415
434
  ```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
435
+ export let bits = (a, b) => a | b // i32 — a bitwise op pins both operands
436
+ export let half = (n) => n * 0.5 // f64 0.5 isn't an integer
422
437
  ```
423
438
 
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.
439
+ 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
440
 
426
- ### Shipping & running the `.wasm`
441
+ </details>
427
442
 
428
- Compile once, then run the binary anywhere.
429
443
 
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:
444
+ <details>
445
+ <summary><strong>Is jz production-ready?</strong></summary>
431
446
 
432
- ```js
433
- import { instantiate } from 'jz/interop'
434
- import wasmBytes from './program.wasm' // bundler-specific; or fetch(...)
447
+ 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
448
 
436
- const { exports, memory } = instantiate(wasmBytes)
437
- exports.greet(memory.String('hello')) // marshal works exactly as at compile time
438
- ```
449
+ </details>
439
450
 
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
451
 
442
- **Native runtimes.** Compile with `host: 'wasi'` and run on any WASM runtime:
452
+ <details>
453
+ <summary><strong>Can I compile in the browser or a Worker?</strong></summary>
443
454
 
444
- ```sh
445
- jz program.js --host wasi -o program.wasm
446
- wasmtime program.wasm # also `wasmer run` / `deno run`
447
- ```
455
+ 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.
456
+
457
+ </details>
448
458
 
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
459
 
451
- **Memory ABI (non-JS hosts).** The allocator is exposed as two exports:
460
+ <details>
461
+ <summary><strong>Can jz compile itself?</strong></summary>
452
462
 
453
- ```
454
- (func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
455
- (func $_clear) ;; rewinds heap pointer to 1024
456
- ```
463
+ 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.
457
464
 
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.
465
+ `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.
459
466
 
460
- <details>
461
- <summary><strong>Zero-copy strings</strong></summary>
467
+ </details>
462
468
 
463
- <br>
464
469
 
465
- Strings have two boundary carriers; the compiler picks per export-param:
470
+ <details>
471
+ <summary><strong>Can I compile jz to C?</strong></summary>
466
472
 
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 |
473
+ Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
471
474
 
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
475
+ ```sh
476
+ jz program.js -o program.wasm
477
+ wasm-opt -O3 program.wasm -o program.opt.wasm
478
+ wasm2c program.opt.wasm -o program.c
479
+ cc -O3 program.c -o program
484
480
  ```
485
481
 
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.
482
+ 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
483
 
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`.
484
+ [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
485
 
490
486
  </details>
491
487
 
492
- <details>
493
- <summary><strong>Custom sections</strong></summary>
494
488
 
495
- <br>
496
489
 
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
490
 
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. |
491
+ ## Performance
505
492
 
506
- Names are stable; binary layouts are not re-derive from the latest `interop.js` if you parse them yourself.
493
+ <img src="bench/bench.svg?v=5" alt="jz vs alternatives geometric-mean speed across the bench corpus, every rival compiled to WebAssembly and run in V8 (apples-to-apples); native C is the lone reference, jz = 1.00× baseline" width="720">
507
494
 
508
- </details>
509
495
 
496
+ See [bench →](https://dy.github.io/jz/bench/)
510
497
 
511
498
  ## Examples
512
499
 
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)):
500
+ <table>
501
+ <tr>
502
+ <td width="33%"><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> — frequency sweeps the nodal figure.</td>
503
+ <td width="33%"><a href="https://dy.github.io/jz/examples/julia/"><img src="examples/thumbs/julia.webp" width="100%" alt="Julia set"></a><br><b>julia</b> — escape-time Julia set; drag the constant to morph it.</td>
504
+ <td width="33%"><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, luminous curves.</td>
505
+ </tr>
506
+ <tr>
507
+ <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>
508
+ <td><a href="https://dy.github.io/jz/examples/nbody/"><img src="examples/thumbs/nbody.webp" width="100%" alt="N-body gravity"></a><br><b>nbody</b> — three-body gravity; fading trails trace the orbits.</td>
509
+ <td><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>
510
+ </tr>
511
+ <tr>
512
+ <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>
513
+ <td><a href="https://dy.github.io/jz/examples/cloth/"><img src="examples/thumbs/cloth.webp" width="100%" alt="Cloth simulation"></a><br><b>cloth</b> — Verlet mass-spring sheet; drag it, watch it settle.</td>
514
+ <td><a href="https://dy.github.io/jz/examples/erosion/"><img src="examples/thumbs/erosion.webp" width="100%" alt="Terrain erosion"></a><br><b>erosion</b> — hydraulic droplets carve a fractal terrain.</td>
515
+ </tr>
516
+ </table>
514
517
 
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
+ See [all examples →](https://dy.github.io/jz/examples/)
518
519
 
519
- Each folder has a `build.mjs` and an `index.html` — build, then open the page.
520
520
 
521
521
 
522
522
  ## Alternatives
523
523
 
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.
524
+ Small & fast JS subset full JS spec & bundled engine:
527
525
 
528
- <details>
529
- <summary><strong>Which one to choose?</strong></summary>
526
+ * [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) — TS-like dialect → WASM; small, fast output, but needs type annotations (not JS).
527
+ * [awasm-compiler](https://github.com/paulmillr/awasm-compiler) reproducible WASM assembled through a typed *builder API*.
528
+ * [Porffor](https://github.com/CanadaHonk/porffor) — AOT JS→WASM (and C) targeting the full spec, grown against test262.
529
+ * [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.
530
+ * [jawsm](https://github.com/drogus/jawsm) — JS→WASM in Rust on WasmGC; no interpreter, but leans on the engine's GC.
531
+ * [Javy](https://github.com/bytecodealliance/javy) — embeds QuickJS; runs almost any JS, but ships a full interpreter (large, interpreter-speed).
532
+ * [ComponentizeJS / jco](https://github.com/bytecodealliance/ComponentizeJS) — WASM Component via embedded SpiderMonkey; standards-complete, but bundles a JS engine.
530
533
 
531
- <br>
532
534
 
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. |
535
+ ## Built with
539
536
 
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.
537
+ * [**subscript**](https://github.com/dy/subscript) JS parser. Minimal, extensible, builds the exact AST jz needs. Jessie subset keeps the grammar small and deterministic.
538
+ * [**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`.
541
539
 
542
- </details>
543
540
 
544
- ## Build with
541
+ ## Contributing
545
542
 
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.
543
+ Setup, code layout, and the bench/perf invariants are in [CONTRIBUTING.md](docs/CONTRIBUTING.md);
544
+ the architecture & design rationale (NaN-boxing, type inference, native pipeline) in [DESIGN.md](docs/DESIGN.md).
548
545
 
549
546
 
550
547
  <p align=center>MIT • <a href="https://github.com/krishnized/license/">ॐ</a></p>