jz 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +381 -0
- package/cli.js +163 -0
- package/index.js +217 -0
- package/module/array.js +1317 -0
- package/module/collection.js +791 -0
- package/module/console.js +190 -0
- package/module/core.js +642 -0
- package/module/function.js +180 -0
- package/module/index.js +15 -0
- package/module/json.js +504 -0
- package/module/math.js +389 -0
- package/module/number.js +605 -0
- package/module/object.js +357 -0
- package/module/regex.js +913 -0
- package/module/schema.js +104 -0
- package/module/string.js +928 -0
- package/module/symbol.js +54 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +711 -0
- package/package.json +54 -5
- package/src/analyze.js +1906 -0
- package/src/compile.js +2175 -0
- package/src/ctx.js +243 -0
- package/src/emit.js +2095 -0
- package/src/host.js +524 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +391 -0
- package/src/optimize.js +1352 -0
- package/src/prepare.js +1598 -0
- package/wasi.js +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dmitry Iv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<img src="logo.svg" alt="jz logo" width="120"/>
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
##  [](https://github.com/dy/jz/actions/workflows/test.yml)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
**JZ** (_javascript zero_) is **minimal modern functional JS subset**, compiling to WASM.<br/>
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import jz from 'jz'
|
|
12
|
+
|
|
13
|
+
// Distance between two points
|
|
14
|
+
const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
|
|
15
|
+
dist(3, 4) // 5
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why?
|
|
19
|
+
|
|
20
|
+
**Write plain JS, compile to WASM** – fast, portable and long-lasting. 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
|
+
* **Static** – no runtime, no GC, no dynamic constructs.
|
|
23
|
+
* **Valid jz = valid js** — test in browser, compile to wasm.
|
|
24
|
+
* **Minimal** — output is close to hand-written WAT.
|
|
25
|
+
<!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
|
|
26
|
+
|
|
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
|
+
| Good for | Not for |
|
|
31
|
+
|-----------------------------|----------------------------|
|
|
32
|
+
| Numeric / math compute | UI / frontend |
|
|
33
|
+
| DSP / audio / bytebeats | Backend / APIs |
|
|
34
|
+
| Parsing / transforms | Async / I/O-heavy logic |
|
|
35
|
+
| WASM utilities | JavaScript runtime |
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import jz, { compile } from 'jz'
|
|
42
|
+
|
|
43
|
+
// Compile, instantiate
|
|
44
|
+
const { exports: { add } } = jz('export let add = (a, b) => a + b')
|
|
45
|
+
add(2, 3) // 5
|
|
46
|
+
|
|
47
|
+
// Compile only — returns raw WASM binary (no JS adaptation)
|
|
48
|
+
const wasm = compile('export let f = (x) => x * 2')
|
|
49
|
+
const mod = new WebAssembly.Module(wasm)
|
|
50
|
+
const inst = new WebAssembly.Instance(mod)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## CLI
|
|
54
|
+
|
|
55
|
+
`npm install -g jz`
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# Compile
|
|
59
|
+
jz program.js # → program.wasm
|
|
60
|
+
|
|
61
|
+
# Evaluate
|
|
62
|
+
jz -e "1 + 2" # 3
|
|
63
|
+
|
|
64
|
+
# Show help
|
|
65
|
+
jz --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Language
|
|
69
|
+
|
|
70
|
+
JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
|
|
71
|
+
|
|
72
|
+
<!--
|
|
73
|
+
```mermaid
|
|
74
|
+
%%{init: {'flowchart': {'titleTopMargin': 0, 'padding': 0, 'margin': 0}}}%%
|
|
75
|
+
flowchart TB
|
|
76
|
+
classDef plain fill:none,stroke:none,font-size:14px,font-weight:bold,padding:0px,margin:0px
|
|
77
|
+
|
|
78
|
+
subgraph JS[JS — not supported]
|
|
79
|
+
subgraph JZify[JZ + jzify]
|
|
80
|
+
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
|
|
82
|
+
end
|
|
83
|
+
z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
|
|
84
|
+
end
|
|
85
|
+
n1["async/await, Promise, generators, this, class, eval, Function, with, Proxy, Reflect, WeakMap, WeakSet, dynamic import, DOM, fetch, Intl, Node APIs"]:::plain
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
style JZ fill:#ffe0b2,stroke-width:0
|
|
89
|
+
style JZify fill:#fff9c4,stroke-width:0
|
|
90
|
+
style JS fill:#ffffff,stroke:#ccc,stroke-width:1px
|
|
91
|
+
style n1 min-width:720px
|
|
92
|
+
```
|
|
93
|
+
-->
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
97
|
+
│ JZify test262: 68% │
|
|
98
|
+
│ var function arguments switch new Foo() │
|
|
99
|
+
│ == != instanceof undefined │
|
|
100
|
+
│ │
|
|
101
|
+
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
|
102
|
+
│ │ JZ test262: 54% │ │
|
|
103
|
+
│ │ let/const => ...xs destructuring import/export │ │
|
|
104
|
+
│ │ if/else for/while/do-while/of/in break/continue │ │
|
|
105
|
+
│ │ try/catch/finally throw │ │
|
|
106
|
+
│ │ operators strings booleans numbers arrays objects `${}` │ │
|
|
107
|
+
│ │ Math Number String Array Object JSON RegExp Symbol null │ │
|
|
108
|
+
│ │ ArrayBuffer DataView TypedArray Map Set │ │
|
|
109
|
+
│ │ console setTimeout/setInterval Date performance │ │
|
|
110
|
+
│ └────────────────────────────────────────────────────────────────────┘ │
|
|
111
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
112
|
+
|
|
113
|
+
Not supported
|
|
114
|
+
async/await Promise function* yield
|
|
115
|
+
this class super extends delete labels
|
|
116
|
+
eval Function with Proxy Reflect WeakMap WeakSet
|
|
117
|
+
dynamic import DOM fetch Intl Node APIs
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## FAQ
|
|
123
|
+
|
|
124
|
+
### How to pass data between JS and WASM?
|
|
125
|
+
|
|
126
|
+
Numbers pass directly as f64. Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const { exports, memory } = jz(\`
|
|
130
|
+
export let greet = (s) => s.length
|
|
131
|
+
export let sum = (a) => a.reduce((s, x) => s + x, 0)
|
|
132
|
+
export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
|
|
133
|
+
export let process = (buf) => buf.map(x => x * 2)
|
|
134
|
+
\`)
|
|
135
|
+
|
|
136
|
+
// JS → WASM (write)
|
|
137
|
+
memory.String('hello') // → string pointer
|
|
138
|
+
memory.Array([1, 2, 3]) // → array pointer
|
|
139
|
+
memory.Float64Array([1.0, 2.0]) // → typed array pointer
|
|
140
|
+
memory.Int32Array([10, 20, 30]) // all typed array constructors available
|
|
141
|
+
|
|
142
|
+
// ⚠ Objects: keys and order must match the jz source declaration.
|
|
143
|
+
// jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
|
|
144
|
+
// If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
|
|
145
|
+
memory.Object({ x: 3, y: 4 }) // → object pointer
|
|
146
|
+
|
|
147
|
+
// Strings/arrays inside objects are auto-wrapped to pointers:
|
|
148
|
+
memory.Object({ name: 'jz', count: 3 }) // name auto-wrapped via memory.String
|
|
149
|
+
|
|
150
|
+
// Call with pointers
|
|
151
|
+
exports.greet(memory.String('hello')) // 5
|
|
152
|
+
exports.sum(memory.Array([1, 2, 3])) // 6
|
|
153
|
+
exports.dist(memory.Object({ x: 3, y: 4 })) // 5
|
|
154
|
+
|
|
155
|
+
// WASM → JS (read)
|
|
156
|
+
memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
jz\`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x\`
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
<!--
|
|
166
|
+
### How does everything fit in f64?
|
|
167
|
+
|
|
168
|
+
All values are IEEE 754 f64 (at WASM boundary). Integers up to 2^53 are exact. Heap types use [NaN-boxing](https://nachtimwald.com/2019/11/06/nan-boxing/): quiet NaN (`0x7FF8`) + 51-bit payload `[type:4][aux:15][offset:32]`.
|
|
169
|
+
|
|
170
|
+
| Type | Code | Payload | Example |
|
|
171
|
+
|------|------|---------|---------|
|
|
172
|
+
| Number | — | regular f64 | `3.14`, `42`, `NaN` |
|
|
173
|
+
| Null | 0 | reserved pattern | `null` (distinct from `0` and `NaN`) |
|
|
174
|
+
| Array | 1 | aux=length, offset=heap | `[1, 2, 3]` |
|
|
175
|
+
| ArrayBuffer | 2 | offset=heap | `new ArrayBuffer(16)` |
|
|
176
|
+
| TypedArray | 3 | aux=elemType, offset=heap | `new Float64Array(n)` |
|
|
177
|
+
| String | 4 | offset=heap | `"hello world"` (>4 chars) |
|
|
178
|
+
| SSO String | 5 | aux=packed chars | `"hi"` (<=4 ASCII chars, zero alloc) |
|
|
179
|
+
| Object | 6 | aux=schemaId, offset=heap | `{x: 1, y: 2}` |
|
|
180
|
+
| Hash | 7 | offset=heap | dynamic string-keyed objects |
|
|
181
|
+
| Set | 8 | offset=heap | `new Set()` |
|
|
182
|
+
| Map | 9 | offset=heap | `new Map()` |
|
|
183
|
+
| Closure | 10 | aux=funcIdx, offset=env | `x => x + captured` |
|
|
184
|
+
| External | 11 | offset=hostMap index | JS host object references |
|
|
185
|
+
|
|
186
|
+
**Why NaN-boxing?** used by LuaJIT, JavaScriptCore, SpiderMonkey. The alternatives — tagged unions (OCaml, Haskell), pointer tagging (V8 Smis), or separate type+value pairs — all require branching at call boundaries or multi-word passing. NaN-boxing fits any value in one 64-bit word: one calling convention, one memory layout, one comparison instruction.
|
|
187
|
+
|
|
188
|
+
**The f64 tradeoff**: f64 arithmetic is ~1.2x slower than i32 for pure integer work on most architectures. jz mitigates this — `analyzeLocals` preserves i32 for loop counters, bitwise ops, and comparisons, so the penalty only applies to mixed-type parameters. The gain: zero interop cost at the JS↔WASM boundary (f64 is WASM's native JS-compatible type), no marshaling, no boxing/unboxing. For jz's target workloads (DSP, typed arrays, math), f64 is the natural type anyway.
|
|
189
|
+
|
|
190
|
+
**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
|
+
-->
|
|
192
|
+
|
|
193
|
+
### How does template interpolation work?
|
|
194
|
+
|
|
195
|
+
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
|
+
|
|
197
|
+
```js
|
|
198
|
+
jz`export let f = () => ${'hello'}.length` // 5 — string compiled as literal
|
|
199
|
+
jz`export let f = () => ${[10, 20, 30]}[1]` // 20 — array compiled as literal
|
|
200
|
+
jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3 — object compiled as literal
|
|
201
|
+
|
|
202
|
+
// Nested values work too
|
|
203
|
+
jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
|
|
207
|
+
|
|
208
|
+
### Does it support ES module imports?
|
|
209
|
+
|
|
210
|
+
Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const { exports } = jz(
|
|
214
|
+
'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
|
|
215
|
+
{ modules: { './math.jz': 'export let add = (a, b) => a + b' } }
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
|
|
220
|
+
|
|
221
|
+
**CLI** resolves filesystem imports automatically.
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
// Transitive bundling — all merged into one WASM
|
|
231
|
+
const { exports } = jz(mainSrc, { modules: {
|
|
232
|
+
'./math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
|
|
233
|
+
// Fetch sources yourself, pass them in
|
|
234
|
+
'./utils.jz': await fetch('./util.jz').then(r => r.text())
|
|
235
|
+
} })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Can I call JS/host functions from jz?
|
|
239
|
+
|
|
240
|
+
Yes — JS functions are wired at instantiation via the `imports` option:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
const { exports } = jz(
|
|
244
|
+
'import { log } from "host"; export let f = (x) => { log(x); return x }',
|
|
245
|
+
{ imports: { host: { log: console.log } } }
|
|
246
|
+
)
|
|
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
|
+
|
|
251
|
+
```js
|
|
252
|
+
// Pass the entire Math namespace — sin, cos, sqrt, PI, etc. auto-wired
|
|
253
|
+
const { exports } = jz(
|
|
254
|
+
'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
|
|
255
|
+
{ imports: { math: Math } }
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// Pass Date static methods
|
|
259
|
+
const { exports } = jz(
|
|
260
|
+
'import { now } from "date"; export let f = () => now()',
|
|
261
|
+
{ imports: { date: Date } }
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// Pass window / globalThis
|
|
265
|
+
const { exports } = jz(
|
|
266
|
+
'import { parseInt } from "window"; export let f = () => parseInt("42")',
|
|
267
|
+
{ imports: { window: globalThis } }
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Can two modules share data?
|
|
272
|
+
|
|
273
|
+
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
|
+
|
|
275
|
+
```js
|
|
276
|
+
const memory = jz.memory()
|
|
277
|
+
|
|
278
|
+
const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
|
|
279
|
+
const b = jz('export let read = (o) => o.x + o.y', { memory })
|
|
280
|
+
|
|
281
|
+
// Object from module a, processed by module b — same memory, merged schemas
|
|
282
|
+
b.exports.read(a.exports.make()) // 30
|
|
283
|
+
|
|
284
|
+
// Read from JS too — memory knows all schemas
|
|
285
|
+
memory.read(a.exports.make()) // {x: 10, y: 20}
|
|
286
|
+
|
|
287
|
+
// Write from JS before any compilation
|
|
288
|
+
memory.String('hello') // → NaN-boxed pointer
|
|
289
|
+
memory.Array([1, 2, 3]) // → NaN-boxed pointer
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`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
|
+
|
|
294
|
+
All modules sharing a memory use a single bump allocator (heap pointer at byte 1020). Use `.instance.exports` for raw pointers, `.exports` for the JS-wrapped surface.
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
### How do I run compiled WASM outside the browser?
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
jz program.js -o program.wasm
|
|
301
|
+
|
|
302
|
+
# Run with any WASM runtime
|
|
303
|
+
wasmtime program.wasm # WASI support built in
|
|
304
|
+
wasmer run program.wasm
|
|
305
|
+
deno run program.wasm
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`console.log` compiles to WASI `fd_write` — works natively on wasmtime/wasmer/deno without polyfills.
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
### What host features are supported?
|
|
312
|
+
|
|
313
|
+
The compiled `.wasm` uses one import namespace:
|
|
314
|
+
|
|
315
|
+
- `wasi_snapshot_preview1` — standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
|
|
316
|
+
|
|
317
|
+
| JS API | Maps to | Notes |
|
|
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 |
|
|
325
|
+
|
|
326
|
+
### How do I add custom operators / extend the stdlib?
|
|
327
|
+
|
|
328
|
+
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
|
+
|
|
330
|
+
```js
|
|
331
|
+
import { emitter } from 'jz/src/compile.js'
|
|
332
|
+
|
|
333
|
+
// Register a custom operator: my.double(x) → x * 2
|
|
334
|
+
emitter['my.double'] = (x) => {
|
|
335
|
+
return ['f64.mul', ['f64.const', 2], x]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
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 and stdlib on `ctx`.
|
|
340
|
+
|
|
341
|
+
### Can I compile jz to C?
|
|
342
|
+
|
|
343
|
+
Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
jz program.js -o program.wasm
|
|
347
|
+
wasm2c program.wasm -o program.c
|
|
348
|
+
cc program.c -o program
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
## Benchmark
|
|
353
|
+
|
|
354
|
+
| | **jz** | [Node](https://nodejs.org/) | [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/) | [Porffor](https://github.com/CanadaHonk/porffor) |
|
|
355
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
356
|
+
| [**biquad**](bench/biquad/biquad.js) | **6.44ms**<br>**3.4kB** | 12.30ms<br>3.2kB | 9.04ms<br>1.9kB | 6.48ms<br>767 B | 5.43ms | 9.03ms<br>fma | 5.09ms | 5.33ms | 3.15s | — |
|
|
357
|
+
| [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10ms**<br>**1.6kB** | 0.18ms<br>1.4kB | 0.08ms<br>1.5kB | — | 0.13ms | 0.07ms | 0.12ms | 0.12ms | 5.21ms | 0.46ms<br>2.6kB |
|
|
358
|
+
| [**mat4**](bench/mat4/mat4.js) | **4.00ms**<br>**1.7kB** | 11.64ms<br>1.1kB | 9.18ms<br>1.5kB | 7.99ms<br>353 B | 2.62ms | 11.93ms | 2.60ms | 0.80ms | 323.69ms | 87.65ms<br>2.3kB |
|
|
359
|
+
| [**aos**](bench/aos/aos.js) | **1.50ms**<br>**2.3kB** | 1.81ms<br>1.1kB | 1.91ms<br>2.2kB | — | 1.22ms | 0.90ms | 0.99ms | 1.20ms | 2.23ms | — |
|
|
360
|
+
| [**bitwise**](bench/bitwise/bitwise.js) | **4.93ms**<br>**1.2kB** | 5.31ms<br>1005 B | 12.36ms<br>1.5kB | 4.96ms<br>355 B | 1.31ms | 5.24ms | 4.26ms | 1.30ms | 14.89ms | — |
|
|
361
|
+
| [**poly**](bench/poly/poly.js) | **1.13ms**<br>**1.3kB** | 2.31ms<br>1014 B | 1.14ms<br>1.3kB | — | 0.52ms | 0.80ms | — | 0.52ms | 0.60ms | — |
|
|
362
|
+
| [**callback**](bench/callback/callback.js) | **0.01ms**<br>**1.5kB** | 1.03ms<br>828 B | 1.48ms<br>1.9kB | — | 0.09ms | 0.20ms | 0.01ms | 0.08ms | 1.84ms | — |
|
|
363
|
+
| [**json**](bench/json/json.js) | **0.20ms**<br>**2.8kB** | 0.38ms<br>923 B | — | — | 0.02ms | 1.06ms | — | 0.03ms | 1.19ms | — |
|
|
364
|
+
| [**watr**](bench/watr/watr.js) | **1.82ms**<br>**137.1kB** | 1.50ms<br>85.3kB | — | — | — | — | — | — | — | — |
|
|
365
|
+
|
|
366
|
+
_Numbers from `node bench/bench.mjs` on Apple Silicon._
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
## Alternatives
|
|
370
|
+
|
|
371
|
+
* [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.
|
|
372
|
+
* [assemblyscript](https://github.com/AssemblyScript/assemblyscript) — TypeScript-subset compiling to WASM — small, performant output, but requires type annotations.
|
|
373
|
+
* [jawsm](https://github.com/drogus/jawsm) — JS→WASM compiler in Rust. Compiles standard JS with a runtime that provides GC and closures in WASM.
|
|
374
|
+
|
|
375
|
+
## Build with
|
|
376
|
+
|
|
377
|
+
* [subscript](https://github.com/dy/subscript) — JS parser. Minimal, extensible, builds the exact AST jz needs without a full ES parser. Jessie subset keeps the grammar small and deterministic.
|
|
378
|
+
* [watr](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Handles binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into a valid `.wasm` binary.
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
<p align=center>MIT • <a href="https://github.com/krishnized/license/">ॐ</a></p>
|
package/cli.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JZ CLI - Command-line interface for JZ compiler
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
8
|
+
import { dirname, resolve, join } from 'path'
|
|
9
|
+
import { parse } from 'subscript/jessie'
|
|
10
|
+
import jz, { compile } from './index.js'
|
|
11
|
+
import jzifyFn, { codegen } from './src/jzify.js'
|
|
12
|
+
|
|
13
|
+
function showHelp() {
|
|
14
|
+
console.log(`
|
|
15
|
+
jz - min JS → WASM compiler
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
jz <file.js> Compile JS to WASM (auto-jzify)
|
|
19
|
+
jz --strict <file.js> Strict mode (no auto-transform)
|
|
20
|
+
jz --jzify <file.js> Transform JS → jz (auto-derives output file)
|
|
21
|
+
jz -e <expression> Evaluate expression
|
|
22
|
+
jz --help Show this help
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
jz program.js # → program.wasm
|
|
26
|
+
jz program.js --wat # → program.wat
|
|
27
|
+
jz program.js -o out.wasm # custom output name
|
|
28
|
+
jz program.js -o - # write to stdout
|
|
29
|
+
jz --strict program.js # strict mode
|
|
30
|
+
jz --jzify lib.js # → lib.jz
|
|
31
|
+
jz -e "1 + 2"
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--output, -o <file> Output file (.wat, .wasm, or - for stdout)
|
|
35
|
+
--strict Strict jz mode (no auto-transform)
|
|
36
|
+
--jzify Transform JS to jz (no compilation)
|
|
37
|
+
--eval, -e Evaluate expression or file
|
|
38
|
+
--wat Output WAT text instead of binary
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const args = process.argv.slice(2)
|
|
44
|
+
|
|
45
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
46
|
+
showHelp()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const evalIdx = args.indexOf('-e') !== -1 ? args.indexOf('-e') : args.indexOf('--eval')
|
|
52
|
+
const jzifyIdx = args.indexOf('--jzify')
|
|
53
|
+
if (jzifyIdx !== -1) await handleJzify(args.slice(jzifyIdx + 1))
|
|
54
|
+
else if (evalIdx !== -1) await handleEvaluate(args.slice(evalIdx + 1))
|
|
55
|
+
else await handleCompile(args)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(error)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleEvaluate(args) {
|
|
63
|
+
const input = args.join(' ')
|
|
64
|
+
let code
|
|
65
|
+
|
|
66
|
+
if (args.length === 1 && (args[0].endsWith('.js') || args[0].endsWith('.jz')))
|
|
67
|
+
code = readFileSync(args[0], 'utf8')
|
|
68
|
+
else
|
|
69
|
+
code = `export let _ = () => ${input}`
|
|
70
|
+
|
|
71
|
+
const { exports } = jz(code)
|
|
72
|
+
|
|
73
|
+
// If there's an exported _ (expression eval), call it
|
|
74
|
+
if (exports._) console.log(exports._())
|
|
75
|
+
else console.log(exports)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleJzify(args) {
|
|
79
|
+
let inputFile = null, outputFile = null
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
|
|
82
|
+
else if (!inputFile) inputFile = args[i]
|
|
83
|
+
}
|
|
84
|
+
if (!inputFile) throw new Error('No input file specified')
|
|
85
|
+
if (!outputFile) outputFile = inputFile.replace(/\.js$/, '.jz')
|
|
86
|
+
const code = readFileSync(inputFile, 'utf8')
|
|
87
|
+
const ast = parse(code)
|
|
88
|
+
const transformed = jzifyFn(ast)
|
|
89
|
+
const out = codegen(transformed) + '\n'
|
|
90
|
+
if (outputFile === '-') {
|
|
91
|
+
process.stdout.write(out)
|
|
92
|
+
} else {
|
|
93
|
+
writeFileSync(outputFile, out)
|
|
94
|
+
console.log(`${inputFile} → ${outputFile} (${out.length} chars)`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleCompile(args) {
|
|
99
|
+
let inputFile = null, outputFile = null, wat = false, strict = false
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < args.length; i++) {
|
|
102
|
+
if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
|
|
103
|
+
else if (args[i] === '--wat') wat = true
|
|
104
|
+
else if (args[i] === '--strict') strict = true
|
|
105
|
+
else if (!inputFile) inputFile = args[i]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!inputFile) throw new Error('No input file specified')
|
|
109
|
+
if (!outputFile) outputFile = inputFile.replace(/\.(js|jz)$/, wat ? '.wat' : '.wasm')
|
|
110
|
+
if (outputFile.endsWith('.wat')) wat = true
|
|
111
|
+
|
|
112
|
+
const code = readFileSync(inputFile, 'utf8')
|
|
113
|
+
|
|
114
|
+
// Resolve imports
|
|
115
|
+
const dir = dirname(resolve(inputFile))
|
|
116
|
+
const modules = {}
|
|
117
|
+
|
|
118
|
+
const pkgFile = join(dir, 'package.json')
|
|
119
|
+
if (existsSync(pkgFile)) {
|
|
120
|
+
try {
|
|
121
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'))
|
|
122
|
+
if (pkg.imports) for (const [spec, path] of Object.entries(pkg.imports)) {
|
|
123
|
+
const full = resolve(dir, path)
|
|
124
|
+
try { modules[spec] = readFileSync(full, 'utf8') } catch {}
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const importRe = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g
|
|
130
|
+
let m; while ((m = importRe.exec(code)) !== null) {
|
|
131
|
+
const spec = m[1]
|
|
132
|
+
if (!modules[spec] && (spec.startsWith('./') || spec.startsWith('../'))) {
|
|
133
|
+
const full = resolve(dir, spec)
|
|
134
|
+
try { modules[spec] = readFileSync(full, 'utf8') }
|
|
135
|
+
catch { try { modules[spec] = readFileSync(full + '.js', 'utf8') } catch {} }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// .jz = strict (no auto-transform), .js = auto-jzify
|
|
140
|
+
// --strict forces strict for any extension
|
|
141
|
+
const opts = {
|
|
142
|
+
wat,
|
|
143
|
+
jzify: !strict && !inputFile.endsWith('.jz'),
|
|
144
|
+
...(Object.keys(modules).length && { modules }),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const result = compile(code, opts)
|
|
148
|
+
|
|
149
|
+
if (outputFile === '-') {
|
|
150
|
+
process.stdout.write(result)
|
|
151
|
+
} else if (wat) {
|
|
152
|
+
writeFileSync(outputFile, result)
|
|
153
|
+
console.log(`${inputFile} → ${outputFile} (${result.length} chars)`)
|
|
154
|
+
} else {
|
|
155
|
+
writeFileSync(outputFile, result)
|
|
156
|
+
console.log(`${inputFile} → ${outputFile} (${result.byteLength} bytes)`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
main().catch(error => {
|
|
161
|
+
console.error(error)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
})
|