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.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /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
|
+
 [](http://npmjs.org/package/jz) [](https://github.com/dy/jz/actions/workflows/test.yml) [](https://github.com/dy/jz/actions/workflows/bench.yml)
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
##  [](http://npmjs.org/jz) [](https://github.com/dy/jz/actions/workflows/test.yml) [](https://github.com/dy/jz/actions/workflows/test262.yml) [](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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
|
-
//
|
|
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
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `strict: true` |
|
|
75
|
-
| `alloc: false` | Omit
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
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
|
-
|
|
92
|
-
jz
|
|
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
|
-
|
|
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
|
-
│ │
|
|
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
|
|
130
|
-
Proxy Reflect
|
|
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>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
- **
|
|
146
|
-
-
|
|
147
|
-
- **Errors are
|
|
148
|
-
- **`
|
|
149
|
-
-
|
|
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
|
-
<
|
|
200
|
+
<details>
|
|
201
|
+
<summary><strong>Can I use existing npm packages or JS libraries?</strong></summary>
|
|
159
202
|
|
|
160
|
-
Only
|
|
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
|
|
213
|
+
<summary><strong>Can I use import/export?</strong></summary>
|
|
171
214
|
|
|
172
|
-
|
|
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.
|
|
179
|
-
{ modules: { './math.
|
|
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
|
|
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>
|
|
229
|
+
<summary><strong>Can I call into the host (functions, objects)?</strong></summary>
|
|
189
230
|
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
// Whole namespace — sin, 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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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>
|
|
250
|
+
<summary><strong>Can I interpolate values (template literals)?</strong></summary>
|
|
209
251
|
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
220
|
-
|
|
221
|
-
<br>
|
|
268
|
+
<summary><strong>How to pass numbers, strings, arrays, objects JS ↔ WASM?</strong></summary>
|
|
222
269
|
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
282
|
+
// Pass in
|
|
283
|
+
exports.greet(memory.String('hello')) // 5
|
|
284
|
+
exports.dist(memory.Object({ x: 3, y: 4 })) // 5
|
|
231
285
|
|
|
232
|
-
|
|
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
|
-
|
|
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>
|
|
296
|
+
<summary><strong>Do I need jz at runtime?</strong></summary>
|
|
240
297
|
|
|
241
|
-
|
|
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
|
-
|
|
246
|
-
jz
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
309
|
+
<summary><strong>Can I run the `.wasm` without a JavaScript host (WASI)?</strong></summary>
|
|
258
310
|
|
|
259
|
-
|
|
311
|
+
There's two possible `host` targets:
|
|
260
312
|
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
+
Pass an existing `WebAssembly.Memory` to wrap it: `jz.memory(new WebAssembly.Memory({ initial: 4 }))`.
|
|
326
359
|
|
|
327
|
-
|
|
360
|
+
Each compiled module exposes two call surfaces:
|
|
328
361
|
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
+
<details>
|
|
397
|
+
<summary><strong>How do I inspect or debug the output?</strong></summary>
|
|
377
398
|
|
|
378
|
-
|
|
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
|
-
|
|
405
|
+
</details>
|
|
381
406
|
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
+
<details>
|
|
430
|
+
<summary><strong>Why no type annotations?</strong></summary>
|
|
410
431
|
|
|
411
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
+
</details>
|
|
427
442
|
|
|
428
|
-
Compile once, then run the binary anywhere.
|
|
429
443
|
|
|
430
|
-
|
|
444
|
+
<details>
|
|
445
|
+
<summary><strong>Is jz production-ready?</strong></summary>
|
|
431
446
|
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
+
<details>
|
|
453
|
+
<summary><strong>Can I compile in the browser or a Worker?</strong></summary>
|
|
443
454
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
461
|
-
<summary><strong>Zero-copy strings</strong></summary>
|
|
467
|
+
</details>
|
|
462
468
|
|
|
463
|
-
<br>
|
|
464
469
|
|
|
465
|
-
|
|
470
|
+
<details>
|
|
471
|
+
<summary><strong>Can I compile jz to C?</strong></summary>
|
|
466
472
|
|
|
467
|
-
|
|
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
|
-
```
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
541
|
+
## Contributing
|
|
545
542
|
|
|
546
|
-
|
|
547
|
-
|
|
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>
|