jz 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
<img src="jz.svg" alt="
|
|
1
|
+
<a href="https://dy.github.io/jz/"><img src="jz.svg" alt="JZ logo" width="120"/></a>
|
|
2
2
|
|
|
3
3
|
 [](http://npmjs.org/package/jz) [](https://github.com/dy/jz/actions/workflows/test.yml) [](https://github.com/dy/jz/actions/workflows/bench.yml)
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
5
|
+
**JZ** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
|
|
7
6
|
|
|
8
7
|
```js
|
|
9
8
|
import jz from 'jz'
|
|
@@ -12,19 +11,23 @@ const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
|
|
|
12
11
|
dist(3, 4) // 5
|
|
13
12
|
```
|
|
14
13
|
|
|
15
|
-
**[repl](https://dy.github.io/jz/repl/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
|
|
14
|
+
**[site](https://dy.github.io/jz/)** · **[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/)**
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
## Why?
|
|
19
18
|
|
|
20
|
-
|
|
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.
|
|
20
|
+
|
|
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 |
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|-----------------------------|----------------------------|
|
|
24
|
-
| Numeric / math compute | UI / frontend |
|
|
25
|
-
| DSP / audio / bytebeats | Backend / APIs |
|
|
26
|
-
| Parsing / transforms | Async / I/O-heavy logic |
|
|
27
|
-
| WASM utilities | JavaScript runtime |
|
|
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).
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
## Usage
|
|
@@ -32,7 +35,7 @@ jz distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_D
|
|
|
32
35
|
`npm install jz`
|
|
33
36
|
|
|
34
37
|
```js
|
|
35
|
-
import jz, { compile } from 'jz'
|
|
38
|
+
import jz, { compile, compileModule, instantiate } from 'jz'
|
|
36
39
|
|
|
37
40
|
// Compile + instantiate
|
|
38
41
|
const { exports: { add } } = jz('export let add = (a, b) => a + b')
|
|
@@ -41,6 +44,10 @@ add(2, 3) // 5
|
|
|
41
44
|
// Compile only — raw WASM binary
|
|
42
45
|
const wasm = compile('export let f = (x) => x * 2')
|
|
43
46
|
|
|
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
|
+
|
|
44
51
|
// Async startup
|
|
45
52
|
const asyncMod = await WebAssembly.compile(wasm)
|
|
46
53
|
const asyncInst = await WebAssembly.instantiate(asyncMod)
|
|
@@ -56,14 +63,20 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
|
|
|
56
63
|
|---|---|
|
|
57
64
|
| `modules: { specifier: source }` | Static ES imports to bundle. CLI import resolution does this from files automatically. |
|
|
58
65
|
| `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. |
|
|
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. |
|
|
60
67
|
| `host: 'js' \| 'wasi'` | Runtime-service lowering. Default `js`; `wasi` for standalone runtimes. |
|
|
61
|
-
| `optimize` | `false`/`0` off, `1`
|
|
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). |
|
|
62
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. |
|
|
63
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. |
|
|
64
76
|
| `randomSeed` | `Math.random` seeding — default draws from host entropy (non-reproducible); a number fixes it for a reproducible sequence, `true` forces entropy explicitly. |
|
|
65
77
|
| `wat: true` | `compile()` returns WAT text instead of WASM binary. |
|
|
66
|
-
| `
|
|
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). |
|
|
67
80
|
</details>
|
|
68
81
|
|
|
69
82
|
## CLI
|
|
@@ -74,7 +87,7 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
|
|
|
74
87
|
jz program.js # → program.wasm
|
|
75
88
|
jz program.js --wat # → program.wat
|
|
76
89
|
jz program.js -o out.wasm # custom output (- for stdout)
|
|
77
|
-
jz program.js -O3 # optimization: -O0 off, -O1
|
|
90
|
+
jz program.js -O3 # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
|
|
78
91
|
jz program.js --host wasi # standalone WASI output
|
|
79
92
|
jz --strict program.js # pure canonical subset (also implied by .jz extension)
|
|
80
93
|
jz -e "1 + 2" # eval → 3
|
|
@@ -84,7 +97,7 @@ jz -e "1 + 2" # eval → 3
|
|
|
84
97
|
<summary><code>jz --help</code></summary>
|
|
85
98
|
|
|
86
99
|
```
|
|
87
|
-
jz
|
|
100
|
+
jz - min JS → WASM compiler
|
|
88
101
|
|
|
89
102
|
Usage:
|
|
90
103
|
jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
|
|
@@ -98,8 +111,10 @@ Examples:
|
|
|
98
111
|
jz program.js --wat # → program.wat
|
|
99
112
|
jz program.js -o out.wasm # custom output name
|
|
100
113
|
jz program.js -o - # write to stdout
|
|
101
|
-
jz program.js -O3 #
|
|
114
|
+
jz program.js -O3 # optimize for speed
|
|
102
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)
|
|
103
118
|
jz program.js --host wasi # emit WASI Preview 1 imports
|
|
104
119
|
jz --strict program.js # strict mode
|
|
105
120
|
jz --jzify lib.js # → lib.jz
|
|
@@ -107,11 +122,22 @@ Examples:
|
|
|
107
122
|
|
|
108
123
|
Options:
|
|
109
124
|
--output, -o <file> Output file (.wat, .wasm, or - for stdout)
|
|
110
|
-
-O<n>, --optimize <n> Optimization level: 0 off, 1
|
|
111
|
-
3
|
|
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.
|
|
112
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
|
|
113
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
|
|
114
139
|
--names Emit wasm name section for profilers/debuggers
|
|
140
|
+
--stats Print compile-phase timings to stderr
|
|
115
141
|
--strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
|
|
116
142
|
--jzify Transform JS to jz source (no compilation)
|
|
117
143
|
--eval, -e Evaluate expression or file
|
|
@@ -125,7 +151,7 @@ Options:
|
|
|
125
151
|
|
|
126
152
|
## Language
|
|
127
153
|
|
|
128
|
-
|
|
154
|
+
JZ is a **strict modern JS subset**. Built-in jzify transform extends support to legacy patterns.
|
|
129
155
|
|
|
130
156
|
```
|
|
131
157
|
┌────────────────────────────────────────────────────────────────────────┐
|
|
@@ -159,10 +185,12 @@ Not supported
|
|
|
159
185
|
<details>
|
|
160
186
|
<summary><strong>What are the differences with JS?</strong></summary>
|
|
161
187
|
|
|
188
|
+
Each follows one rule: **JZ takes WASM/native conventions over JS edge-cases when it's free and the f64 value-precision of real computation is preserved** ([rationale](CONTRIBUTING.md#principles)).
|
|
189
|
+
|
|
162
190
|
- **Numbers are f64**; integer-proven values (loop counters, array idx, `| 0`) are `i32` and **wrap at ±2³¹**.
|
|
163
191
|
- **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
192
|
- **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
|
-
- **
|
|
193
|
+
- **Array indices are integers, typed-array access is unchecked** — an index coerces to `i32` (asm.js-style), so a fractional or `NaN` index *truncates* (`a[1.5]`→`a[1]`, `a[NaN]`→`a[0]`) rather than yielding JS's `undefined`. A `Float64Array`/etc. is fixed-size (`arr.length = n` won't compile) and read **raw**: an out-of-bounds or negative index reads arbitrary linear memory (a large one traps), not `undefined` — pass valid in-bounds integers. Plain `[]` arrays *are* bounds-checked (`undefined` past the end / for a negative index).
|
|
166
194
|
- **No GC** — call `memory.reset()` between batches; `WeakMap`/`WeakSet` wired to `Map`/`Set`.
|
|
167
195
|
- **`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
196
|
- **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.
|
|
@@ -174,12 +202,12 @@ Not supported
|
|
|
174
202
|
<details>
|
|
175
203
|
<summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
|
|
176
204
|
|
|
177
|
-
Only the ones that fit the
|
|
205
|
+
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.
|
|
178
206
|
|
|
179
207
|
- **Relative imports** (`./dep.js`) bundle at compile time.
|
|
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
|
|
208
|
+
- **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.
|
|
181
209
|
|
|
182
|
-
|
|
210
|
+
JZ is for compiling *your* numeric/DSP/parser code, not for running the npm ecosystem.
|
|
183
211
|
|
|
184
212
|
</details>
|
|
185
213
|
|
|
@@ -223,7 +251,7 @@ jz('import { parseInt } from "window"; export let f = () => parseInt("42")',
|
|
|
223
251
|
<details>
|
|
224
252
|
<summary><strong>Can I interpolate values (template literals)?</strong></summary>
|
|
225
253
|
|
|
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
|
|
254
|
+
`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:
|
|
227
255
|
|
|
228
256
|
```js
|
|
229
257
|
jz`export let f = () => ${'hello'}.length` // 5
|
|
@@ -241,7 +269,7 @@ Interpolated functions become host calls. Non-serializable values (host objects,
|
|
|
241
269
|
<details>
|
|
242
270
|
<summary><strong>How to pass numbers, strings, arrays, objects JS ↔ WASM?</strong></summary>
|
|
243
271
|
|
|
244
|
-
**Numbers cross natively** as `f64`/`i32`. **Heap values** — strings, arrays, objects, typed arrays — cross as NaN-
|
|
272
|
+
**Numbers cross natively** as `f64`/`i32`. **Heap values** — strings, arrays, objects, typed arrays — plus the `null`/`undefined`/boolean atoms cross as the **i64 NaN-box carrier** (a `BigInt` on the JS side) holding a tagged pointer into linear memory, allocated through the module's `_alloc`/`_clear` exports. i64 rather than f64 so the NaN payload survives JSC/Safari, which canonicalizes f64 NaN bits at the boundary; numbers stay f64 (free). That carrier-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)); a per-export [`jz:i64exp`](interop.js) custom section maps which params/results ride i64, and the signature itself is self-describing for non-JS hosts. The one shortcut: arrays of ≤ 8 numeric elements come back as plain JS arrays via WASM multi-value.
|
|
245
273
|
|
|
246
274
|
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`:
|
|
247
275
|
|
|
@@ -267,15 +295,15 @@ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2
|
|
|
267
295
|
</details>
|
|
268
296
|
|
|
269
297
|
<details>
|
|
270
|
-
<summary><strong>Do I need
|
|
298
|
+
<summary><strong>Do I need JZ at runtime?</strong></summary>
|
|
271
299
|
|
|
272
300
|
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.
|
|
273
301
|
|
|
274
|
-
- **Pure-number modules — nothing but the `.wasm`.** Instantiate with raw `WebAssembly`, zero
|
|
302
|
+
- **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
303
|
- **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
304
|
- **No JavaScript host at all** — compile with `host: 'wasi'`; see the next question.
|
|
277
305
|
|
|
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.
|
|
306
|
+
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.
|
|
279
307
|
|
|
280
308
|
</details>
|
|
281
309
|
|
|
@@ -285,7 +313,7 @@ For contrast, Rust (`wasm-bindgen`), Go (TinyGo), and C/Zig (Emscripten/WASI-lib
|
|
|
285
313
|
There's two possible `host` targets:
|
|
286
314
|
|
|
287
315
|
- **`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).
|
|
316
|
+
- **`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.
|
|
289
317
|
|
|
290
318
|
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:
|
|
291
319
|
|
|
@@ -301,7 +329,7 @@ Either way the `.wasm` carries at most one import namespace (none, `env`, or `wa
|
|
|
301
329
|
<details>
|
|
302
330
|
<summary><strong>How does memory work?</strong></summary>
|
|
303
331
|
|
|
304
|
-
|
|
332
|
+
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
333
|
Memory is never reclaimed implicitly — a long-running program that allocates per call grows without bound. Reset between independent batches:
|
|
306
334
|
|
|
307
335
|
```js
|
|
@@ -334,7 +362,7 @@ Pass an existing `WebAssembly.Memory` to wrap it: `jz.memory(new WebAssembly.Mem
|
|
|
334
362
|
Each compiled module exposes two call surfaces:
|
|
335
363
|
|
|
336
364
|
- **`.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
|
|
365
|
+
- **`.instance.exports`** — the raw `WebAssembly.Instance` exports: numbers pass through untouched, and a boxed return (string/array/object/atom) comes back as its raw i64 carrier (a `BigInt`). Decode it on the host with `memory.read(ptr)`, and pass it straight back *in* as an argument — the i64 carrier preserves the NaN payload across the boundary on every engine (including JSC/Safari), so no value is lost.
|
|
338
366
|
|
|
339
367
|
</details>
|
|
340
368
|
|
|
@@ -345,9 +373,9 @@ No runtime, no GC — a module is your code plus a small bump allocator. The geo
|
|
|
345
373
|
|
|
346
374
|
- **`optimize: 'size'`** — keeps every size pass, drops loop unrolling and SIMD.
|
|
347
375
|
- **`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 `
|
|
376
|
+
- **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `names: true`).
|
|
349
377
|
|
|
350
|
-
Hand-written WAT is still ~3–8× smaller on tight kernels —
|
|
378
|
+
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)).
|
|
351
379
|
|
|
352
380
|
</details>
|
|
353
381
|
|
|
@@ -355,7 +383,7 @@ Hand-written WAT is still ~3–8× smaller on tight kernels — jz carries gener
|
|
|
355
383
|
<details>
|
|
356
384
|
<summary><strong>Which optimizations are applied?</strong></summary>
|
|
357
385
|
|
|
358
|
-
Ordinary JS is already fast —
|
|
386
|
+
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):
|
|
359
387
|
|
|
360
388
|
- **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
389
|
- **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).
|
|
@@ -363,23 +391,23 @@ Ordinary JS is already fast — jz infers the right machine type for your number
|
|
|
363
391
|
- **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
392
|
- **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.
|
|
365
393
|
|
|
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`
|
|
394
|
+
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).
|
|
367
395
|
|
|
368
396
|
</details>
|
|
369
397
|
|
|
370
398
|
<details>
|
|
371
399
|
<summary><strong>How do I inspect or debug the output?</strong></summary>
|
|
372
400
|
|
|
373
|
-
- **Semantics** — valid
|
|
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.
|
|
401
|
+
- **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.
|
|
402
|
+
- **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.
|
|
375
403
|
- **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: `
|
|
404
|
+
- **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.
|
|
377
405
|
- **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.
|
|
378
406
|
|
|
379
407
|
</details>
|
|
380
408
|
|
|
381
409
|
<details>
|
|
382
|
-
<summary><strong>How does
|
|
410
|
+
<summary><strong>How does JZ work?</strong></summary>
|
|
383
411
|
|
|
384
412
|
A source string flows through six stages into wasm bytes — no IR leaves the process, the whole thing is one pass per `compile()`:
|
|
385
413
|
|
|
@@ -403,7 +431,7 @@ Each stage lives in its own place: parsing in [`subscript`](https://github.com/d
|
|
|
403
431
|
<details>
|
|
404
432
|
<summary><strong>Why no type annotations?</strong></summary>
|
|
405
433
|
|
|
406
|
-
Because `let x: i32` isn't valid JS — annotations would break the promise that valid
|
|
434
|
+
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:
|
|
407
435
|
|
|
408
436
|
```js
|
|
409
437
|
export let bits = (a, b) => a | b // i32 — a bitwise op pins both operands
|
|
@@ -416,7 +444,7 @@ Literals (`0` vs `0.5`), operators (`|` `<<` `&` ⇒ i32), and how a value is us
|
|
|
416
444
|
|
|
417
445
|
|
|
418
446
|
<details>
|
|
419
|
-
<summary><strong>Is
|
|
447
|
+
<summary><strong>Is JZ production-ready?</strong></summary>
|
|
420
448
|
|
|
421
449
|
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.
|
|
422
450
|
|
|
@@ -428,13 +456,15 @@ It's **experimental** (pre-1.0) — the supported subset and the wasm ABI may st
|
|
|
428
456
|
|
|
429
457
|
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.
|
|
430
458
|
|
|
459
|
+
Because compiling is that cheap, WASM becomes a *live medium*, not just a build artifact: hot-swap a compute kernel without a reload, recompile user-supplied source on the fly, or treat compiling as part of scripting — not a deploy step.
|
|
460
|
+
|
|
431
461
|
</details>
|
|
432
462
|
|
|
433
463
|
|
|
434
464
|
<details>
|
|
435
|
-
<summary><strong>Can
|
|
465
|
+
<summary><strong>Can JZ compile itself?</strong></summary>
|
|
436
466
|
|
|
437
|
-
Yes — fully.
|
|
467
|
+
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.
|
|
438
468
|
|
|
439
469
|
`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.
|
|
440
470
|
|
|
@@ -442,7 +472,7 @@ Yes — fully. jz compiles its own **entire** source to `dist/jz.wasm`: the whol
|
|
|
442
472
|
|
|
443
473
|
|
|
444
474
|
<details>
|
|
445
|
-
<summary><strong>Can I compile
|
|
475
|
+
<summary><strong>Can I compile JZ to C?</strong></summary>
|
|
446
476
|
|
|
447
477
|
Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
|
|
448
478
|
|
|
@@ -453,58 +483,49 @@ wasm2c program.opt.wasm -o program.c
|
|
|
453
483
|
cc -O3 program.c -o program
|
|
454
484
|
```
|
|
455
485
|
|
|
456
|
-
The full native pipeline (
|
|
486
|
+
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).
|
|
457
487
|
|
|
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;
|
|
488
|
+
[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.
|
|
459
489
|
|
|
460
490
|
</details>
|
|
461
491
|
|
|
462
492
|
|
|
463
493
|
|
|
464
494
|
|
|
465
|
-
|
|
466
|
-
|
|
467
495
|
## Performance
|
|
468
496
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
<img src="bench/bench.svg?v=2" alt="jz vs alternatives — geomean speed across the bench corpus" width="720">
|
|
497
|
+
<img src="bench/bench.svg?v=8" 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">
|
|
472
498
|
|
|
473
|
-
<sub>Local snapshot (M4 Max, darwin/arm64). Bun/Zig/Rust/Go/NumPy rows are hand-run reference points.</sub>
|
|
474
499
|
|
|
500
|
+
See [bench →](https://dy.github.io/jz/bench/)
|
|
475
501
|
|
|
476
502
|
## Examples
|
|
477
503
|
|
|
478
504
|
<table>
|
|
479
505
|
<tr>
|
|
480
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
481
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
482
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
506
|
+
<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>
|
|
507
|
+
<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>
|
|
508
|
+
<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>
|
|
483
509
|
</tr>
|
|
484
510
|
<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
511
|
<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>
|
|
512
|
+
<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>
|
|
513
|
+
<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>
|
|
493
514
|
</tr>
|
|
494
515
|
<tr>
|
|
495
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
496
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
497
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
516
|
+
<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>
|
|
517
|
+
<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>
|
|
518
|
+
<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>
|
|
498
519
|
</tr>
|
|
499
520
|
</table>
|
|
500
521
|
|
|
501
|
-
[
|
|
522
|
+
See [all examples →](https://dy.github.io/jz/examples/)
|
|
502
523
|
|
|
503
524
|
|
|
504
525
|
|
|
505
526
|
## Alternatives
|
|
506
527
|
|
|
507
|
-
|
|
528
|
+
Small & fast JS subset → full JS spec & bundled engine:
|
|
508
529
|
|
|
509
530
|
* [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) — TS-like dialect → WASM; small, fast output, but needs type annotations (not JS).
|
|
510
531
|
* [awasm-compiler](https://github.com/paulmillr/awasm-compiler) — reproducible WASM assembled through a typed *builder API*.
|
|
@@ -517,8 +538,14 @@ From small, fast JS subset to full JS spec, bundled engine:
|
|
|
517
538
|
|
|
518
539
|
## Built with
|
|
519
540
|
|
|
520
|
-
* [**subscript**](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST
|
|
521
|
-
* [**watr**](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Binary encoding, validation, and peephole optimization.
|
|
541
|
+
* [**subscript**](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST JZ needs. Jessie subset keeps the grammar small and deterministic.
|
|
542
|
+
* [**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`.
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
## Contributing
|
|
546
|
+
|
|
547
|
+
Setup, code layout, and the bench/perf invariants are in [CONTRIBUTING.md](CONTRIBUTING.md);
|
|
548
|
+
the architecture & design rationale (NaN-boxing, type inference, native pipeline) in [research.md](.work/research.md).
|
|
522
549
|
|
|
523
550
|
|
|
524
|
-
<p align=center>
|
|
551
|
+
<p align=center><a href="https://github.com/dy">dy</a> • <a href="https://github.com/krishnized/license/">ॐ</a></p>
|