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/index.js ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * jz - JS subset → WASM compiler.
3
+ *
4
+ * # Pipeline stages + contracts
5
+ *
6
+ * source (string)
7
+ * ↓ parse (subscript/jessie) — lexing + expression-oriented AST
8
+ * raw AST: nested arrays `[op, ...args]`, no ctx mutation
9
+ * ↓ jzify (opt-in via opts.jzify) — lower full-JS subset (var/function/class/switch) to jz-native
10
+ * desugared AST: arrow functions + let/const/if only
11
+ * ↓ prepare — validate (reject disallowed ops), normalize (++/--→+=/-=, scope rename),
12
+ * extract (functions→ctx.func.list with sig), resolve (imports→ctx.module.imports),
13
+ * track (object-literal schemas via ctx.schema.register)
14
+ * prepared AST: normalized, with `ctx.func.list` / `ctx.module.imports` / `ctx.schema.list`
15
+ * populated. Arrow bodies carry no type info yet.
16
+ * ↓ compile — drives per-function emit, interleaves analysis (locals/valTypes/captures/
17
+ * narrowing fixpoint) with IR generation via the emitter table (src/emit.js).
18
+ * Writes: `ctx.func.valTypes`/`.locals`, `ctx.types.*`, `ctx.runtime.*`, `ctx.core.includes`.
19
+ * Also calls optimizeFunc (src/optimize.js): `hoistPtrType` + fused peephole/inline/memarg walk.
20
+ * WAT IR: watr S-expression `['module', ...sections]`, every instruction node carries `.type`.
21
+ * ↓ watrOptimize (opt-out via opts.optimize=false) — CSE, DCE, const folding at WAT level
22
+ * ↓ optimizeFunc 2nd pass — re-folds rebox/unbox roundtrips that watrOptimize's inliner
23
+ * re-introduces at inline boundaries (caller's boxPtrIR meets callee's
24
+ * i32.wrap_i64(i64.reinterpret_f64 __env)). watr's peephole doesn't cover this.
25
+ * ↓ watrPrint (opts.wat=true) → WAT text, or watrCompile → Uint8Array binary
26
+ *
27
+ * # State
28
+ * Single shared `ctx` (src/ctx.js). Reset at compile() entry via `reset(emitter, GLOBALS)`.
29
+ * Each subkey has a declared lifecycle + ownership — see ctx.js docstring for the table.
30
+ *
31
+ * # Extension
32
+ * Modules in module/ register operator handlers on ctx.core.emit and stdlibs on ctx.core.stdlib.
33
+ * Feature flags (ctx.features.*) gate conditional stdlib branches for dead-code elimination.
34
+ * Capability hooks (ctx.schema.register, ctx.closure.make) are installed by capability modules.
35
+ *
36
+ * Interop host layer (memory marshaling, wrap, instantiate) lives in src/host.js.
37
+ *
38
+ * @module jz
39
+ */
40
+
41
+ import { parse } from 'subscript/jessie'
42
+ import { compile as watrCompile, print as watrPrint, optimize as watrOptimize } from "watr";
43
+ import { ctx, reset } from './src/ctx.js'
44
+ import prepare, { GLOBALS } from './src/prepare.js'
45
+ import compile from './src/compile.js'
46
+ import { emitter } from './src/emit.js'
47
+ import { optimizeFunc, resolveOptimize } from './src/optimize.js'
48
+ import jzify from './src/jzify.js'
49
+ import { normalizeSource } from './src/source.js'
50
+ import {
51
+ memory as enhanceMemory, instantiate as instantiateRuntime,
52
+ } from './src/host.js'
53
+
54
+ const importSpecMayReturnExternal = (spec) => {
55
+ if (typeof spec === 'function') return true
56
+ return false
57
+ }
58
+
59
+ const importsMayReturnExternal = (imports) => {
60
+ if (!imports) return false
61
+ for (const mod of Object.values(imports))
62
+ for (const spec of Object.values(mod || {}))
63
+ if (importSpecMayReturnExternal(spec)) return true
64
+ return false
65
+ }
66
+
67
+ const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
68
+
69
+ const compileProfiler = (profile) => {
70
+ if (!profile) return null
71
+ profile.entries ||= []
72
+ profile.totals ||= {}
73
+ return {
74
+ time(name, fn) {
75
+ const start = nowMs()
76
+ try { return fn() }
77
+ finally {
78
+ const ms = nowMs() - start
79
+ profile.entries.push({ name, ms })
80
+ profile.totals[name] = (profile.totals[name] || 0) + ms
81
+ }
82
+ },
83
+ }
84
+ }
85
+
86
+ /**
87
+ * jz — JS subset → WASM compiler.
88
+ *
89
+ * jz('code') or jz`code` → { exports, memory, instance, module }
90
+ * jz.compile('code') → Uint8Array (raw WASM binary)
91
+ * jz.compile('code', { wat: true }) → string (WAT text)
92
+ * jz.memory([src]) → enhanced WebAssembly.Memory (read/write JS↔WASM values)
93
+ *
94
+ * @example
95
+ * const { exports: { add } } = jz('export let add = (a, b) => a + b')
96
+ * add(2, 3) // 5
97
+ */
98
+ jz.memory = enhanceMemory
99
+
100
+ /**
101
+ * Compile jz source to WASM binary or WAT text. Low-level — no instantiation.
102
+ * @param {string} code - jz source
103
+ * @param {object} [opts]
104
+ * @param {boolean} [opts.wat] - Return WAT text instead of binary
105
+ * @param {boolean} [opts.strict] - Reject dynamic features (obj[k], for-in, unknown
106
+ * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
107
+ * into output; large size win for static programs.
108
+ * @param {boolean} [opts.runtimeExports=true] - Export runtime allocator helpers
109
+ * (`_alloc`, `_reset`) for JS memory wrapping. Set false for standalone host-run
110
+ * modules that only call exported wasm functions.
111
+ * @param {boolean|number|object} [opts.optimize] - Optimization level/config.
112
+ * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
113
+ * - `1`: encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
114
+ * - `true` / `2` (default): all current passes (watr CSE/DCE/inline + every jz pass).
115
+ * - `3`: reserved for future aggressive passes (currently == 2).
116
+ * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
117
+ * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
118
+ * @param {object} [opts.profile] - Optional mutable profile sink populated with
119
+ * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
120
+ * @returns {Uint8Array|string}
121
+ */
122
+ jz.compile = (code, opts = {}) => {
123
+ const profiler = compileProfiler(opts.profile)
124
+ const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
125
+
126
+ reset(emitter, GLOBALS)
127
+ ctx.error.src = code
128
+
129
+ if (opts.memory) ctx.memory.shared = true
130
+ if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
131
+ if (opts.modules) ctx.module.importSources = opts.modules
132
+ if (opts.imports) {
133
+ ctx.module.hostImports = opts.imports
134
+ if (importsMayReturnExternal(opts.imports)) ctx.features.external = true
135
+ }
136
+ // jzify: true → accept full JS subset (function/var/switch lowered to arrows/let/if).
137
+ // Default: strict jz (prepare rejects disallowed JS features). subscript handles ASI natively.
138
+ if (opts.jzify) ctx.transform.jzify = jzify
139
+ if (opts.noTailCall) ctx.transform.noTailCall = true
140
+ if (opts.strict) ctx.transform.strict = true
141
+ if (opts.runtimeExports === false) ctx.transform.runtimeExports = false
142
+ if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
143
+ ctx.transform.optimize = resolveOptimize(opts.optimize)
144
+
145
+ if (opts._interp) {
146
+ for (const [name, fn] of Object.entries(opts._interp)) {
147
+ if (name.startsWith('__ext_')) continue;
148
+ ctx.features.external = true
149
+ const params = Array(fn.length).fill(['param', 'f64'])
150
+ ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
151
+ }
152
+ }
153
+
154
+ let parsed = time('parse', () => parse(normalizeSource(code)))
155
+ if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
156
+ const ast = time('prepare', () => prepare(parsed))
157
+ const module = time('compile', () => compile(ast, profiler))
158
+
159
+ const cfg = ctx.transform.optimize
160
+ const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module)) : module
161
+ // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
162
+ // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
163
+ // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
164
+ // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
165
+ if (cfg.watr) {
166
+ time('watrReopt', () => {
167
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
168
+ })
169
+ }
170
+ return opts.wat
171
+ ? time('watrPrint', () => watrPrint(optimized))
172
+ : time('watrCompile', () => watrCompile(optimized))
173
+ }
174
+
175
+ /**
176
+ * Compile, instantiate, and wrap. Works as both jz('code') and jz`code ${val}`.
177
+ * @param {string|TemplateStringsArray} code
178
+ * @param {...any} args - Interpolation values (template tag) or options (string call)
179
+ * @returns {{exports, memory, instance, module}}
180
+ */
181
+ export default function jz(code, ...args) {
182
+ // Template tag: jz`code ${val}` — numbers, functions, strings, arrays, objects
183
+ if (Array.isArray(code)) {
184
+ const interp = {}, data = {}, hoisted = []
185
+
186
+ // Serialize JS value to jz source literal. Returns null if not serializable.
187
+ const serialize = (v) => {
188
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v)
189
+ if (v === null) return 'null'
190
+ if (typeof v === 'string') return JSON.stringify(v)
191
+ if (Array.isArray(v)) {
192
+ const elems = v.map(serialize)
193
+ return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
194
+ }
195
+ if (typeof v === 'object') {
196
+ const props = Object.keys(v).map(k => {
197
+ const s = serialize(v[k])
198
+ return s !== null ? `${k}: ${s}` : null
199
+ })
200
+ return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
201
+ }
202
+ return null
203
+ }
204
+
205
+ let src = code[0]
206
+ for (let i = 0; i < args.length; i++) {
207
+ const v = args[i]
208
+ if (typeof v === 'function') {
209
+ const key = `$$${i}`; interp[key] = v; src += key
210
+ } else {
211
+ const s = serialize(v)
212
+ if (s !== null && (typeof v === 'number' || typeof v === 'boolean')) {
213
+ // Scalars inline directly
214
+ src += s
215
+ } else if (s !== null) {
216
+ // Strings, arrays, objects — hoist as compile-time literal
217
+ const key = `$$${i}`
218
+ hoisted.push(`let ${key} = ${s}`)
219
+ src += key
220
+ } else {
221
+ // Non-serializable (host objects, etc.) — post-instantiation getter
222
+ const key = `$$${i}`, ref = { ptr: 0 }
223
+ data[key] = { val: v, ref }; interp[key] = () => ref.ptr
224
+ src += `${key}()`
225
+ }
226
+ }
227
+ src += code[i + 1]
228
+ }
229
+ if (hoisted.length) src = hoisted.join('; ') + '; ' + src
230
+ const hasInterp = Object.keys(interp).length
231
+ const result = instantiateRuntime(jz.compile, src, { _interp: hasInterp ? interp : null })
232
+ // Patch data getters: allocate values in WASM memory, update closure refs
233
+ for (const [, { val, ref }] of Object.entries(data)) {
234
+ if (typeof val === 'string') ref.ptr = result.memory.String(val)
235
+ else if (Array.isArray(val)) ref.ptr = result.memory.Array(val)
236
+ else ref.ptr = result.memory.Object(val)
237
+ }
238
+ return result
239
+ }
240
+
241
+ // String call: jz('code', opts?) — compile + instantiate + wrap
242
+ return instantiateRuntime(jz.compile, code, args[0] || {})
243
+ }
244
+
245
+ export { jz }
246
+ const jzCompile = jz.compile
247
+ export { jzCompile as compile }