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/index.js ADDED
@@ -0,0 +1,217 @@
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, { emitter } from './src/compile.js'
46
+ import { optimizeFunc, resolveOptimize } from './src/optimize.js'
47
+ import jzify from './src/jzify.js'
48
+ import {
49
+ memory as enhanceMemory, instantiate as instantiateRuntime,
50
+ } from './src/host.js'
51
+
52
+ const importSpecMayReturnExternal = (spec) => {
53
+ if (typeof spec === 'function') return true
54
+ return false
55
+ }
56
+
57
+ const importsMayReturnExternal = (imports) => {
58
+ if (!imports) return false
59
+ for (const mod of Object.values(imports))
60
+ for (const spec of Object.values(mod || {}))
61
+ if (importSpecMayReturnExternal(spec)) return true
62
+ return false
63
+ }
64
+
65
+ /**
66
+ * jz — JS subset → WASM compiler.
67
+ *
68
+ * jz('code') or jz`code` → { exports, memory, instance, module }
69
+ * jz.compile('code') → Uint8Array (raw WASM binary)
70
+ * jz.compile('code', { wat: true }) → string (WAT text)
71
+ * jz.memory([src]) → enhanced WebAssembly.Memory (read/write JS↔WASM values)
72
+ *
73
+ * @example
74
+ * const { exports: { add } } = jz('export let add = (a, b) => a + b')
75
+ * add(2, 3) // 5
76
+ */
77
+ jz.memory = enhanceMemory
78
+
79
+ /**
80
+ * Compile jz source to WASM binary or WAT text. Low-level — no instantiation.
81
+ * @param {string} code - jz source
82
+ * @param {object} [opts]
83
+ * @param {boolean} [opts.wat] - Return WAT text instead of binary
84
+ * @param {boolean} [opts.strict] - Reject dynamic features (obj[k], for-in, unknown
85
+ * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
86
+ * into output; large size win for static programs.
87
+ * @param {boolean} [opts.runtimeExports=true] - Export runtime allocator helpers
88
+ * (`_alloc`, `_reset`) for JS memory wrapping. Set false for standalone host-run
89
+ * modules that only call exported wasm functions.
90
+ * @param {boolean|number|object} [opts.optimize] - Optimization level/config.
91
+ * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
92
+ * - `1`: encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
93
+ * - `true` / `2` (default): all current passes (watr CSE/DCE/inline + every jz pass).
94
+ * - `3`: reserved for future aggressive passes (currently == 2).
95
+ * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
96
+ * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
97
+ * @returns {Uint8Array|string}
98
+ */
99
+ jz.compile = (code, opts = {}) => {
100
+ reset(emitter, GLOBALS)
101
+ ctx.error.src = code
102
+
103
+ if (opts.memory) ctx.memory.shared = true
104
+ if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
105
+ if (opts.modules) ctx.module.importSources = opts.modules
106
+ if (opts.imports) {
107
+ ctx.module.hostImports = opts.imports
108
+ if (importsMayReturnExternal(opts.imports)) ctx.features.external = true
109
+ }
110
+ // jzify: true → accept full JS subset (function/var/switch lowered to arrows/let/if).
111
+ // Default: strict jz (prepare rejects disallowed JS features). subscript handles ASI natively.
112
+ if (opts.jzify) ctx.transform.jzify = jzify
113
+ if (opts.noTailCall) ctx.transform.noTailCall = true
114
+ if (opts.strict) ctx.transform.strict = true
115
+ if (opts.runtimeExports === false) ctx.transform.runtimeExports = false
116
+ if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
117
+ ctx.transform.optimize = resolveOptimize(opts.optimize)
118
+
119
+ if (opts._interp) {
120
+ for (const [name, fn] of Object.entries(opts._interp)) {
121
+ if (name.startsWith('__ext_')) continue;
122
+ ctx.features.external = true
123
+ const params = Array(fn.length).fill(['param', 'f64'])
124
+ ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
125
+ }
126
+ }
127
+
128
+ let parsed = parse(code)
129
+ if (opts.jzify) parsed = jzify(parsed)
130
+ const ast = prepare(parsed)
131
+ const module = compile(ast)
132
+
133
+ const cfg = ctx.transform.optimize
134
+ const optimized = cfg.watr ? watrOptimize(module) : module
135
+ // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
136
+ // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
137
+ // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
138
+ // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
139
+ if (cfg.watr) {
140
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
141
+ }
142
+ return opts.wat ? watrPrint(optimized) : watrCompile(optimized)
143
+ }
144
+
145
+ /**
146
+ * Compile, instantiate, and wrap. Works as both jz('code') and jz`code ${val}`.
147
+ * @param {string|TemplateStringsArray} code
148
+ * @param {...any} args - Interpolation values (template tag) or options (string call)
149
+ * @returns {{exports, memory, instance, module}}
150
+ */
151
+ export default function jz(code, ...args) {
152
+ // Template tag: jz`code ${val}` — numbers, functions, strings, arrays, objects
153
+ if (Array.isArray(code)) {
154
+ const interp = {}, data = {}, hoisted = []
155
+
156
+ // Serialize JS value to jz source literal. Returns null if not serializable.
157
+ const serialize = (v) => {
158
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v)
159
+ if (v === null) return 'null'
160
+ if (typeof v === 'string') return JSON.stringify(v)
161
+ if (Array.isArray(v)) {
162
+ const elems = v.map(serialize)
163
+ return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
164
+ }
165
+ if (typeof v === 'object') {
166
+ const props = Object.keys(v).map(k => {
167
+ const s = serialize(v[k])
168
+ return s !== null ? `${k}: ${s}` : null
169
+ })
170
+ return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
171
+ }
172
+ return null
173
+ }
174
+
175
+ let src = code[0]
176
+ for (let i = 0; i < args.length; i++) {
177
+ const v = args[i]
178
+ if (typeof v === 'function') {
179
+ const key = `$$${i}`; interp[key] = v; src += key
180
+ } else {
181
+ const s = serialize(v)
182
+ if (s !== null && (typeof v === 'number' || typeof v === 'boolean')) {
183
+ // Scalars inline directly
184
+ src += s
185
+ } else if (s !== null) {
186
+ // Strings, arrays, objects — hoist as compile-time literal
187
+ const key = `$$${i}`
188
+ hoisted.push(`let ${key} = ${s}`)
189
+ src += key
190
+ } else {
191
+ // Non-serializable (host objects, etc.) — post-instantiation getter
192
+ const key = `$$${i}`, ref = { ptr: 0 }
193
+ data[key] = { val: v, ref }; interp[key] = () => ref.ptr
194
+ src += `${key}()`
195
+ }
196
+ }
197
+ src += code[i + 1]
198
+ }
199
+ if (hoisted.length) src = hoisted.join('; ') + '; ' + src
200
+ const hasInterp = Object.keys(interp).length
201
+ const result = instantiateRuntime(jz.compile, src, { _interp: hasInterp ? interp : null })
202
+ // Patch data getters: allocate values in WASM memory, update closure refs
203
+ for (const [, { val, ref }] of Object.entries(data)) {
204
+ if (typeof val === 'string') ref.ptr = result.memory.String(val)
205
+ else if (Array.isArray(val)) ref.ptr = result.memory.Array(val)
206
+ else ref.ptr = result.memory.Object(val)
207
+ }
208
+ return result
209
+ }
210
+
211
+ // String call: jz('code', opts?) — compile + instantiate + wrap
212
+ return instantiateRuntime(jz.compile, code, args[0] || {})
213
+ }
214
+
215
+ export { jz }
216
+ const jzCompile = jz.compile
217
+ export { jzCompile as compile }