jz 0.0.0 → 0.1.1

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,373 @@
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, specs evolution 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
+ **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
+ | Good for | Not for |
33
+ |-----------------------------|----------------------------|
34
+ | Numeric / math compute | UI / frontend |
35
+ | DSP / audio / bytebeats | Backend / APIs |
36
+ | Parsing / transforms | Async / I/O-heavy logic |
37
+ | WASM utilities | JavaScript runtime |
38
+
39
+
40
+ ## Usage
41
+
42
+ ```js
43
+ import jz, { compile } from 'jz'
44
+
45
+ // Compile, instantiate
46
+ const { exports: { add } } = jz('export let add = (a, b) => a + b')
47
+ add(2, 3) // 5
48
+
49
+ // Compile only — returns raw WASM binary (no JS adaptation)
50
+ const wasm = compile('export let f = (x) => x * 2')
51
+ const mod = new WebAssembly.Module(wasm)
52
+ const inst = new WebAssembly.Instance(mod)
53
+ ```
54
+
55
+ ## CLI
56
+
57
+ `npm install -g jz`
58
+
59
+ ```sh
60
+ # Compile
61
+ jz program.js # → program.wasm
62
+
63
+ # Evaluate
64
+ jz -e "1 + 2" # 3
65
+
66
+ # Show help
67
+ jz --help
68
+ ```
69
+
70
+ ## Language
71
+
72
+ JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
73
+
74
+ <!--
75
+ ```mermaid
76
+ %%{init: {'flowchart': {'titleTopMargin': 0, 'padding': 0, 'margin': 0}}}%%
77
+ flowchart TB
78
+ classDef plain fill:none,stroke:none,font-size:14px,font-weight:bold,padding:0px,margin:0px
79
+
80
+ subgraph JS[JS — not supported]
81
+ subgraph JZify[JZ + jzify]
82
+ subgraph JZ[JZ strict]
83
+ 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
84
+ end
85
+ z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
86
+ end
87
+ n1["async/await, Promise, generators, this, class, eval, Function, with, Proxy, Reflect, WeakMap, WeakSet, dynamic import, DOM, fetch, Intl, Node APIs"]:::plain
88
+ end
89
+
90
+ style JZ fill:#ffe0b2,stroke-width:0
91
+ style JZify fill:#fff9c4,stroke-width:0
92
+ style JS fill:#ffffff,stroke:#ccc,stroke-width:1px
93
+ style n1 min-width:720px
94
+ ```
95
+ -->
96
+
97
+ ```
98
+ ┌────────────────────────────────────────────────────────────────────────┐
99
+ │ JZify │
100
+ │ var function arguments switch new Foo() │
101
+ │ == != instanceof undefined │
102
+ │ │
103
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
104
+ │ │ JZ │ │
105
+ │ │ let/const => ...xs destructuring import/export `${}` │ │
106
+ │ │ if/else for/while/do-while/of/in break/continue │ │
107
+ │ │ try/catch/finally throw │ │
108
+ │ │ operators strings booleans numbers arrays objects null │ │
109
+ │ │ Math Number String Array Object JSON RegExp Symbol │ │
110
+ │ │ ArrayBuffer DataView TypedArray Map Set │ │
111
+ │ │ console setTimeout/setInterval Date performance │ │
112
+ │ └────────────────────────────────────────────────────────────────────┘ │
113
+ └────────────────────────────────────────────────────────────────────────┘
114
+
115
+ Not supported
116
+ async/await Promise function* yield
117
+ this class super extends delete labels
118
+ eval Function with Proxy Reflect WeakMap WeakSet
119
+ dynamic import DOM fetch Intl Node APIs
120
+ ```
121
+
122
+
123
+ ## Benchmark
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
143
+
144
+ ### How to pass data between JS and WASM?
145
+
146
+ Numbers pass directly as f64. Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
147
+
148
+ ```js
149
+ const { exports, memory } = jz(\`
150
+ export let greet = (s) => s.length
151
+ export let sum = (a) => a.reduce((s, x) => s + x, 0)
152
+ export let dist = (p) => (p.x * p.x + p.y * p.y) ** 0.5
153
+ export let process = (buf) => buf.map(x => x * 2)
154
+ \`)
155
+
156
+ // JS → WASM (write)
157
+ memory.String('hello') // → string pointer
158
+ memory.Array([1, 2, 3]) // → array pointer
159
+ memory.Float64Array([1.0, 2.0]) // → typed array pointer
160
+ memory.Int32Array([10, 20, 30]) // all typed array constructors available
161
+
162
+ // Objects: keys and order must match the jz source declaration.
163
+ // jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
164
+ memory.Object({ x: 3, y: 4 }) // → object pointer
165
+
166
+ // Strings/arrays inside objects are auto-wrapped to pointers:
167
+ memory.Object({ name: 'jz', count: 3 }) // name auto-wrapped via memory.String
168
+
169
+ // Call with pointers
170
+ exports.greet(memory.String('hello')) // 5
171
+ exports.sum(memory.Array([1, 2, 3])) // 6
172
+ exports.dist(memory.Object({ x: 3, y: 4 })) // 5
173
+
174
+ // WASM → JS (read)
175
+ memory.read(exports.process(memory.Float64Array([1, 2, 3]))) // Float64Array [2, 4, 6]
176
+ ```
177
+
178
+ Template interpolation handles most of this automatically — strings, arrays, numbers, and numeric objects are marshaled for you:
179
+
180
+ ```js
181
+ jz\`export let f = () => ${'hello'}.length + ${[1,2,3]}[0] + ${{x: 5, y: 10}}.x\`
182
+ ```
183
+
184
+ <!--
185
+ ### How does everything fit in f64?
186
+
187
+ 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]`.
188
+
189
+ | Type | Code | Payload | Example |
190
+ |------|------|---------|---------|
191
+ | Number | — | regular f64 | `3.14`, `42`, `NaN` |
192
+ | Null | 0 | reserved pattern | `null` (distinct from `0` and `NaN`) |
193
+ | Array | 1 | aux=length, offset=heap | `[1, 2, 3]` |
194
+ | ArrayBuffer | 2 | offset=heap | `new ArrayBuffer(16)` |
195
+ | TypedArray | 3 | aux=elemType, offset=heap | `new Float64Array(n)` |
196
+ | String | 4 | offset=heap | `"hello world"` (>4 chars) |
197
+ | SSO String | 5 | aux=packed chars | `"hi"` (<=4 ASCII chars, zero alloc) |
198
+ | Object | 6 | aux=schemaId, offset=heap | `{x: 1, y: 2}` |
199
+ | Hash | 7 | offset=heap | dynamic string-keyed objects |
200
+ | Set | 8 | offset=heap | `new Set()` |
201
+ | Map | 9 | offset=heap | `new Map()` |
202
+ | Closure | 10 | aux=funcIdx, offset=env | `x => x + captured` |
203
+ | External | 11 | offset=hostMap index | JS host object references |
204
+
205
+ **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.
206
+
207
+ **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.
208
+
209
+ **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
+ -->
211
+
212
+ ### How does template interpolation work?
213
+
214
+ 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
+
216
+ ```js
217
+ jz`export let f = () => ${'hello'}.length` // 5 — string compiled as literal
218
+ jz`export let f = () => ${[10, 20, 30]}[1]` // 20 — array compiled as literal
219
+ jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3 — object compiled as literal
220
+
221
+ // Nested values work too
222
+ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
223
+ ```
224
+
225
+ Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
226
+
227
+ ### Does it support imports?
228
+
229
+ Yes — standard ES `import` syntax, bundled at compile-time into one WASM.
230
+
231
+ ```js
232
+ // modules: jz source bundled at compile time
233
+ const { exports } = jz(
234
+ 'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
235
+ { modules: { './math.jz': 'export let add = (a, b) => a + b' } }
236
+ )
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
+ ```
244
+
245
+ Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
246
+
247
+ 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:
248
+
249
+ ```js
250
+ // Pass the entire Math namespace — sin, cos, sqrt, PI, etc. auto-wired
251
+ const { exports } = jz(
252
+ 'import { sin, PI } from "math"; export let f = () => sin(PI / 2)',
253
+ { imports: { math: Math } }
254
+ )
255
+
256
+ // Pass Date static methods
257
+ const { exports } = jz(
258
+ 'import { now } from "date"; export let f = () => now()',
259
+ { imports: { date: Date } }
260
+ )
261
+
262
+ // Pass window / globalThis
263
+ const { exports } = jz(
264
+ 'import { parseInt } from "window"; export let f = () => parseInt("42")',
265
+ { imports: { window: globalThis } }
266
+ )
267
+ ```
268
+
269
+ **CLI** resolves filesystem imports automatically.
270
+
271
+ ```sh
272
+ jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
273
+ ```
274
+
275
+ **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
276
+
277
+ ```js
278
+ // Transitive bundling — all merged into one WASM
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
+ ```
285
+
286
+ ### Can two modules share data?
287
+
288
+ 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
+
290
+ ```js
291
+ const memory = jz.memory()
292
+
293
+ const a = jz('export let make = () => { let o = {x: 10, y: 20}; return o }', { memory })
294
+ const b = jz('export let read = (o) => o.x + o.y', { memory })
295
+
296
+ // Object from module a, processed by module b — same memory, merged schemas
297
+ b.exports.read(a.exports.make()) // 30
298
+
299
+ // Read from JS too — memory knows all schemas
300
+ memory.read(a.exports.make()) // {x: 10, y: 20}
301
+
302
+ // Write from JS before any compilation
303
+ memory.String('hello') // → NaN-boxed pointer
304
+ memory.Array([1, 2, 3]) // → NaN-boxed pointer
305
+ ```
306
+
307
+ `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
+
309
+ 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.
310
+
311
+
312
+ ### How do I run compiled WASM outside the browser?
313
+
314
+ ```sh
315
+ jz program.js -o program.wasm
316
+
317
+ # Run with any WASM runtime
318
+ wasmtime program.wasm # WASI support built in
319
+ wasmer run program.wasm
320
+ deno run program.wasm
321
+ ```
322
+
323
+ Pure numeric modules have no imports and instantiate with standard
324
+ `WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS
325
+ hosts such as EdgeJS. Compile once at startup or build time, then reuse the
326
+ module; do not compile JZ source per request.
327
+
328
+ `console.log` compiles to WASI `fd_write` by default — works natively on
329
+ wasmtime/wasmer/deno without polyfills. In JS hosts, `jz()` auto-applies the
330
+ small `jz/wasi` polyfill; pass `{ write(fd, text) { ... } }` to capture or route
331
+ stdout/stderr without depending on `process.stdout`.
332
+
333
+
334
+ ### What host features are supported?
335
+
336
+ The compiled `.wasm` uses one import namespace:
337
+
338
+ - none — pure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
339
+ - `wasi_snapshot_preview1` — standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node/EdgeJS-like hosts, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
340
+
341
+ | JS API | Maps to | Notes |
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 |
349
+
350
+ ### Can I compile jz to C?
351
+
352
+ Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
353
+
354
+ ```sh
355
+ jz program.js -o program.wasm
356
+ wasm2c program.wasm -o program.c
357
+ cc program.c -o program
358
+ ```
359
+
360
+
361
+ ## Alternatives
362
+
363
+ * [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.
364
+ * [assemblyscript](https://github.com/AssemblyScript/assemblyscript) — TypeScript-subset compiling to WASM — small, performant output, but requires type annotations.
365
+ * [jawsm](https://github.com/drogus/jawsm) — JS→WASM compiler in Rust. Compiles standard JS with a runtime that provides GC and closures in WASM.
366
+
367
+ ## Build with
368
+
369
+ * [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.
370
+ * [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.
371
+
372
+
373
+ <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
+ })