jz 0.1.0 → 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 +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
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,16 +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
|
-
Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
|
|
28
|
-
<!-- Used internally by: web-audio-api, color-space, audiojs -->
|
|
29
|
-
|
|
30
28
|
| Good for | Not for |
|
|
31
29
|
|-----------------------------|----------------------------|
|
|
32
30
|
| Numeric / math compute | UI / frontend |
|
|
@@ -34,6 +32,9 @@ Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https:/
|
|
|
34
32
|
| Parsing / transforms | Async / I/O-heavy logic |
|
|
35
33
|
| WASM utilities | JavaScript runtime |
|
|
36
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
|
+
|
|
37
38
|
|
|
38
39
|
## Usage
|
|
39
40
|
|
|
@@ -48,8 +49,33 @@ add(2, 3) // 5
|
|
|
48
49
|
const wasm = compile('export let f = (x) => x * 2')
|
|
49
50
|
const mod = new WebAssembly.Module(wasm)
|
|
50
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
|
|
51
57
|
```
|
|
52
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
|
+
|
|
53
79
|
## CLI
|
|
54
80
|
|
|
55
81
|
`npm install -g jz`
|
|
@@ -78,7 +104,7 @@ flowchart TB
|
|
|
78
104
|
subgraph JS[JS — not supported]
|
|
79
105
|
subgraph JZify[JZ + jzify]
|
|
80
106
|
subgraph JZ[JZ strict]
|
|
81
|
-
j1["let/const, arrows, default/rest params, flow, break/continue, try/catch, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
|
|
107
|
+
j1["let/const, arrows, default/rest params, flow, break/continue, try/catch/finally, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
|
|
82
108
|
end
|
|
83
109
|
z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
|
|
84
110
|
end
|
|
@@ -94,12 +120,12 @@ flowchart TB
|
|
|
94
120
|
|
|
95
121
|
```
|
|
96
122
|
┌────────────────────────────────────────────────────────────────────────┐
|
|
97
|
-
│ JZify
|
|
123
|
+
│ JZify │
|
|
98
124
|
│ var function arguments switch new Foo() │
|
|
99
125
|
│ == != instanceof undefined │
|
|
100
126
|
│ │
|
|
101
127
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
|
102
|
-
│ │ JZ
|
|
128
|
+
│ │ JZ │ │
|
|
103
129
|
│ │ let/const => ...xs destructuring import/export │ │
|
|
104
130
|
│ │ if/else for/while/do-while/of/in break/continue │ │
|
|
105
131
|
│ │ try/catch/finally throw │ │
|
|
@@ -109,29 +135,30 @@ flowchart TB
|
|
|
109
135
|
│ │ console setTimeout/setInterval Date performance │ │
|
|
110
136
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
|
111
137
|
└────────────────────────────────────────────────────────────────────────┘
|
|
112
|
-
|
|
113
138
|
Not supported
|
|
114
139
|
async/await Promise function* yield
|
|
115
140
|
this class super extends delete labels
|
|
116
141
|
eval Function with Proxy Reflect WeakMap WeakSet
|
|
117
142
|
dynamic import DOM fetch Intl Node APIs
|
|
118
143
|
```
|
|
144
|
+
## FAQ
|
|
119
145
|
|
|
146
|
+
<details>
|
|
147
|
+
<summary><strong>How to pass data between JS and WASM?</strong></summary>
|
|
120
148
|
|
|
149
|
+
<br>
|
|
121
150
|
|
|
122
|
-
## FAQ
|
|
123
|
-
|
|
124
|
-
### How to pass data between JS and WASM?
|
|
125
151
|
|
|
126
|
-
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:
|
|
127
153
|
|
|
128
154
|
```js
|
|
129
|
-
const { exports, memory } = jz
|
|
155
|
+
const { exports, memory } = jz`
|
|
130
156
|
export let greet = (s) => s.length
|
|
131
157
|
export let sum = (a) => a.reduce((s, x) => s + x, 0)
|
|
132
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]
|
|
133
160
|
export let process = (buf) => buf.map(x => x * 2)
|
|
134
|
-
|
|
161
|
+
`
|
|
135
162
|
|
|
136
163
|
// JS → WASM (write)
|
|
137
164
|
memory.String('hello') // → string pointer
|
|
@@ -152,14 +179,17 @@ exports.greet(memory.String('hello')) // 5
|
|
|
152
179
|
exports.sum(memory.Array([1, 2, 3])) // 6
|
|
153
180
|
exports.dist(memory.Object({ x: 3, y: 4 })) // 5
|
|
154
181
|
|
|
155
|
-
//
|
|
182
|
+
// direct JS array return
|
|
183
|
+
exports.rgb(100) // [100, 50, 20]
|
|
184
|
+
|
|
185
|
+
// read pointer value
|
|
156
186
|
memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
|
|
157
187
|
```
|
|
158
188
|
|
|
159
189
|
Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
|
|
160
190
|
|
|
161
191
|
```js
|
|
162
|
-
jz
|
|
192
|
+
jz`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x`
|
|
163
193
|
```
|
|
164
194
|
|
|
165
195
|
<!--
|
|
@@ -190,7 +220,12 @@ All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. H
|
|
|
190
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.
|
|
191
221
|
-->
|
|
192
222
|
|
|
193
|
-
|
|
223
|
+
</details>
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary><strong>How does template interpolation work?</strong></summary>
|
|
227
|
+
|
|
228
|
+
<br>
|
|
194
229
|
|
|
195
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:
|
|
196
231
|
|
|
@@ -205,7 +240,12 @@ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
|
|
|
205
240
|
|
|
206
241
|
Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
|
|
207
242
|
|
|
208
|
-
|
|
243
|
+
</details>
|
|
244
|
+
|
|
245
|
+
<details>
|
|
246
|
+
<summary><strong>Does it support ES module imports?</strong></summary>
|
|
247
|
+
|
|
248
|
+
<br>
|
|
209
249
|
|
|
210
250
|
Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
|
|
211
251
|
|
|
@@ -235,40 +275,49 @@ const { exports } = jz(mainSrc, { modules: {
|
|
|
235
275
|
} })
|
|
236
276
|
```
|
|
237
277
|
|
|
238
|
-
|
|
278
|
+
</details>
|
|
239
279
|
|
|
240
|
-
|
|
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:
|
|
241
286
|
|
|
242
287
|
```js
|
|
288
|
+
// Custom function
|
|
243
289
|
const { exports } = jz(
|
|
244
290
|
'import { log } from "host"; export let f = (x) => { log(x); return x }',
|
|
245
291
|
{ imports: { host: { log: console.log } } }
|
|
246
292
|
)
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
You can also pass whole host environment objects — `Math`, `Date`, `window`, `console`, or any custom namespace object. jz extracts the functions it needs via `Object.getOwnPropertyNames`, so non-enumerable built-ins (like `Math.sin`) work automatically:
|
|
250
293
|
|
|
251
|
-
|
|
252
|
-
// Pass the entire Math namespace — sin, cos, sqrt, PI, etc. auto-wired
|
|
294
|
+
// Whole namespace — sin, cos, sqrt, PI, etc. all auto-wired
|
|
253
295
|
const { exports } = jz(
|
|
254
296
|
'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
|
|
255
297
|
{ imports: { math: Math } }
|
|
256
298
|
)
|
|
257
299
|
|
|
258
|
-
//
|
|
300
|
+
// Date static methods
|
|
259
301
|
const { exports } = jz(
|
|
260
302
|
'import { now } from "date"; export let f = () => now()',
|
|
261
303
|
{ imports: { date: Date } }
|
|
262
304
|
)
|
|
263
305
|
|
|
264
|
-
//
|
|
306
|
+
// window / globalThis
|
|
265
307
|
const { exports } = jz(
|
|
266
308
|
'import { parseInt } from "window"; export let f = () => parseInt("42")',
|
|
267
309
|
{ imports: { window: globalThis } }
|
|
268
310
|
)
|
|
269
311
|
```
|
|
270
312
|
|
|
271
|
-
|
|
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.
|
|
314
|
+
|
|
315
|
+
</details>
|
|
316
|
+
|
|
317
|
+
<details>
|
|
318
|
+
<summary><strong>Can two modules share data?</strong></summary>
|
|
319
|
+
|
|
320
|
+
<br>
|
|
272
321
|
|
|
273
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:
|
|
274
323
|
|
|
@@ -291,10 +340,54 @@ memory.Array([1, 2, 3]) // → NaN-boxed pointer
|
|
|
291
340
|
|
|
292
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.
|
|
293
342
|
|
|
294
|
-
|
|
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>
|
|
295
350
|
|
|
351
|
+
<br>
|
|
296
352
|
|
|
297
|
-
|
|
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>
|
|
386
|
+
|
|
387
|
+
<details>
|
|
388
|
+
<summary><strong>How do I run compiled WASM outside the browser?</strong></summary>
|
|
389
|
+
|
|
390
|
+
<br>
|
|
298
391
|
|
|
299
392
|
```sh
|
|
300
393
|
jz program.js -o program.wasm
|
|
@@ -305,40 +398,79 @@ wasmer run program.wasm
|
|
|
305
398
|
deno run program.wasm
|
|
306
399
|
```
|
|
307
400
|
|
|
308
|
-
|
|
401
|
+
Pure numeric modules have no imports and instantiate with standard
|
|
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.
|
|
309
403
|
|
|
404
|
+
Two host modes select how runtime services lower:
|
|
310
405
|
|
|
311
|
-
|
|
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.
|
|
412
|
+
|
|
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'`.
|
|
415
|
+
|
|
416
|
+
</details>
|
|
417
|
+
|
|
418
|
+
<details>
|
|
419
|
+
<summary><strong>What host features are supported?</strong></summary>
|
|
420
|
+
|
|
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 |
|
|
312
432
|
|
|
313
|
-
The compiled `.wasm` uses one import namespace:
|
|
433
|
+
The compiled `.wasm` uses at most one import namespace:
|
|
314
434
|
|
|
315
|
-
-
|
|
435
|
+
- none — pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
|
|
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.
|
|
316
438
|
|
|
317
|
-
|
|
318
|
-
|--------|---------|-------|
|
|
319
|
-
| `console.log()` | WASI `fd_write` (fd=1) | Multiple args space-separated, newline appended |
|
|
320
|
-
| `console.warn()`, `console.error()` | WASI `fd_write` (fd=2) | Writes to stderr |
|
|
321
|
-
| `Date.now()` | WASI `clock_time_get` (realtime) | Returns ms since epoch |
|
|
322
|
-
| `performance.now()` | WASI `clock_time_get` (monotonic) | Returns ms, high-resolution |
|
|
323
|
-
| `setTimeout`, `clearTimeout` | WASM timer queue + `__timer_tick` | JS runtime drives tick via `setInterval`; wasmtime uses blocking `__timer_loop` |
|
|
324
|
-
| `setInterval`, `clearInterval` | WASM timer queue + `__timer_tick` | Same — native WASM implementation, no host imports |
|
|
439
|
+
</details>
|
|
325
440
|
|
|
326
|
-
|
|
441
|
+
<details>
|
|
442
|
+
<summary><strong>How do I add custom operators / extend the stdlib?</strong></summary>
|
|
443
|
+
|
|
444
|
+
<br>
|
|
327
445
|
|
|
328
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:
|
|
329
447
|
|
|
330
448
|
```js
|
|
331
|
-
import { emitter } from '
|
|
449
|
+
import { emitter } from './src/emit.js'
|
|
450
|
+
import { typed } from './src/ir.js'
|
|
332
451
|
|
|
333
452
|
// Register a custom operator: my.double(x) → x * 2
|
|
334
453
|
emitter['my.double'] = (x) => {
|
|
335
|
-
return ['f64.mul', ['f64.const', 2], x]
|
|
454
|
+
return ['f64.mul', ['f64.const', 2], typed(x, 'f64')]
|
|
336
455
|
}
|
|
337
456
|
```
|
|
338
457
|
|
|
339
|
-
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 registers emitters
|
|
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>
|
|
340
472
|
|
|
341
|
-
|
|
473
|
+
<br>
|
|
342
474
|
|
|
343
475
|
Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
|
|
344
476
|
|
|
@@ -347,23 +479,50 @@ jz program.js -o program.wasm
|
|
|
347
479
|
wasm2c program.wasm -o program.c
|
|
348
480
|
cc program.c -o program
|
|
349
481
|
```
|
|
482
|
+
</details>
|
|
350
483
|
|
|
351
484
|
|
|
352
485
|
## Benchmark
|
|
353
486
|
|
|
354
|
-
| |
|
|
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/) |
|
|
355
488
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
356
|
-
| [
|
|
357
|
-
| [
|
|
358
|
-
| [
|
|
359
|
-
| [
|
|
360
|
-
| [
|
|
361
|
-
| [
|
|
362
|
-
| [
|
|
363
|
-
| [
|
|
364
|
-
| [
|
|
365
|
-
|
|
366
|
-
|
|
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>
|
|
367
526
|
|
|
368
527
|
|
|
369
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
|
|