jz 0.1.1 → 0.2.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 +257 -90
- package/cli.js +34 -8
- package/index.js +102 -13
- package/module/array.js +396 -133
- package/module/collection.js +455 -212
- package/module/console.js +169 -88
- package/module/core.js +246 -144
- package/module/date.js +691 -0
- package/module/function.js +81 -21
- package/module/index.js +2 -1
- package/module/json.js +483 -138
- package/module/math.js +79 -39
- package/module/number.js +188 -78
- package/module/object.js +227 -47
- package/module/regex.js +33 -30
- package/module/schema.js +14 -17
- package/module/string.js +434 -235
- package/module/symbol.js +4 -3
- package/module/timer.js +89 -31
- package/module/typedarray.js +174 -44
- package/package.json +3 -10
- package/src/analyze.js +626 -132
- package/src/autoload.js +14 -6
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +634 -145
- package/src/host.js +244 -51
- package/src/ir.js +81 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1132 -4
- package/src/prepare.js +321 -75
- package/src/vectorize.js +1016 -0
- package/wasi.js +13 -0
- package/src/key.js +0 -73
- package/src/source.js +0 -76
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<img src="
|
|
1
|
+
<img src="jz.svg" alt="jz logo" width="120"/>
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
##  [](https://github.com/dy/jz/actions/workflows/test.yml)
|
|
5
|
+
##  [](http://npmjs.org/jz) [](https://github.com/dy/jz/actions/workflows/test.yml)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
**JZ** (_javascript zero_) is **minimal modern functional JS subset**, compiling to WASM.<br/>
|
|
@@ -17,18 +17,14 @@ dist(3, 4) // 5
|
|
|
17
17
|
|
|
18
18
|
## Why?
|
|
19
19
|
|
|
20
|
-
**Write plain JS, compile to WASM** – fast, portable and long-lasting
|
|
20
|
+
**Write plain JS, compile to WASM** – fast, portable and long-lasting.<br>
|
|
21
|
+
JZ distills the modern functional core – the "good parts" ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) – from legacy semantics, features overhead and perf quirks.
|
|
21
22
|
|
|
22
|
-
* **Static** – no runtime, no GC, no dynamic constructs.
|
|
23
|
+
* **Static AOT** – no runtime, no GC, no dynamic constructs.
|
|
23
24
|
* **Valid jz = valid js** — test in browser, compile to wasm.
|
|
24
25
|
* **Minimal** — output is close to hand-written WAT.
|
|
25
26
|
<!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
|
|
26
27
|
|
|
27
|
-
**Used by**: [web-audio-api](https://github.com/audiojs/web-audio-api), [color-space](https://github.com/colorjs/color-space), [audiojs](https://github.com/colorjs/audiojs). Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
|
|
28
|
-
<!-- * [audio-filter](https://github.com/audiojs/audio-filter)
|
|
29
|
-
* [digital-filter](https://github.com/audiojs/digital-filter)
|
|
30
|
-
* [time-stretch](https://github.com/audiojs/time-stretch) -->
|
|
31
|
-
|
|
32
28
|
| Good for | Not for |
|
|
33
29
|
|-----------------------------|----------------------------|
|
|
34
30
|
| Numeric / math compute | UI / frontend |
|
|
@@ -36,6 +32,9 @@ dist(3, 4) // 5
|
|
|
36
32
|
| Parsing / transforms | Async / I/O-heavy logic |
|
|
37
33
|
| WASM utilities | JavaScript runtime |
|
|
38
34
|
|
|
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
|
+
|
|
39
38
|
|
|
40
39
|
## Usage
|
|
41
40
|
|
|
@@ -50,8 +49,33 @@ add(2, 3) // 5
|
|
|
50
49
|
const wasm = compile('export let f = (x) => x * 2')
|
|
51
50
|
const mod = new WebAssembly.Module(wasm)
|
|
52
51
|
const inst = new WebAssembly.Instance(mod)
|
|
52
|
+
|
|
53
|
+
// Async WASM startup — jz source compilation is still synchronous
|
|
54
|
+
const asyncMod = await WebAssembly.compile(wasm)
|
|
55
|
+
const asyncInst = await WebAssembly.instantiate(asyncMod)
|
|
56
|
+
asyncInst.exports.f(21) // 42
|
|
53
57
|
```
|
|
54
58
|
|
|
59
|
+
<details>
|
|
60
|
+
<summary><strong>Options</strong></summary><br>
|
|
61
|
+
|
|
62
|
+
Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones:
|
|
63
|
+
|
|
64
|
+
| Option | Use |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, and `undefined` by lowering them to the JZ subset. |
|
|
67
|
+
| `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
|
|
68
|
+
| `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
|
|
69
|
+
| `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. `memoryPages` remains as a legacy alias for the page count. |
|
|
70
|
+
| `host: 'js' \| 'wasi'` | Select runtime-service lowering. Default `js` uses small `env.*` imports auto-wired by `jz()`; `wasi` emits WASI Preview 1 imports for wasmtime/wasmer/deno. |
|
|
71
|
+
| `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default, `3` enables aggressive experimental passes, object form overrides individual passes. |
|
|
72
|
+
| `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
|
|
73
|
+
| `alloc: false` | Omit raw allocator exports like `_alloc`/`_clear` when compiling standalone WASM that never marshals heap values across the host boundary. |
|
|
74
|
+
| `wat: true` | `compile()` returns WAT text instead of a WASM binary. |
|
|
75
|
+
| `profile` | Pass a mutable sink to collect compile-stage timings; set `profile.names = true` to also emit a WASM `name` section for profiler/debugger symbolication. `profileNames` remains as a legacy alias. |
|
|
76
|
+
|
|
77
|
+
</details>
|
|
78
|
+
|
|
55
79
|
## CLI
|
|
56
80
|
|
|
57
81
|
`npm install -g jz`
|
|
@@ -102,56 +126,39 @@ flowchart TB
|
|
|
102
126
|
│ │
|
|
103
127
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
|
104
128
|
│ │ JZ │ │
|
|
105
|
-
│ │ let/const => ...xs destructuring import/export
|
|
129
|
+
│ │ let/const => ...xs destructuring import/export │ │
|
|
106
130
|
│ │ if/else for/while/do-while/of/in break/continue │ │
|
|
107
131
|
│ │ try/catch/finally throw │ │
|
|
108
|
-
│ │ operators strings booleans numbers arrays objects
|
|
109
|
-
│ │ Math Number String Array Object JSON RegExp Symbol
|
|
132
|
+
│ │ operators strings booleans numbers arrays objects `${}` │ │
|
|
133
|
+
│ │ Math Number String Array Object JSON RegExp Symbol null │ │
|
|
110
134
|
│ │ ArrayBuffer DataView TypedArray Map Set │ │
|
|
111
135
|
│ │ console setTimeout/setInterval Date performance │ │
|
|
112
136
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
|
113
137
|
└────────────────────────────────────────────────────────────────────────┘
|
|
114
|
-
|
|
115
138
|
Not supported
|
|
116
139
|
async/await Promise function* yield
|
|
117
140
|
this class super extends delete labels
|
|
118
141
|
eval Function with Proxy Reflect WeakMap WeakSet
|
|
119
142
|
dynamic import DOM fetch Intl Node APIs
|
|
120
143
|
```
|
|
144
|
+
## FAQ
|
|
121
145
|
|
|
146
|
+
<details>
|
|
147
|
+
<summary><strong>How to pass data between JS and WASM?</strong></summary>
|
|
122
148
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
| | **jz** | [Node](https://nodejs.org/) | [AS](https://github.com/AssemblyScript/assemblyscript) | WAT | C | [Go](https://go.dev/) | [Rust](https://www.rust-lang.org/) |
|
|
126
|
-
|---|---|---|---|---|---|---|---|
|
|
127
|
-
| [**biquad**](bench/biquad/biquad.js) | **6.44 ms**<br>**3.4 kB** | 12.30 ms<br>3.2 kB | 9.04 ms<br>1.9 kB | 6.48 ms<br>767 B | 5.43 ms<br>32.7 kB | 9.03 ms<br>1.60 MB<br>fma | 5.33 ms<br>380.7 kB |
|
|
128
|
-
| [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10 ms**<br>**1.6 kB** | 0.18 ms<br>1.4 kB | 0.08 ms<br>1.5 kB | — | 0.13 ms<br>32.9 kB | 0.07 ms<br>1.60 MB | 0.12 ms<br>380.7 kB |
|
|
129
|
-
| [**mat4**](bench/mat4/mat4.js) | **4.00 ms**<br>**1.7 kB** | 11.64 ms<br>1.1 kB | 9.18 ms<br>1.5 kB | 7.99 ms<br>353 B | 2.62 ms<br>32.8 kB | 11.93 ms<br>1.60 MB | 0.80 ms<br>380.7 kB |
|
|
130
|
-
| [**aos**](bench/aos/aos.js) | **1.50 ms**<br>**2.3 kB** | 1.81 ms<br>1.1 kB | 1.91 ms<br>2.2 kB | — | 1.22 ms<br>32.9 kB | 0.90 ms<br>1.60 MB | 1.20 ms<br>380.7 kB |
|
|
131
|
-
| [**bitwise**](bench/bitwise/bitwise.js) | **4.93 ms**<br>**1.2 kB** | 5.31 ms<br>1005 B | 12.36 ms<br>1.5 kB | 4.96 ms<br>355 B | 1.31 ms<br>32.9 kB | 5.24 ms<br>1.60 MB | 1.30 ms<br>380.7 kB |
|
|
132
|
-
| [**poly**](bench/poly/poly.js) | **1.13 ms**<br>**1.3 kB** | 2.31 ms<br>1014 B | 1.14 ms<br>1.3 kB | — | 0.52 ms<br>32.9 kB | 0.80 ms<br>1.60 MB | 0.52 ms<br>380.7 kB |
|
|
133
|
-
| [**callback**](bench/callback/callback.js) | **0.01 ms**<br>**1.5 kB** | 1.03 ms<br>828 B | 1.48 ms<br>1.9 kB | — | 0.09 ms<br>32.9 kB | 0.20 ms<br>1.60 MB | 0.08 ms<br>380.7 kB |
|
|
134
|
-
| [**json**](bench/json/json.js) | **0.20 ms**<br>**2.8 kB** | 0.38 ms<br>923 B | — | — | 0.02 ms<br>32.8 kB | 1.06 ms<br>1.97 MB | 0.03 ms<br>380.7 kB |
|
|
135
|
-
| [**watr**](bench/watr/watr.js) | **1.82 ms**<br>**137.1 kB** | 1.50 ms<br>2.6 kB | — | — | — | — | — |
|
|
136
|
-
|
|
137
|
-
_Numbers from `node bench/bench.mjs` on Apple Silicon, May 2026. `jz` uses host imports for benchmark timing/logging (measures wasm without WASI console/perf bloat). Raw-JS size is the entry file; jz size is the bundled wasm artifact. `fma` is the documented Go arm64 fused-multiply-add checksum class. See [benchmark](./bench/)._
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## FAQ
|
|
149
|
+
<br>
|
|
143
150
|
|
|
144
|
-
### How to pass data between JS and WASM?
|
|
145
151
|
|
|
146
|
-
Numbers pass directly as f64. Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
|
|
152
|
+
Numbers pass directly as f64, arrays of ≤ 8 elements return as plain JS arrays (multi-value). Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
|
|
147
153
|
|
|
148
154
|
```js
|
|
149
|
-
const { exports, memory } = jz
|
|
155
|
+
const { exports, memory } = jz`
|
|
150
156
|
export let greet = (s) => s.length
|
|
151
157
|
export let sum = (a) => a.reduce((s, x) => s + x, 0)
|
|
152
158
|
export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
|
|
159
|
+
export let rgb = (c) => [c, c * 0.5, c * 0.2]
|
|
153
160
|
export let process = (buf) => buf.map(x => x * 2)
|
|
154
|
-
|
|
161
|
+
`
|
|
155
162
|
|
|
156
163
|
// JS → WASM (write)
|
|
157
164
|
memory.String('hello') // → string pointer
|
|
@@ -159,8 +166,9 @@ memory.Array([1, 2, 3]) // → array pointer
|
|
|
159
166
|
memory.Float64Array([1.0, 2.0]) // → typed array pointer
|
|
160
167
|
memory.Int32Array([10, 20, 30]) // all typed array constructors available
|
|
161
168
|
|
|
162
|
-
// Objects: keys and order must match the jz source declaration.
|
|
169
|
+
// ⚠ Objects: keys and order must match the jz source declaration.
|
|
163
170
|
// jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
|
|
171
|
+
// If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
|
|
164
172
|
memory.Object({ x: 3, y: 4 }) // → object pointer
|
|
165
173
|
|
|
166
174
|
// Strings/arrays inside objects are auto-wrapped to pointers:
|
|
@@ -171,14 +179,17 @@ exports.greet(memory.String('hello')) // 5
|
|
|
171
179
|
exports.sum(memory.Array([1, 2, 3])) // 6
|
|
172
180
|
exports.dist(memory.Object({ x: 3, y: 4 })) // 5
|
|
173
181
|
|
|
174
|
-
//
|
|
182
|
+
// direct JS array return
|
|
183
|
+
exports.rgb(100) // [100, 50, 20]
|
|
184
|
+
|
|
185
|
+
// read pointer value
|
|
175
186
|
memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
|
|
176
187
|
```
|
|
177
188
|
|
|
178
189
|
Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
|
|
179
190
|
|
|
180
191
|
```js
|
|
181
|
-
jz
|
|
192
|
+
jz`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x`
|
|
182
193
|
```
|
|
183
194
|
|
|
184
195
|
<!--
|
|
@@ -209,7 +220,12 @@ All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. H
|
|
|
209
220
|
**NaN preservation**: IEEE 754 defines 2^52 − 1 distinct NaN bit patterns. WASM preserves NaN payload bits through arithmetic (spec requires `nondeterministic_nan`), and JS engines canonicalize only on certain operations (`Math.fround`, structured clone). jz uses quiet NaNs (`0x7FF8` prefix) which survive all standard paths. The 51 payload bits encode type (4), aux metadata (15), and heap offset (32) — enough for 4GB addressable memory and 12 type codes.
|
|
210
221
|
-->
|
|
211
222
|
|
|
212
|
-
|
|
223
|
+
</details>
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary><strong>How does template interpolation work?</strong></summary>
|
|
227
|
+
|
|
228
|
+
<br>
|
|
213
229
|
|
|
214
230
|
Numbers and booleans inline directly into source. Strings, arrays, and objects are serialized as jz source literals and compiled at compile time — no post-instantiation allocation, no getter overhead:
|
|
215
231
|
|
|
@@ -224,66 +240,84 @@ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
|
|
|
224
240
|
|
|
225
241
|
Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
|
|
226
242
|
|
|
227
|
-
|
|
243
|
+
</details>
|
|
244
|
+
|
|
245
|
+
<details>
|
|
246
|
+
<summary><strong>Does it support ES module imports?</strong></summary>
|
|
228
247
|
|
|
229
|
-
|
|
248
|
+
<br>
|
|
249
|
+
|
|
250
|
+
Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
|
|
230
251
|
|
|
231
252
|
```js
|
|
232
|
-
// modules: jz source bundled at compile time
|
|
233
253
|
const { exports } = jz(
|
|
234
254
|
'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
|
|
235
255
|
{ modules: { './math.jz': 'export let add = (a, b) => a + b' } }
|
|
236
256
|
)
|
|
237
|
-
|
|
238
|
-
// imports: JS functions wired at instantiation
|
|
239
|
-
const { exports } = jz(
|
|
240
|
-
'import { log } from "host"; export let f = (x) => { log(x); return x }',
|
|
241
|
-
{ imports: { host: { log: console.log } } }
|
|
242
|
-
)
|
|
243
257
|
```
|
|
244
258
|
|
|
245
259
|
Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
|
|
246
260
|
|
|
247
|
-
|
|
261
|
+
**CLI** resolves filesystem imports automatically.
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
|
|
248
268
|
|
|
249
269
|
```js
|
|
250
|
-
//
|
|
270
|
+
// Transitive bundling — all merged into one WASM
|
|
271
|
+
const { exports } = jz(mainSrc, { modules: {
|
|
272
|
+
'./math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
|
|
273
|
+
// Fetch sources yourself, pass them in
|
|
274
|
+
'./utils.jz': await fetch('./util.jz').then(r => r.text())
|
|
275
|
+
} })
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
</details>
|
|
279
|
+
|
|
280
|
+
<details>
|
|
281
|
+
<summary><strong>How do I pass values from the host to jz?</strong></summary>
|
|
282
|
+
|
|
283
|
+
<br>
|
|
284
|
+
|
|
285
|
+
Any host namespace — functions, constants, custom objects — wires in via the `imports` option. jz extracts what's needed via `Object.getOwnPropertyNames`, so non-enumerable built-ins (`Math.sin`, `Date.now`) work automatically:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
// Custom function
|
|
289
|
+
const { exports } = jz(
|
|
290
|
+
'import { log } from "host"; export let f = (x) => { log(x); return x }',
|
|
291
|
+
{ imports: { host: { log: console.log } } }
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
|
|
251
295
|
const { exports } = jz(
|
|
252
296
|
'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
|
|
253
297
|
{ imports: { math: Math } }
|
|
254
298
|
)
|
|
255
299
|
|
|
256
|
-
//
|
|
300
|
+
// Date static methods
|
|
257
301
|
const { exports } = jz(
|
|
258
302
|
'import { now } from "date"; export let f = () => now()',
|
|
259
303
|
{ imports: { date: Date } }
|
|
260
304
|
)
|
|
261
305
|
|
|
262
|
-
//
|
|
306
|
+
// window / globalThis
|
|
263
307
|
const { exports } = jz(
|
|
264
308
|
'import { parseInt } from "window"; export let f = () => parseInt("42")',
|
|
265
309
|
{ imports: { window: globalThis } }
|
|
266
310
|
)
|
|
267
311
|
```
|
|
268
312
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
```sh
|
|
272
|
-
jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
|
|
273
|
-
```
|
|
313
|
+
For per-call data (numbers, strings, arrays, objects, typed arrays), see *How to pass data between JS and WASM?* above — pointers via `memory.String`/`memory.Array`/`memory.Object` or template interpolation.
|
|
274
314
|
|
|
275
|
-
|
|
315
|
+
</details>
|
|
276
316
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const { exports } = jz(mainSrc, { modules: {
|
|
280
|
-
'./math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
|
|
281
|
-
// Fetch sources yourself, pass them in
|
|
282
|
-
'./utils.jz': await fetch('./util.jz').then(r => r.text())
|
|
283
|
-
} })
|
|
284
|
-
```
|
|
317
|
+
<details>
|
|
318
|
+
<summary><strong>Can two modules share data?</strong></summary>
|
|
285
319
|
|
|
286
|
-
|
|
320
|
+
<br>
|
|
287
321
|
|
|
288
322
|
Yes — `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate automatically, so objects created in one module are readable by another:
|
|
289
323
|
|
|
@@ -306,10 +340,54 @@ memory.Array([1, 2, 3]) // → NaN-boxed pointer
|
|
|
306
340
|
|
|
307
341
|
`jz.memory()` returns an actual `WebAssembly.Memory` (monkey-patched with `.read()`, `.String()`, `.Array()`, `.Object()`, `.write()`, etc). You can also pass an existing memory: `jz.memory(new WebAssembly.Memory({ initial: 4 }))` patches and returns the same object. Passing raw `WebAssembly.Memory` to `{ memory }` auto-wraps it.
|
|
308
342
|
|
|
309
|
-
|
|
343
|
+
Modules sharing a memory share a single bump allocator — see *How does memory work?* below. Use `.instance.exports` for raw pointers, `.exports` for the JS-wrapped surface.
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
</details>
|
|
347
|
+
|
|
348
|
+
<details>
|
|
349
|
+
<summary><strong>How does memory work? How do I reset it?</strong></summary>
|
|
350
|
+
|
|
351
|
+
<br>
|
|
352
|
+
|
|
353
|
+
jz uses a **bump allocator**: every heap value (string, array, object, typed array) bumps a single pointer forward. No free list, no GC, no per-object header overhead beyond `[len][cap]`. Bytes 0–1023 are reserved (data segment + heap-pointer slot at byte 1020); the heap starts at byte 1024 and grows the WASM memory automatically when full.
|
|
354
|
+
|
|
355
|
+
This means **memory is never reclaimed implicitly** — long-running programs that allocate per call will grow without bound. The fix is to reset the heap pointer between independent batches:
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
const { exports, memory } = jz`
|
|
359
|
+
export let process = (n) => {
|
|
360
|
+
let xs = []
|
|
361
|
+
for (let i = 0; i < n; i++) xs.push(i * 2)
|
|
362
|
+
return xs.reduce((s, x) => s + x, 0)
|
|
363
|
+
}
|
|
364
|
+
`
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < 1000; i++) {
|
|
367
|
+
const sum = exports.process(100) // allocates an array each call
|
|
368
|
+
memory.reset() // drop everything; heap ptr → 1024
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
After `memory.reset()` all previously returned pointers are invalid — read what you need first, then reset.
|
|
373
|
+
|
|
374
|
+
For finer control, allocate manually: `memory.alloc(bytes)` returns a raw offset using the same bump pointer. Pure scalar modules (no strings/arrays/objects) are compiled without the allocator at all — no `_alloc`, no `_clear`, no memory section.
|
|
375
|
+
|
|
376
|
+
**Non-JS hosts** (wasmtime, wasmer, deno, EdgeJS, embedded WASM) get the same allocator via two exports:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
(func $_alloc (param $bytes i32) (result i32)) ;; returns heap offset
|
|
380
|
+
(func $_clear) ;; rewinds heap pointer to 1024
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`memory.reset()` and `memory.alloc()` are JS-side aliases for these. Headers vary by type: strings store `[len:i32]` + utf8 bytes (offset = `_alloc(4+n) + 4`); arrays / typed arrays / objects store `[len:i32, cap:i32]` + payload (offset = `_alloc(8+bytes) + 8`). The pointer crossing the WASM boundary is the f64 NaN-box `0x7FF8 << 48 | type << 47 | aux << 32 | offset` — see [`src/host.js`](src/host.js) for type codes and the canonical encoders. Call `_clear()` between batches to reclaim. Strip both with `compile(code, { alloc: false })` if you only call functions and never marshal heap values across the boundary.
|
|
384
|
+
|
|
385
|
+
</details>
|
|
310
386
|
|
|
387
|
+
<details>
|
|
388
|
+
<summary><strong>How do I run compiled WASM outside the browser?</strong></summary>
|
|
311
389
|
|
|
312
|
-
|
|
390
|
+
<br>
|
|
313
391
|
|
|
314
392
|
```sh
|
|
315
393
|
jz program.js -o program.wasm
|
|
@@ -321,33 +399,78 @@ deno run program.wasm
|
|
|
321
399
|
```
|
|
322
400
|
|
|
323
401
|
Pure numeric modules have no imports and instantiate with standard
|
|
324
|
-
`WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS
|
|
325
|
-
|
|
326
|
-
|
|
402
|
+
`WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS hosts such as EdgeJS. Compile once at startup or build time, then reuse the module; do not compile JZ source per request.
|
|
403
|
+
|
|
404
|
+
Two host modes select how runtime services lower:
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
jz.compile(code) // host: 'js' (default) — env.* imports
|
|
408
|
+
jz.compile(code, { host: 'wasi' }) // wasi_snapshot_preview1.* imports
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`host: 'js'` (default) — `console.log`/`Date.now`/`performance.now` import from `env.*` and the JS host (`jz()` runtime) wires them automatically. Host-side stringification means jz drops `__ftoa`/`__write_*`/`__to_str` from the binary.
|
|
327
412
|
|
|
328
|
-
`console.log` compiles to WASI `fd_write
|
|
329
|
-
wasmtime/wasmer/deno
|
|
330
|
-
small `jz/wasi` polyfill; pass `{ write(fd, text) { ... } }` to capture or route
|
|
331
|
-
stdout/stderr without depending on `process.stdout`.
|
|
413
|
+
`host: 'wasi'` — `console.log` compiles to WASI `fd_write`, clocks to
|
|
414
|
+
`clock_time_get`. Output runs natively on wasmtime/wasmer/deno. In JS hosts, the small `jz/wasi` polyfill is auto-applied; pass `{ write(fd, text) {…} }` to capture stdout/stderr. `host: 'wasi'` errors at compile time if a program would emit `env.__ext_*` (dynamic dispatch into the JS host) — annotate the receiver or stay on `host: 'js'`.
|
|
332
415
|
|
|
416
|
+
</details>
|
|
333
417
|
|
|
334
|
-
|
|
418
|
+
<details>
|
|
419
|
+
<summary><strong>What host features are supported?</strong></summary>
|
|
335
420
|
|
|
336
|
-
|
|
421
|
+
<br>
|
|
422
|
+
|
|
423
|
+
| JS API | `host: 'js'` (default) | `host: 'wasi'` |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `console.log()` | `env.print(val: i64, fd: i32, sep: i32)` — host stringifies | WASI `fd_write` (fd=1), space-separated, newline appended |
|
|
426
|
+
| `console.warn`/`error` | same, fd=2 | WASI `fd_write` (fd=2) |
|
|
427
|
+
| `Date.now()` | `env.now(0) -> f64` (epoch ms) | `clock_time_get` (realtime) |
|
|
428
|
+
| `performance.now()` | `env.now(1) -> f64` (monotonic ms) | `clock_time_get` (monotonic) |
|
|
429
|
+
| `setTimeout`/`clearTimeout` | `env.setTimeout(cb, delay, repeat) -> f64` / `env.clearTimeout(id) -> f64` — host schedules; fires via exported `__invoke_closure` | WASM timer queue + `__timer_tick` (or blocking `__timer_loop` on wasmtime) |
|
|
430
|
+
| `setInterval`/`clearInterval` | same `env.setTimeout` (repeat=1) / `env.clearTimeout` | WASM timer queue + `__timer_tick` |
|
|
431
|
+
| dynamic `obj.method()` | `env.__ext_call` (JS resolves) | error at compile time |
|
|
432
|
+
|
|
433
|
+
The compiled `.wasm` uses at most one import namespace:
|
|
337
434
|
|
|
338
435
|
- none — pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
|
|
339
|
-
- `
|
|
436
|
+
- `env` — JS-host services (default). Auto-wired by the `jz()` runtime.
|
|
437
|
+
- `wasi_snapshot_preview1` — standard WASI Preview 1. Run natively on wasmtime/wasmer/deno.
|
|
438
|
+
|
|
439
|
+
</details>
|
|
440
|
+
|
|
441
|
+
<details>
|
|
442
|
+
<summary><strong>How do I add custom operators / extend the stdlib?</strong></summary>
|
|
340
443
|
|
|
341
|
-
|
|
342
|
-
|--------|---------|-------|
|
|
343
|
-
| `console.log()` | WASI `fd_write` (fd=1) | Multiple args space-separated, newline appended |
|
|
344
|
-
| `console.warn()`, `console.error()` | WASI `fd_write` (fd=2) | Writes to stderr |
|
|
345
|
-
| `Date.now()` | WASI `clock_time_get` (realtime) | Returns ms since epoch |
|
|
346
|
-
| `performance.now()` | WASI `clock_time_get` (monotonic) | Returns ms, high-resolution |
|
|
347
|
-
| `setTimeout`, `clearTimeout` | WASM timer queue + `__timer_tick` | JS runtime drives tick via `setInterval`; wasmtime uses blocking `__timer_loop` |
|
|
348
|
-
| `setInterval`, `clearInterval` | WASM timer queue + `__timer_tick` | Same — native WASM implementation, no host imports |
|
|
444
|
+
<br>
|
|
349
445
|
|
|
350
|
-
|
|
446
|
+
jz's emitter table (`ctx.core.emit`) maps AST operators → WASM IR generators. Module files in `module/` register handlers on it. To add your own:
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
import { emitter } from './src/emit.js'
|
|
450
|
+
import { typed } from './src/ir.js'
|
|
451
|
+
|
|
452
|
+
// Register a custom operator: my.double(x) → x * 2
|
|
453
|
+
emitter['my.double'] = (x) => {
|
|
454
|
+
return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
The naming convention follows the AST path: `Math.sin` → `math.sin`, `arr.push` → `.push`, typed variants like `.f64:push`. See any file in `module/` for the full pattern — each exports a function that receives `ctx` and registers emitters, stdlib, globals, or helpers.
|
|
459
|
+
|
|
460
|
+
Inside a runtime module, import directly from the layer you need:
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
import { emit } from '../src/emit.js'
|
|
464
|
+
import { asF64, temp } from '../src/ir.js'
|
|
465
|
+
import { valTypeOf, VAL } from '../src/analyze.js'
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
</details>
|
|
469
|
+
|
|
470
|
+
<details>
|
|
471
|
+
<summary><strong>Can I compile jz to C?</strong></summary>
|
|
472
|
+
|
|
473
|
+
<br>
|
|
351
474
|
|
|
352
475
|
Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
|
|
353
476
|
|
|
@@ -356,6 +479,50 @@ jz program.js -o program.wasm
|
|
|
356
479
|
wasm2c program.wasm -o program.c
|
|
357
480
|
cc program.c -o program
|
|
358
481
|
```
|
|
482
|
+
</details>
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
## Benchmark
|
|
486
|
+
|
|
487
|
+
| | 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/) |
|
|
488
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
489
|
+
| [biquad](bench/biquad/biquad.js) | 4.63ms<br>4.0kB | 8.68ms<br>3.2kB | fails | 6.59ms<br>1.9kB | 6.45ms<br>767 B | 5.30ms | 8.91ms<br>fma | 5.06ms | 5.28ms | 3.12s |
|
|
490
|
+
| [tokenizer](bench/tokenizer/tokenizer.js) | 0.07ms<br>1.8kB | 0.12ms<br>1.4kB | 0.34ms<br>2.6kB | 0.05ms<br>1.5kB | 0.08ms<br>344 B | 0.14ms | 0.07ms | 0.12ms | 0.12ms | 5.15ms |
|
|
491
|
+
| [mat4](bench/mat4/mat4.js) | 2.12ms<br>3.7kB | 11.80ms<br>1.2kB | 88.54ms<br>2.4kB<br>diff | 9.21ms<br>1.6kB | 8.06ms<br>414 B | 2.73ms | 11.93ms | 2.73ms | 1.77ms | 387.60ms |
|
|
492
|
+
| [aos](bench/aos/aos.js) | 1.11ms<br>2.3kB | 1.26ms<br>1.1kB | fails | 1.33ms<br>2.2kB | 1.07ms<br>481 B | 1.20ms | 0.91ms | 0.91ms | 1.20ms | 2.57ms |
|
|
493
|
+
| [mandelbrot](bench/mandelbrot/mandelbrot.js) | 8.02ms<br>1.2kB | 9.06ms<br>1.8kB | 9.71ms<br>3.0kB | 8.00ms<br>1.3kB | — | 8.31ms | 8.80ms | 7.83ms | 8.52ms | — |
|
|
494
|
+
| [bitwise](bench/bitwise/bitwise.js) | 0.98ms<br>1.3kB | 3.76ms<br>1005 B | fails | 8.79ms<br>1.5kB | 4.86ms<br>355 B | 1.30ms | 5.20ms | 4.15ms | 1.30ms | 14.72ms |
|
|
495
|
+
| [poly](bench/poly/poly.js) | 0.27ms<br>1.4kB | 1.62ms<br>1014 B | fails | 0.73ms<br>1.3kB | 0.81ms<br>359 B | 0.57ms | 0.79ms | 0.89ms | 0.63ms | 0.60ms |
|
|
496
|
+
| [callback](bench/callback/callback.js) | 0.03ms<br>1.6kB | 0.69ms<br>828 B | fails | 1.04ms<br>1.9kB | 0.24ms<br>267 B | 0.08ms | 0.23ms | 0.01ms | 0.12ms | 1.78ms |
|
|
497
|
+
| [json](bench/json/json.js) | 0.24ms<br>11.0kB | 0.37ms<br>1.2kB | fails | — | — | 0.03ms | 1.05ms | <0.01ms | 0.03ms | 1.21ms |
|
|
498
|
+
| [json-dynamic](bench/json-dynamic/json-dynamic.js) | 0.24ms<br>11.3kB | 0.39ms<br>1.2kB | — | — | — | — | — | — | — | — |
|
|
499
|
+
| [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
|
|
500
|
+
|
|
501
|
+
_Numbers from `node bench/bench.mjs` on Apple Silicon. Porffor cells were refreshed with `porf` 0.61.13; `fails` means the latest Porffor compiler/runtime did not complete that benchmark._
|
|
502
|
+
|
|
503
|
+
<details>
|
|
504
|
+
<summary><strong>Optimizations</strong></summary>
|
|
505
|
+
|
|
506
|
+
<br>
|
|
507
|
+
High-impact summary behind the benchmark table, not an exhaustive list.
|
|
508
|
+
|
|
509
|
+
| Optimization | Effect |
|
|
510
|
+
|---|---|
|
|
511
|
+
| Escape scalar replacement | Removes short-lived object/array literals before allocation. |
|
|
512
|
+
| Stack rest-param scalarization | Fixed-arity internal calls avoid heap rest arrays. |
|
|
513
|
+
| Scoped arena rewind | Safely rewinds allocations in functions proven not to return or persist heap values. |
|
|
514
|
+
| 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. |
|
|
515
|
+
| 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. |
|
|
516
|
+
| Typed-array specialization and address fusion | Monomorphic/bimorphic typed-array paths skip generic index dispatch and fuse repeated address bases/offsets in hot loops. |
|
|
517
|
+
| 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. |
|
|
518
|
+
| SIMD lane-local vectorization | Beats V8 on bitwise and keeps scalar feedback loops such as biquad untouched. |
|
|
519
|
+
| Small constant loop unroll | Required for biquad and mat4 speed; size cost is pinned. |
|
|
520
|
+
| OBJECT-only ternary type propagation | Keeps bimorphic object reads on typed dynamic dispatch without broad type-risk. |
|
|
521
|
+
| Benchmark checksum helper inlining | Avoids pulling generic ToNumber/string conversion into typed-array checksum binaries; mandelbrot drops from ~5.0kB to ~1.2kB. |
|
|
522
|
+
|
|
523
|
+
`npm run test:bench-pin` 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.
|
|
524
|
+
|
|
525
|
+
</details>
|
|
359
526
|
|
|
360
527
|
|
|
361
528
|
## Alternatives
|
package/cli.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
8
8
|
import { dirname, resolve, join } from 'path'
|
|
9
|
-
import {
|
|
9
|
+
import { pathToFileURL } from 'url'
|
|
10
|
+
import { execFileSync } from 'child_process'
|
|
11
|
+
import { parse } from 'subscript/feature/jessie'
|
|
10
12
|
import jz, { compile } from './index.js'
|
|
11
13
|
import jzifyFn, { codegen } from './src/jzify.js'
|
|
12
14
|
|
|
@@ -36,6 +38,7 @@ Options:
|
|
|
36
38
|
--jzify Transform JS to jz (no compilation)
|
|
37
39
|
--eval, -e Evaluate expression or file
|
|
38
40
|
--wat Output WAT text instead of binary
|
|
41
|
+
--resolve Resolve bare specifiers via Node.js module resolution
|
|
39
42
|
`)
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -96,12 +99,13 @@ async function handleJzify(args) {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
async function handleCompile(args) {
|
|
99
|
-
let inputFile = null, outputFile = null, wat = false, strict = false
|
|
102
|
+
let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false
|
|
100
103
|
|
|
101
104
|
for (let i = 0; i < args.length; i++) {
|
|
102
105
|
if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
|
|
103
106
|
else if (args[i] === '--wat') wat = true
|
|
104
107
|
else if (args[i] === '--strict') strict = true
|
|
108
|
+
else if (args[i] === '--resolve') resolveNode = true
|
|
105
109
|
else if (!inputFile) inputFile = args[i]
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -126,21 +130,43 @@ async function handleCompile(args) {
|
|
|
126
130
|
} catch {}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// Recursively resolve relative imports from entry file and all discovered modules
|
|
129
134
|
const importRe = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const resolveBareModule = (specifier, fromDir) => execFileSync(
|
|
136
|
+
process.execPath,
|
|
137
|
+
['--input-type=module', '-e', 'process.stdout.write(import.meta.resolve(process.argv[1]))', specifier],
|
|
138
|
+
{ cwd: fromDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
139
|
+
).trim()
|
|
140
|
+
const resolveModule = (specifier, fromDir) => {
|
|
141
|
+
if (modules[specifier]) return
|
|
142
|
+
// Relative imports: resolve from filesystem
|
|
143
|
+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
144
|
+
const full = resolve(fromDir, specifier)
|
|
145
|
+
let src
|
|
146
|
+
try { src = readFileSync(full, 'utf8') }
|
|
147
|
+
catch { try { src = readFileSync(full + '.js', 'utf8') } catch { return } }
|
|
148
|
+
modules[specifier] = src
|
|
149
|
+
let m; importRe.lastIndex = 0
|
|
150
|
+
while ((m = importRe.exec(src)) !== null) resolveModule(m[1], dirname(full))
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
// Bare specifiers: opt-in Node.js resolution
|
|
154
|
+
if (resolveNode) {
|
|
155
|
+
try {
|
|
156
|
+
const resolved = resolveBareModule(specifier, fromDir)
|
|
157
|
+
if (resolved.startsWith('file:')) modules[specifier] = readFileSync(new URL(resolved), 'utf8')
|
|
158
|
+
} catch {}
|
|
136
159
|
}
|
|
137
160
|
}
|
|
161
|
+
let m; importRe.lastIndex = 0
|
|
162
|
+
while ((m = importRe.exec(code)) !== null) resolveModule(m[1], dir)
|
|
138
163
|
|
|
139
164
|
// .jz = strict (no auto-transform), .js = auto-jzify
|
|
140
165
|
// --strict forces strict for any extension
|
|
141
166
|
const opts = {
|
|
142
167
|
wat,
|
|
143
168
|
jzify: !strict && !inputFile.endsWith('.jz'),
|
|
169
|
+
importMetaUrl: pathToFileURL(resolve(inputFile)).href,
|
|
144
170
|
...(Object.keys(modules).length && { modules }),
|
|
145
171
|
}
|
|
146
172
|
|