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