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 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
+ ## ![stability](https://img.shields.io/badge/stability-experimental-black) [![test](https://github.com/dy/jz/actions/workflows/test.yml/badge.svg)](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
+ })