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/src/prepare.js ADDED
@@ -0,0 +1,1598 @@
1
+ /**
2
+ * AST preparation: single-pass traversal that validates, resolves, and normalizes.
3
+ *
4
+ * # Stage contract
5
+ * IN: raw jessie AST from subscript/jessie (possibly jzified).
6
+ * OUT: normalized AST + populated `ctx.func.list`, `ctx.module.imports`, `ctx.schema.list`,
7
+ * `ctx.scope.consts`, `ctx.module.moduleInits`.
8
+ * POST: no `var`/`function`/`class`/`this` remain; ++/-- rewritten as +=/-=; arrow
9
+ * bodies carry no type metadata yet (that's analyze/compile's job).
10
+ *
11
+ * # Concerns (per-node handler table, applied together per op)
12
+ * 1. Validate — reject prohibited features (this, class, async, var, delete, ...)
13
+ * 2. Resolve — scope chain + import bindings (Math.sin → math.sin, etc.)
14
+ * 3. Extract — arrow functions → ctx.func.list with sig
15
+ * 4. Normalize — ++/-- → +=/-=, unary ± disambiguation, for-head flattening
16
+ * 5. Auto-import — Math/Array/etc usage triggers includeModule(...)
17
+ * 6. Track schemas — object literals, Object.assign inference (inferAssignSchema)
18
+ *
19
+ * Each handler may touch multiple concerns, but helpers keep each concern self-contained.
20
+ * Unhandled ops fall through to recursive prep() of their children.
21
+ *
22
+ * @module prepare
23
+ */
24
+
25
+ import { parse } from 'subscript/jessie'
26
+ import { ctx, err, derive } from './ctx.js'
27
+ import { T, STMT_OPS, VAL, extractParams, collectParamNames, classifyParam } from './analyze.js'
28
+ import * as mods from '../module/index.js'
29
+
30
+ let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
31
+ let scopes = [] // block scope stack: [{names: Set, renames: Map}]
32
+
33
+ const hostReturnValType = spec => {
34
+ if (!spec || typeof spec === 'function') return null
35
+ const ret = spec.returns ?? spec.return ?? spec.result
36
+ if (ret === 'number' || ret === 'f64' || ret === Number) return VAL.NUMBER
37
+ if (ret === 'string' || ret === String) return VAL.STRING
38
+ if (ret === 'bigint' || ret === BigInt) return VAL.BIGINT
39
+ return null
40
+ }
41
+
42
+ const addHostImport = (mod, name, alias, spec) => {
43
+ const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
44
+ const params = Array(nParams).fill(['param', 'f64'])
45
+ if (!ctx.module.imports.some(i => i[3]?.[1] === `$${alias}`)) {
46
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, ['func', `$${alias}`, ...params, ['result', 'f64']]])
47
+ }
48
+ ctx.scope.chain[alias] = alias
49
+ const vt = hostReturnValType(spec)
50
+ if (vt) ctx.module.hostImportValTypes.set(alias, vt)
51
+ }
52
+
53
+ /**
54
+ * @typedef {null|number|string|ASTNode[]} ASTNode
55
+ */
56
+
57
+ /**
58
+ * Prepare AST node for compilation.
59
+ * @param {ASTNode} node - Raw AST from parser
60
+ * @returns {ASTNode} Normalized AST
61
+ */
62
+ export default function prepare(node) {
63
+ depth = 0
64
+ scopes = []
65
+ includeModule('core')
66
+ fuseSparseMapReads(node)
67
+ const ast = prep(node)
68
+ // Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
69
+ // `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
70
+ // module's closure.table machinery. defFunc paths don't trigger fn-module load,
71
+ // so scan post-prep and include `fn` if any user func appears in a value position.
72
+ if (!ctx.module.modules.fn && ctx.func.list.length) {
73
+ const funcNames = new Set(ctx.func.list.map(f => f.name))
74
+ const visit = (n) => {
75
+ if (!Array.isArray(n)) return false
76
+ const [op, ...args] = n
77
+ if (op === '()') {
78
+ // callee at args[0]: skip if it's a bare func name (direct call); recurse rest
79
+ if (typeof args[0] !== 'string' || !funcNames.has(args[0])) {
80
+ if (visit(args[0])) return true
81
+ }
82
+ for (let i = 1; i < args.length; i++) {
83
+ const a = args[i]
84
+ if (typeof a === 'string' && funcNames.has(a)) return true
85
+ if (visit(a)) return true
86
+ }
87
+ return false
88
+ }
89
+ if (op === '.' || op === '?.') {
90
+ // obj at args[0] can be a func ref; prop at args[1] is a name, never a ref
91
+ if (typeof args[0] === 'string' && funcNames.has(args[0])) return true
92
+ return visit(args[0])
93
+ }
94
+ if (op === '=>') {
95
+ // body only — params are bindings, not refs
96
+ return visit(args[1])
97
+ }
98
+ for (const a of args) {
99
+ if (typeof a === 'string' && funcNames.has(a)) return true
100
+ if (visit(a)) return true
101
+ }
102
+ return false
103
+ }
104
+ let needs = visit(ast)
105
+ if (!needs) for (const f of ctx.func.list) if (f.body && visit(f.body)) { needs = true; break }
106
+ if (!needs && ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) if (visit(mi)) { needs = true; break }
107
+ if (needs) includeModule('fn')
108
+ }
109
+
110
+ // Native timers: inline WASM timer queue when referenced (no host imports needed)
111
+ const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'])
112
+ const usedTimers = new Set()
113
+ const scanTimers = (n) => {
114
+ if (!Array.isArray(n)) {
115
+ if (typeof n === 'string' && TIMER_NAMES.has(n)) usedTimers.add(n)
116
+ return
117
+ }
118
+ for (let i = 0; i < n.length; i++) scanTimers(n[i])
119
+ }
120
+ const allNodes = [ast, ...ctx.func.list.map(f => f.body), ...(ctx.module.moduleInits || [])]
121
+ for (const node of allNodes) scanTimers(node)
122
+ if (usedTimers.size) {
123
+ ctx.features.timers = true // Timer module included — compile.js needs to emit __timer_init
124
+ includeModule('timer')
125
+ includeModule('fn') // call_indirect dispatch for callbacks
126
+ }
127
+
128
+ return ast
129
+ }
130
+
131
+ // Named constants → numeric literals
132
+ export const JZ_NULL = Symbol('null')
133
+ const CONSTANTS = { 'true': 1, 'false': 0, 'null': JZ_NULL, 'undefined': JZ_NULL }
134
+ // NaN/Infinity stay as special f64 values in emit()
135
+ const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
136
+
137
+ /** Resolve variable name through block scope chain (innermost rename wins). */
138
+ function resolveScope(name) {
139
+ for (let i = scopes.length - 1; i >= 0; i--)
140
+ if (scopes[i].has(name)) return scopes[i].get(name)
141
+ return name
142
+ }
143
+
144
+ /** Check if name is declared in any current scope level. */
145
+ function isDeclared(name) {
146
+ return scopes.some(s => s.has(name))
147
+ }
148
+
149
+ /** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
150
+ const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6 }
151
+ function resolveTypeof(node) {
152
+ const [op, a, b] = node
153
+ // typeof x == 'string' → type check
154
+ if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
155
+ const code = TYPEOF_MAP[b[1]]
156
+ if (code != null) return [op, ['typeof', a[1]], [, code]]
157
+ }
158
+ // 'string' == typeof x
159
+ if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
160
+ const code = TYPEOF_MAP[a[1]]
161
+ if (code != null) return [op, ['typeof', b[1]], [, code]]
162
+ }
163
+ return node
164
+ }
165
+
166
+ // Property-based narrowing: unambiguous prop names limit module load.
167
+ // Map value is the exact module set for that property (always includes 'core').
168
+ // Properties not in this table fall back to the generic '.' receiver-type heuristic.
169
+ // Uses null-prototype dict so inherited names (constructor, toString) don't match.
170
+ const PROP_MODULES = Object.assign(Object.create(null), {
171
+ // array-only methods
172
+ push: ['core', 'array'], pop: ['core', 'array'], shift: ['core', 'array'], unshift: ['core', 'array'],
173
+ splice: ['core', 'array'], reverse: ['core', 'array'], sort: ['core', 'array'], fill: ['core', 'array'],
174
+ map: ['core', 'array'], filter: ['core', 'array'], reduce: ['core', 'array'], reduceRight: ['core', 'array'],
175
+ forEach: ['core', 'array'], find: ['core', 'array'], findIndex: ['core', 'array'],
176
+ findLast: ['core', 'array'], findLastIndex: ['core', 'array'],
177
+ every: ['core', 'array'], some: ['core', 'array'], flat: ['core', 'array'], flatMap: ['core', 'array'],
178
+ join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'array'],
179
+ // string-only methods
180
+ charAt: ['core', 'string'], charCodeAt: ['core', 'string'], codePointAt: ['core', 'string'],
181
+ toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], trim: ['core', 'string'],
182
+ trimStart: ['core', 'string'], trimEnd: ['core', 'string'],
183
+ split: ['core', 'string'], replace: ['core', 'string'], replaceAll: ['core', 'string'],
184
+ repeat: ['core', 'string'], startsWith: ['core', 'string'], endsWith: ['core', 'string'],
185
+ padStart: ['core', 'string'], padEnd: ['core', 'string'], normalize: ['core', 'string'],
186
+ matchAll: ['core', 'string'], match: ['core', 'string'],
187
+ substring: ['core', 'string'], substr: ['core', 'string'],
188
+ // collection-only methods
189
+ add: ['core', 'collection'], clear: ['core', 'collection'],
190
+ // string + array (cross-type but not object/collection)
191
+ slice: ['core', 'string', 'array'], concat: ['core', 'string', 'array'],
192
+ indexOf: ['core', 'string', 'array'], lastIndexOf: ['core', 'string', 'array'],
193
+ includes: ['core', 'string', 'array'],
194
+ // string + array + typedarray (length shared across all sequence types)
195
+ length: ['core', 'string', 'array', 'typedarray'],
196
+ // Note: toString/toFixed/toPrecision/toExponential not narrowed — they only have
197
+ // number-schema emitters; unknown receivers fall through to __ext_call (collection module).
198
+ })
199
+
200
+ const OP_MODULES = {
201
+ // '.' handled inline (see '.' handler) for property-based narrowing
202
+ '?.': ['core', 'string', 'collection'],
203
+ '?.[]': ['core', 'array', 'collection'],
204
+ '?.()': ['core', 'fn'],
205
+ 'u+': ['number', 'string'],
206
+ 'in': ['core', 'collection', 'string'],
207
+ '==': ['core', 'string'],
208
+ '!=': ['core', 'string'],
209
+ 'typeof': ['core', 'string'],
210
+ '[': ['core', 'array'],
211
+ '{': ['core', 'object', 'string', 'collection'],
212
+ '//': ['core', 'string', 'regex'],
213
+ }
214
+ const dict = obj => Object.assign(Object.create(null), obj)
215
+
216
+ const cloneNode = (node) => {
217
+ if (!Array.isArray(node)) return node
218
+ const copy = node.map(cloneNode)
219
+ if (node.loc != null) copy.loc = node.loc
220
+ return copy
221
+ }
222
+
223
+ // Call-site module inclusion. Keyed by either a bare callee name (e.g. 'parseFloat')
224
+ // or a dotted path ('console.log'). Value is the module set to load.
225
+ // The lookup fires from the '()' handler before scope resolution.
226
+ // Architectural note: fully eliminating this (auto-imports via jzify) was considered but
227
+ // rejected — Object.fromEntries needs collection+string while o.x does not, so MOD_DEPS
228
+ // transitive inclusion alone over-loads simple paths. This table pinpoints load per callee.
229
+ const CALL_MODULES = dict({
230
+ // Bare-identifier calls
231
+ 'ArrayBuffer': ['core', 'typedarray'],
232
+ 'DataView': ['core', 'typedarray'],
233
+ 'BigInt64Array': ['core', 'typedarray'],
234
+ 'BigUint64Array': ['core', 'typedarray'],
235
+ 'parseFloat': ['number', 'string'],
236
+ 'parseInt': ['number', 'string'],
237
+ 'String': ['core', 'string', 'number'],
238
+ 'Number': ['number', 'string'],
239
+ 'Boolean': ['number'],
240
+ 'TextEncoder': ['core', 'string'],
241
+ 'TextDecoder': ['core', 'string'],
242
+ 'Error': ['core', 'string'],
243
+ 'BigInt': ['number'],
244
+ // Namespace.method calls
245
+ 'console.log': ['core', 'string', 'number', 'console'],
246
+ 'console.warn': ['core', 'string', 'number', 'console'],
247
+ 'console.error': ['core', 'string', 'number', 'console'],
248
+ 'Object.fromEntries': ['collection', 'string'],
249
+ 'Object.keys': ['string'],
250
+ 'Object.entries': ['string'],
251
+ 'Date.now': ['core', 'console'],
252
+ 'performance.now': ['core', 'console'],
253
+ 'String.fromCharCode': ['core', 'string'],
254
+ 'String.fromCodePoint': ['core', 'string'],
255
+ 'BigInt.asIntN': ['number'],
256
+ 'BigInt.asUintN': ['number'],
257
+ 'Float64Array.from': ['core', 'typedarray', 'array'],
258
+ 'Float32Array.from': ['core', 'typedarray', 'array'],
259
+ 'Int32Array.from': ['core', 'typedarray', 'array'],
260
+ 'Uint32Array.from': ['core', 'typedarray', 'array'],
261
+ 'Int16Array.from': ['core', 'typedarray', 'array'],
262
+ 'Uint16Array.from': ['core', 'typedarray', 'array'],
263
+ 'Int8Array.from': ['core', 'typedarray', 'array'],
264
+ 'Uint8Array.from': ['core', 'typedarray', 'array'],
265
+ 'ArrayBuffer.isView': ['core', 'typedarray'],
266
+ })
267
+
268
+ // Method-call inclusion for unknown-receiver cases (e.g. `(x) => x.toFixed(2)`).
269
+ // Keyed by bare method name — fires only when receiver can't be resolved to a namespace.
270
+ const GENERIC_METHOD_MODULES = dict({
271
+ 'toString': ['core', 'string', 'number'],
272
+ 'toFixed': ['core', 'string', 'number'],
273
+ 'toPrecision': ['core', 'string', 'number'],
274
+ 'toExponential': ['core', 'string', 'number'],
275
+ })
276
+
277
+ // Constructor names that users may call without `new` — `Float64Array(5)` → `new Float64Array(5)`.
278
+ // Keeps user source ergonomic; these cannot be plain function calls in JS semantics anyway.
279
+ const CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','Set','Map']
280
+
281
+ /** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
282
+ * into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
283
+ * intermediate array. Pre-prep AST mutation; only fires on shapes where every use of `b` is a
284
+ * numeric `b[idx]` read or a `b.length` read, the arrow is pure with a single named param, and
285
+ * `b` is not referenced after the consumer for-loop. Preserves observable behavior because the
286
+ * arrow's pure-expression body has no order-dependent effects. */
287
+ function fuseSparseMapReads(root) {
288
+ walkSparse(root)
289
+ }
290
+ function walkSparse(node) {
291
+ if (!Array.isArray(node)) return
292
+ for (let i = 1; i < node.length; i++) walkSparse(node[i])
293
+ if (node[0] === ';') tryFuseInBlock(node)
294
+ }
295
+ function tryFuseInBlock(seq) {
296
+ for (let i = 1; i < seq.length - 1; i++) {
297
+ const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
298
+ if (fused) {
299
+ seq.splice(i, 2, ...fused)
300
+ i-- // re-examine same position (chained fusions)
301
+ }
302
+ }
303
+ }
304
+ function tryFusePair(decl, forNode, seq, declIdx) {
305
+ if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
306
+ if (decl.length !== 2) return null // single binding only
307
+ const bind = decl[1]
308
+ if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
309
+ const NAME = bind[1], rhs = bind[2]
310
+ if (!Array.isArray(rhs) || rhs[0] !== '()') return null
311
+ const callee = rhs[1]
312
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
313
+ const RECV = callee[1]
314
+ if (typeof RECV !== 'string' || RECV === NAME) return null
315
+ const arrow = rhs[2]
316
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
317
+ // Single-name param only: `x => …` or `(x) => …`
318
+ const ap = arrow[1]
319
+ const PARAM = typeof ap === 'string' ? ap :
320
+ (Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
321
+ if (!PARAM || PARAM === NAME || PARAM === RECV) return null
322
+ // Body: single-expression arrow only (block bodies skipped — could extend later).
323
+ const aBody = arrow[2]
324
+ if (Array.isArray(aBody) && aBody[0] === '{}') return null
325
+ if (!isPureSparseArrowBody(aBody, PARAM)) return null
326
+ // For-loop: ['for', [';', initStmt, cond, inc], body]
327
+ if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
328
+ const head = forNode[1]
329
+ if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
330
+ const cond = head[2], forBody = forNode[2]
331
+ // Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
332
+ if (!hasOnlySparseUses(cond, NAME)) return null
333
+ if (!hasOnlySparseUses(forBody, NAME)) return null
334
+ if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
335
+ // `NAME` must not be read after the for-loop in the same block.
336
+ for (let k = declIdx + 2; k < seq.length; k++) {
337
+ if (refsName(seq[k], NAME)) return null
338
+ }
339
+ // RECV must not be reassigned inside the for-loop (would invalidate substitution).
340
+ if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
341
+ // PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
342
+ if (bindsName(forNode, PARAM)) return null
343
+ // Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
344
+ const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
345
+ const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
346
+ const newHead = [';', head[1], newCond, head[3]]
347
+ return [['for', newHead, newBody]]
348
+ }
349
+ function isPureSparseArrowBody(n, PARAM) {
350
+ if (typeof n === 'string') return true
351
+ if (!Array.isArray(n)) return true
352
+ const op = n[0]
353
+ // Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
354
+ if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
355
+ if (op === '=>') return false // nested closure is opaque
356
+ if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
357
+ if (op === '=') return false
358
+ for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
359
+ return true
360
+ }
361
+ function hasOnlySparseUses(n, NAME) {
362
+ if (typeof n === 'string') return n !== NAME
363
+ if (!Array.isArray(n)) return true
364
+ const op = n[0]
365
+ if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
366
+ if (op === '.' && n[1] === NAME) {
367
+ if (n[2] === 'length') return true
368
+ return false // any other property access on NAME is opaque
369
+ }
370
+ for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
371
+ return true
372
+ }
373
+ function hasAnyIndexedRead(n, NAME) {
374
+ if (!Array.isArray(n)) return false
375
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
376
+ for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
377
+ return false
378
+ }
379
+ function refsName(n, NAME) {
380
+ if (typeof n === 'string') return n === NAME
381
+ if (!Array.isArray(n)) return false
382
+ for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
383
+ return false
384
+ }
385
+ function assignsName(n, NAME) {
386
+ if (!Array.isArray(n)) return false
387
+ const op = n[0]
388
+ if ((op === '=' || op === '++' || op === '--' ||
389
+ (typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
390
+ && n[1] === NAME) return true
391
+ for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
392
+ return false
393
+ }
394
+ function bindsName(n, NAME) {
395
+ if (!Array.isArray(n)) return false
396
+ const op = n[0]
397
+ if ((op === 'let' || op === 'const')) {
398
+ for (let i = 1; i < n.length; i++) {
399
+ const bind = n[i]
400
+ if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
401
+ }
402
+ }
403
+ if (op === '=>') {
404
+ const p = n[1]
405
+ if (p === NAME) return true
406
+ if (Array.isArray(p)) {
407
+ if (p[0] === '()' && p[1] === NAME) return true
408
+ // skip deeper destructuring forms — conservative
409
+ }
410
+ }
411
+ for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
412
+ return false
413
+ }
414
+ function substSparse(n, NAME, RECV, PARAM, arrowBody) {
415
+ if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
416
+ if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
417
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
418
+ const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
419
+ return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
420
+ }
421
+ return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
422
+ }
423
+ function cloneAndBind(node, PARAM, replacement) {
424
+ if (node === PARAM) return replacement
425
+ if (!Array.isArray(node)) return node
426
+ return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
427
+ }
428
+
429
+ function prep(node) {
430
+ if (Array.isArray(node) && OP_MODULES[node[0]]) includeMods(...OP_MODULES[node[0]])
431
+ if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
432
+ if (node == null) return [, 0] // null/undefined → 0 literal
433
+ if (node === true) return [, 1]
434
+ if (node === false) return [, 0]
435
+ if (!Array.isArray(node)) {
436
+ if (typeof node === 'string') {
437
+ if (node in CONSTANTS) return [, CONSTANTS[node]]
438
+ if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
439
+ if (PROHIBITED[node]) err(PROHIBITED[node])
440
+ // Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
441
+ if (node === 'Boolean' || node === 'Number') { includeMods('core', 'fn'); return ['=>', 'x', 'x'] }
442
+ const resolved = ctx.scope.chain[node]
443
+ if (resolved?.includes('.')) return resolved
444
+ // Cross-module import: mangled name (e.g. __util_js$clone)
445
+ if (resolved && resolved !== node) return resolved
446
+ // Block scope: resolve renames
447
+ if (scopes.length) return resolveScope(node)
448
+ }
449
+ return node
450
+ }
451
+
452
+ const [op, ...args] = node
453
+ if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
454
+ if (op == null) {
455
+ if (typeof args[0] === 'string') {
456
+ includeMods('core', 'string', 'number')
457
+ return ['str', args[0]] // string literal
458
+ }
459
+ return [, args[0]] // number literal
460
+ }
461
+ const handler = handlers[op]
462
+ return handler ? handler(...args) : [op, ...args.map(prep)]
463
+ }
464
+
465
+ // FIXME: can we jzify some of these?
466
+ const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
467
+ 'this': '`this` not supported: use explicit parameter',
468
+ 'super': '`super` not supported: no class inheritance',
469
+ 'arguments': '`arguments` not supported: use rest params',
470
+ 'eval': '`eval` not supported'
471
+ }
472
+
473
+ // Predefined globals seeded into scope.chain at ctx.reset(). Value is the scope alias
474
+ // used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
475
+ // resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
476
+ // Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
477
+ // program (they do not live in any module). jzify auto-injecting imports would still
478
+ // need a list of these names to know what to emit, so the table lives here either way.
479
+ export const GLOBALS = {
480
+ Math: 'math',
481
+ Number: 'Number',
482
+ Array: 'Array',
483
+ Object: 'Object',
484
+ Symbol: 'Symbol',
485
+ JSON: 'JSON',
486
+ isNaN: 'number',
487
+ isFinite: 'number',
488
+ parseInt: 'number',
489
+ parseFloat: 'number',
490
+ Error: 'Error',
491
+ BigInt: 'BigInt',
492
+ TextEncoder: 'TextEncoder',
493
+ TextDecoder: 'TextDecoder',
494
+ }
495
+
496
+ const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
497
+ const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
498
+
499
+ function pushPatternAssign(target, valueExpr, out, decls = null) {
500
+ if (Array.isArray(target) && target[0] === '=') {
501
+ pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
502
+ return
503
+ }
504
+
505
+ if (isDestructPattern(target)) {
506
+ const tmp = `${T}d${ctx.func.uniq++}`
507
+ if (decls) decls.push(['=', tmp, valueExpr])
508
+ else out.push(['=', tmp, valueExpr])
509
+ expandDestruct(target, tmp, out, decls)
510
+ return
511
+ }
512
+
513
+ out.push(['=', target, valueExpr])
514
+ }
515
+
516
+ function expandDestruct(pattern, source, out, decls = null) {
517
+ if (!isDestructPattern(pattern)) return
518
+
519
+ if (pattern[0] === '[]') {
520
+ includeMods('core', 'array', 'collection')
521
+ const items = patternItems(pattern[1])
522
+ for (let j = 0; j < items.length; j++) {
523
+ const item = items[j]
524
+ if (item == null) continue
525
+
526
+ if (Array.isArray(item) && item[0] === '...') {
527
+ pushPatternAssign(item[1], ['()', ['.', source, 'slice'], [, j]], out, decls)
528
+ continue
529
+ }
530
+
531
+ pushPatternAssign(item, ['[]', source, [, j]], out, decls)
532
+ }
533
+ return
534
+ }
535
+
536
+ includeMods('core', 'object', 'string', 'collection')
537
+ const items = patternItems(pattern[1])
538
+
539
+ // Collect explicit keys and detect rest pattern
540
+ let restTarget = null
541
+ const explicitKeys = []
542
+ for (const item of items) {
543
+ if (item == null) continue
544
+ if (Array.isArray(item) && item[0] === '...') { restTarget = item[1]; continue }
545
+ if (typeof item === 'string') explicitKeys.push(item)
546
+ else if (Array.isArray(item) && item[0] === '=') { if (typeof item[1] === 'string') explicitKeys.push(item[1]) }
547
+ else if (Array.isArray(item) && item[0] === ':') explicitKeys.push(item[1])
548
+ }
549
+
550
+ for (const item of items) {
551
+ if (item == null) continue
552
+ if (Array.isArray(item) && item[0] === '...') continue // handled below
553
+
554
+ if (typeof item === 'string') {
555
+ pushPatternAssign(item, ['.', source, item], out, decls)
556
+ continue
557
+ }
558
+
559
+ if (Array.isArray(item) && item[0] === '=') {
560
+ if (typeof item[1] === 'string')
561
+ pushPatternAssign(item[1], ['??', ['.', source, item[1]], prep(item[2])], out, decls)
562
+ continue
563
+ }
564
+
565
+ if (Array.isArray(item) && item[0] === ':') {
566
+ pushPatternAssign(item[2], ['.', source, item[1]], out, decls)
567
+ continue
568
+ }
569
+ }
570
+
571
+ // Object rest: {x, ...rest} = obj → rest = {remaining props from source schema}
572
+ if (restTarget) {
573
+ const srcSchema = typeof source === 'string' && ctx.schema.resolve(source)
574
+ if (srcSchema) {
575
+ const remaining = srcSchema.filter(k => !explicitKeys.includes(k))
576
+ if (remaining.length) {
577
+ const restProps = remaining.map(k => [':', k, ['.', source, k]])
578
+ const restObj = ['{}', remaining.length === 1 ? restProps[0] : [',', ...restProps]]
579
+ // Register schema for the rest variable so property access works
580
+ if (typeof restTarget === 'string') ctx.schema.vars.set(restTarget, ctx.schema.register(remaining))
581
+ pushPatternAssign(restTarget, restObj, out, decls)
582
+ } else {
583
+ pushPatternAssign(restTarget, ['{}'], out, decls)
584
+ }
585
+ } else {
586
+ err('Object rest (...) requires source with known schema — destructure the object before passing to function, or use explicit property access')
587
+ }
588
+ }
589
+ }
590
+
591
+ /** Prepare let/const declaration. */
592
+ function prepDecl(op, ...inits) {
593
+ const rest = []
594
+ for (const i of inits) {
595
+ if (!Array.isArray(i) || i[0] !== '=') { rest.push(i); continue }
596
+ const [, name, init] = i, normed = prep(init)
597
+
598
+ if (isDestructPattern(name)) {
599
+ const tmp = `${T}d${ctx.func.uniq++}`
600
+ rest.push(['=', tmp, normed])
601
+ // Propagate schema to temp so rest destructuring can resolve it
602
+ if (typeof normed === 'string' && ctx.schema.vars.has(normed))
603
+ ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
604
+ else if (Array.isArray(normed) && normed[0] === '{}') {
605
+ const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
606
+ if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
607
+ }
608
+ expandDestruct(name, tmp, rest)
609
+ continue
610
+ }
611
+
612
+ if (!defFunc(name, normed)) {
613
+ let declName = name
614
+ // Block scope: rename if shadowing an outer declaration
615
+ if (typeof name === 'string' && scopes.length > 0 && isDeclared(name)) {
616
+ declName = `${name}${T}${ctx.func.uniq++}`
617
+ scopes[scopes.length - 1].set(name, declName)
618
+ } else if (typeof name === 'string' && scopes.length > 0) {
619
+ scopes[scopes.length - 1].set(name, name)
620
+ }
621
+ // Track const for reassignment checks — only module-scope consts (depth 0)
622
+ if (typeof declName === 'string' && depth === 0) {
623
+ if (ctx.module.currentPrefix) {
624
+ declName = `${ctx.module.currentPrefix}$${declName}`
625
+ ctx.scope.chain[name] = declName
626
+ }
627
+ if (op === 'const') {
628
+ if (!ctx.scope.consts) ctx.scope.consts = new Set()
629
+ ctx.scope.consts.add(declName)
630
+ if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
631
+ (ctx.scope.constStrs ||= new Map()).set(declName, normed[1])
632
+ } else if (op === 'let' && ctx.scope.consts?.has(declName)) {
633
+ ctx.scope.consts.delete(declName)
634
+ ctx.scope.constStrs?.delete(declName)
635
+ }
636
+ }
637
+ // Track object schemas (after prefix so schema is keyed to final name)
638
+ if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
639
+ const props = []
640
+ for (const p of normed.slice(1)) {
641
+ if (Array.isArray(p) && p[0] === ':') props.push(p[1])
642
+ else if (Array.isArray(p) && p[0] === '...') {
643
+ // Merge spread source schema into this object's schema
644
+ const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
645
+ if (srcSchema) for (const n of srcSchema) { if (!props.includes(n)) props.push(n) }
646
+ }
647
+ }
648
+ if (props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
649
+ }
650
+ // Module-scope variable → WASM global (mark as user-declared)
651
+ if (depth === 0 && typeof declName === 'string') {
652
+ if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
653
+ ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
654
+ ctx.scope.userGlobals.add(declName)
655
+ }
656
+ rest.push(['=', declName, normed])
657
+ }
658
+ }
659
+ return rest.length ? [op, ...rest] : null
660
+ }
661
+
662
+ const handlers = {
663
+ // Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
664
+ '...'(expr) {
665
+ // Spread is valid in arrays, calls, and objects - just prep the inner expression
666
+ includeModule('array')
667
+ return ['...', prep(expr)]
668
+ },
669
+
670
+ // Prohibited ops — duplicated from jzify deliberately: .jz source bypasses jzify,
671
+ // so prepare is the actual defense. Messages here fire for both .js and .jz.
672
+ 'async': () => err('async/await not supported: WASM is synchronous'),
673
+ 'await': () => err('async/await not supported: WASM is synchronous'),
674
+ 'class': () => err('class not supported: use object literals'),
675
+ 'yield': () => err('generators not supported: use loops'),
676
+ 'delete': () => err('delete not supported: object shape is fixed'),
677
+ 'in'(key, obj) { return ['in', prep(key), prep(obj)] },
678
+ 'instanceof': () => err('instanceof not supported: use typeof'),
679
+ 'with': () => err('`with` not supported: deprecated'),
680
+ ':': () => err('labeled statements not supported'),
681
+ 'var': () => err('`var` not supported: use let/const'),
682
+ 'function': () => err('`function` not supported: use arrow functions'),
683
+
684
+ // Destructuring assignment: [a, ...b] = expr or {x, y} = expr
685
+ '='(lhs, rhs) {
686
+ // Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
687
+ // Distinguishing from index assignment: destructuring patterns have exactly one payload node.
688
+ if (isDestructPattern(lhs) && lhs.length === 2) {
689
+ const normed = prep(rhs)
690
+ const tmp = `${T}d${ctx.func.uniq++}`
691
+ const decls = [['=', tmp, normed]]
692
+ // Propagate schema to temp so rest destructuring can resolve it
693
+ if (typeof normed === 'string' && ctx.schema.vars.has(normed))
694
+ ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
695
+ const stmts = []
696
+ expandDestruct(lhs, tmp, stmts, decls)
697
+ return prep([';', ['let', ...decls], ...stmts])
698
+ }
699
+ // Parser ambiguity: }[pattern] = rhs mis-parsed as subscript when it's stmt; [pattern] = rhs
700
+ // Detect: ['[]', stmtExpr, commaExpr] with spread in comma → split into stmt + destructuring
701
+ if (Array.isArray(lhs) && lhs[0] === '[]' && lhs.length === 3) {
702
+ const hasSpr = n => Array.isArray(n) && (n[0] === '...' || n.some(hasSpr))
703
+ if (hasSpr(lhs[2])) {
704
+ const preStmt = lhs[1]
705
+ const pattern = ['[]', lhs[2]]
706
+ return prep([';', preStmt, ['=', pattern, rhs]])
707
+ }
708
+ }
709
+ // Function property assignment: fn.prop = arrow → extract as top-level function fn$prop
710
+ if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
711
+ && ctx.func.list.some(f => f.name === lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
712
+ const name = `${lhs[1]}$${lhs[2]}`
713
+ if (defFunc(name, prep(rhs))) return null // extracted as function, no assignment needed
714
+ }
715
+ return ['=', prep(lhs), prep(rhs)]
716
+ },
717
+
718
+ // try/catch/throw
719
+ // Parser produces ['try', body, ['catch', param, handler]?, ['finally', cleanup]?]
720
+ 'try'(body, ...clauses) {
721
+ const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
722
+ const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
723
+ const tryBody = prep(body)
724
+ const caught = catchClause
725
+ ? (() => {
726
+ const [, errName, handler] = catchClause
727
+ return ['catch', tryBody, errName, prep(handler)]
728
+ })()
729
+ : tryBody
730
+ if (finallyClause) return ['finally', caught, prep(finallyClause[1])]
731
+ if (catchClause) {
732
+ const [, errName, handler] = catchClause
733
+ return ['catch', tryBody, errName, prep(handler)]
734
+ }
735
+ return tryBody
736
+ },
737
+ 'throw'(expr) { return ['throw', prep(expr)] },
738
+
739
+ // Template literal: [``, part, ...] → chain of str_concat calls
740
+ // First node is always a string (empty if template starts with ${...}) so concat dispatches correctly.
741
+ '`'(...parts) {
742
+ includeMods('core', 'string', 'number')
743
+ const nodes = parts.map(p =>
744
+ Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
745
+ // Ensure first element is a string so concat chain starts with string dispatch
746
+ if (nodes.length && !(Array.isArray(nodes[0]) && nodes[0][0] === 'str'))
747
+ nodes.unshift(['str', ''])
748
+ return nodes.reduce((acc, n) => ['()', ['.', acc, 'concat'], n])
749
+ },
750
+
751
+ // Tagged template: tag`a${x}b` → tag(['a','b'], x)
752
+ // Parser drops empty string segments; reinsert them to satisfy the strings.length === exprs.length + 1 invariant.
753
+ '``'(tag, ...parts) {
754
+ const strs = [], exprs = []
755
+ let prev = false
756
+ for (const p of parts) {
757
+ const isStr = Array.isArray(p) && p[0] == null && typeof p[1] === 'string'
758
+ if (isStr) { strs.push(p); prev = true }
759
+ else { if (!prev) strs.push([null, '']); exprs.push(p); prev = false }
760
+ }
761
+ if (!prev) strs.push([null, ''])
762
+ const arr = strs.length === 1 ? ['[]', strs[0]] : ['[]', [',', ...strs]]
763
+ const callArgs = exprs.length === 0 ? arr : [',', arr, ...exprs]
764
+ return prep(['()', tag, callArgs])
765
+ },
766
+
767
+ // Import
768
+ 'import'(fromNode) {
769
+ if (!Array.isArray(fromNode) || fromNode[0] !== 'from')
770
+ return err('Dynamic import() not supported')
771
+ return handlers['from'](fromNode[1], fromNode[2])
772
+ },
773
+
774
+ 'from'(specifiers, source) {
775
+ const mod = source?.[1]
776
+ if (!mod || typeof mod !== 'string') return err('Invalid import source')
777
+
778
+ // Host imports override built-ins for named imports
779
+ const hostMod = ctx.module.hostImports?.[mod]
780
+ let remaining = specifiers
781
+ if (hostMod && Array.isArray(specifiers) && specifiers[0] === '{}') {
782
+ const inner = specifiers[1]
783
+ if (inner != null) {
784
+ const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
785
+ const builtinItems = []
786
+ for (const item of items) {
787
+ const name = typeof item === 'string' ? item : item[1]
788
+ const alias = typeof item === 'string' ? item : item[2]
789
+ const spec = hostMod[name]
790
+ if (spec) {
791
+ addHostImport(mod, name, alias, spec)
792
+ } else {
793
+ builtinItems.push(item)
794
+ }
795
+ }
796
+ if (builtinItems.length === 0) return null
797
+ if (!mods[MOD_ALIAS[mod] || mod]) {
798
+ const name = typeof builtinItems[0] === 'string' ? builtinItems[0] : builtinItems[0][1]
799
+ err(`'${name}' not declared in host module '${mod}'`)
800
+ }
801
+ remaining = ['{}', builtinItems.length === 1 ? builtinItems[0] : [',', ...builtinItems]]
802
+ } else {
803
+ return null
804
+ }
805
+ }
806
+
807
+ // Tier 1: Built-in module
808
+ if (mods[MOD_ALIAS[mod] || mod]) {
809
+ includeModule(mod)
810
+ const bind = (name, alias) => {
811
+ const key = mod + '.' + name
812
+ if (!ctx.core.emit[key]) err(`Unknown import: ${name} from '${mod}'`)
813
+ ctx.scope.chain[alias || name] = key
814
+ }
815
+
816
+ if (typeof remaining === 'string') { ctx.scope.chain[remaining] = mod; return null }
817
+ if (Array.isArray(remaining) && remaining[0] === 'as' && remaining[1] === '*') { ctx.scope.chain[remaining[2]] = mod; return null }
818
+
819
+ if (Array.isArray(remaining) && remaining[0] === '{}') {
820
+ const inner = remaining[1]
821
+ if (inner == null) return null
822
+ const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
823
+ for (const item of items)
824
+ if (typeof item === 'string') bind(item)
825
+ else if (Array.isArray(item) && item[0] === 'as') bind(item[1], item[2])
826
+ else err(`Invalid import specifier: ${JSON.stringify(item)}`)
827
+ }
828
+ return null
829
+ }
830
+
831
+ // Tier 2: Source module (bundling)
832
+ if (ctx.module.importSources?.[mod]) {
833
+ const resolved = prepareModule(mod, ctx.module.importSources[mod])
834
+ // Default import: import name from 'mod' → bind to default export
835
+ if (typeof specifiers === 'string') {
836
+ const mangled = resolved.exports.get('default')
837
+ if (!mangled) err(`'${mod}' has no default export`)
838
+ ctx.scope.chain[specifiers] = mangled
839
+ return null
840
+ }
841
+ // Namespace import: import * as X from 'mod' → bind X.prop to mangled names
842
+ if (Array.isArray(specifiers) && specifiers[0] === 'as' && specifiers[1] === '*') {
843
+ const alias = specifiers[2]
844
+ // Store namespace mapping so '.' handler can resolve X.prop → mangled name
845
+ if (!ctx.module.namespaces) ctx.module.namespaces = {}
846
+ ctx.module.namespaces[alias] = resolved.exports
847
+ return null
848
+ }
849
+ // Named imports: import { a, b } from 'mod'
850
+ if (Array.isArray(specifiers) && specifiers[0] === '{}') {
851
+ const inner = specifiers[1]
852
+ if (inner == null) return null
853
+ const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
854
+ for (const item of items) {
855
+ const name = typeof item === 'string' ? item : item[1]
856
+ const alias = typeof item === 'string' ? item : item[2]
857
+ const mangled = resolved.exports.get(name)
858
+ if (!mangled) err(`'${name}' is not exported from '${mod}'`)
859
+ ctx.scope.chain[alias] = mangled
860
+ }
861
+ }
862
+ return null
863
+ }
864
+
865
+ // Tier 3: Host imports (non-built-in modules)
866
+ if (hostMod) {
867
+ if (Array.isArray(specifiers) && specifiers[0] === '{}') {
868
+ const inner = specifiers[1]
869
+ if (inner == null) return null
870
+ const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
871
+ for (const item of items) {
872
+ const name = typeof item === 'string' ? item : item[1]
873
+ const alias = typeof item === 'string' ? item : item[2]
874
+ const spec = hostMod[name]
875
+ if (!spec) err(`'${name}' not declared in host module '${mod}'`)
876
+ addHostImport(mod, name, alias, spec)
877
+ }
878
+ }
879
+ return null
880
+ }
881
+
882
+ err(`Unknown module '${mod}'. Provide it via { modules: { '${mod}': source } } or { imports: { '${mod}': {...} } }`)
883
+ },
884
+
885
+ // === is == in jz (all comparisons are strict). Also handle typeof x === 'type' patterns.
886
+ '==='(a, b) { return prep(resolveTypeof(['==', a, b])) },
887
+ '!=='(a, b) { return prep(resolveTypeof(['!=', a, b])) },
888
+
889
+ // Statements
890
+ ';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
891
+ 'let': (...inits) => prepDecl('let', ...inits),
892
+ 'const': (...inits) => prepDecl('const', ...inits),
893
+
894
+ // Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
895
+ 'if': (cond, then, els) => {
896
+ const c = prep(cond)
897
+ scopes.push(new Map()); const t = prep(then); scopes.pop()
898
+ if (els != null) { scopes.push(new Map()); const e = prep(els); scopes.pop(); return ['if', c, t, e] }
899
+ return ['if', c, t]
900
+ },
901
+ 'while': (cond, body) => {
902
+ const c = prep(cond)
903
+ scopes.push(new Map()); const b = prep(body); scopes.pop()
904
+ return ['while', c, b]
905
+ },
906
+
907
+ 'export': decl => {
908
+ if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
909
+ for (const i of decl.slice(1))
910
+ if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
911
+ ctx.func.exports[i[1]] = true
912
+ // export { name1, name2 as alias } → register named exports
913
+ if (Array.isArray(decl) && decl[0] === '{}') {
914
+ const inner = decl[1]
915
+ if (inner == null) return null
916
+ const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
917
+ for (const item of items) {
918
+ if (typeof item === 'string') {
919
+ const resolved = ctx.scope.chain[item]
920
+ ctx.func.exports[item] = (resolved && resolved !== item) ? resolved : item
921
+ } else if (Array.isArray(item) && item[0] === 'as') {
922
+ const [, source, alias] = item
923
+ const resolved = ctx.scope.chain[source]
924
+ ctx.func.exports[alias] = (resolved && resolved !== source) ? resolved : source
925
+ }
926
+ }
927
+ return null
928
+ }
929
+ // export default expr → mark 'default' export, rewrite to assignment
930
+ if (Array.isArray(decl) && decl[0] === 'default') {
931
+ const val = decl[1]
932
+ // export default name → export existing name as 'default'
933
+ if (typeof val === 'string' && (ctx.func.list.some(f => f.name === val) || ctx.scope.globals.has(val))) {
934
+ ctx.func.exports['default'] = val // alias
935
+ return null
936
+ }
937
+ // export default arrow → create function named 'default'
938
+ ctx.func.exports['default'] = true
939
+ if (Array.isArray(val) && val[0] === '=>') {
940
+ if (defFunc('default', prep(val))) return null
941
+ }
942
+ // export default expr → create global 'default'
943
+ ctx.scope.globals.set('default', `(global $default (mut f64) (f64.const 0))`)
944
+ ctx.scope.userGlobals.add('default')
945
+ return ['=', 'default', prep(val)]
946
+ }
947
+ return prep(decl)
948
+ },
949
+
950
+ // Arrow: don't prep params. Track depth for nested function detection.
951
+ '=>': (params, body) => {
952
+ if (depth > 0) { includeMods('core', 'fn') }
953
+ const raw = extractParams(params)
954
+ const fnScope = new Map()
955
+ for (const n of collectParamNames(raw)) fnScope.set(n, n)
956
+
957
+ depth++
958
+ scopes.push(fnScope)
959
+
960
+ const nextParams = []
961
+ const bodyPrefix = []
962
+ for (const r of raw) {
963
+ const c = classifyParam(r)
964
+ if (c.kind === 'rest') {
965
+ nextParams.push(r)
966
+ if (typeof c.name === 'string') fnScope.set(c.name, c.name)
967
+ } else if (c.kind === 'plain') {
968
+ nextParams.push(c.name)
969
+ } else if (c.kind === 'default') {
970
+ nextParams.push(['=', c.name, prep(c.defValue)])
971
+ } else {
972
+ const tmp = `${T}p${ctx.func.uniq++}`
973
+ fnScope.set(tmp, tmp)
974
+ nextParams.push(c.kind === 'destruct-default' ? ['=', tmp, prep(c.defValue)] : tmp)
975
+ bodyPrefix.push(prep(['let', ['=', c.pattern, tmp]]))
976
+ }
977
+ }
978
+ let preparedBody = prep(body)
979
+ if (bodyPrefix.length) {
980
+ const prefix = bodyPrefix.filter(x => x != null)
981
+ if (Array.isArray(preparedBody) && preparedBody[0] === '{}' && Array.isArray(preparedBody[1]) && preparedBody[1][0] === ';')
982
+ preparedBody = ['{}', [';', ...prefix, ...preparedBody[1].slice(1)]]
983
+ else if (Array.isArray(preparedBody) && preparedBody[0] === '{}')
984
+ preparedBody = ['{}', [';', ...prefix, preparedBody[1]]]
985
+ else
986
+ preparedBody = ['{}', [';', ...prefix, ['return', preparedBody]]]
987
+ }
988
+ const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
989
+ const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
990
+ scopes.pop()
991
+ depth--
992
+ return result
993
+ },
994
+
995
+ // Switch: prep discriminant and case values/bodies
996
+ // Parser appends fall-through flag (number) to case bodies — strip it
997
+ 'switch'(discriminant, ...cases) {
998
+ const prepCase = body => {
999
+ if (Array.isArray(body) && body[0] === ';')
1000
+ return prep([';', ...body.slice(1).filter(s => typeof s !== 'number')])
1001
+ return prep(body)
1002
+ }
1003
+ return ['switch', prep(discriminant), ...cases.map(c => {
1004
+ if (c[0] === 'case') return ['case', prep(c[1]), prepCase(c[2])]
1005
+ if (c[0] === 'default') return ['default', prep(c[1])]
1006
+ return prep(c)
1007
+ })]
1008
+ },
1009
+
1010
+ // Optional chaining / typeof — need ptr module
1011
+ '?.'(obj, prop) { return ['?.', prep(obj), prop] },
1012
+ '?.[]'(obj, idx) { return ['?.[]', prep(obj), prep(idx)] },
1013
+ '?.()'(callee, callArgs) {
1014
+ // Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
1015
+ const items = callArgs == null ? []
1016
+ : Array.isArray(callArgs) && callArgs[0] === ',' ? callArgs.slice(1)
1017
+ : [callArgs]
1018
+ return ['?.()', prep(callee), ...items.map(prep)]
1019
+ },
1020
+ // Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
1021
+ 'typeof'(a) {
1022
+ if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeMods('core', 'string'); return ['str', 'boolean'] }
1023
+ return ['typeof', prep(a)]
1024
+ },
1025
+
1026
+ // Unary +/- disambiguation
1027
+ '+'(a, b) {
1028
+ if (b === undefined) {
1029
+ const na = prep(a)
1030
+ if (isLit(na) && typeof na[1] === 'number') return na
1031
+ includeMods('number', 'string')
1032
+ return ['u+', na]
1033
+ }
1034
+ return ['+', prep(a), prep(b)]
1035
+ },
1036
+ '-'(a, b) {
1037
+ if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
1038
+ return ['-', prep(a), prep(b)]
1039
+ },
1040
+
1041
+ // Ternary: parser emits '?' not '?:'
1042
+ '?'(cond, then, els) { return ['?:', prep(cond), prep(then), prep(els)] },
1043
+
1044
+ // ++/-- prefix vs postfix: parser sends trailing null for postfix
1045
+ // Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
1046
+ // Property increment: obj.prop++ → obj.prop = obj.prop + 1
1047
+ '++'(a, _post) {
1048
+ const n = prep(a)
1049
+ if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['+', n, [, 1]]]
1050
+ return _post !== undefined ? ['-', ['++', n], [, 1]] : ['++', n]
1051
+ },
1052
+ '--'(a, _post) {
1053
+ const n = prep(a)
1054
+ if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['-', n, [, 1]]]
1055
+ return _post !== undefined ? ['+', ['--', n], [, 1]] : ['--', n]
1056
+ },
1057
+
1058
+ // Regex literal: ['//','pattern','flags?'] → include regex module, pass through
1059
+ '//'(pattern, flags) {
1060
+ return ['//', pattern, flags]
1061
+ },
1062
+
1063
+ // auto-include math for ** operator
1064
+ '**'(a, b) { includeModule('math'); return ['**', prep(a), prep(b)] },
1065
+
1066
+ // Function call or grouping parens
1067
+ '()'(callee, ...args) {
1068
+ // Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
1069
+ if (args.length === 0) return prep(callee)
1070
+
1071
+ const hasRealArgs = args.some(a => a != null)
1072
+
1073
+ if (typeof callee === 'string') {
1074
+ if (PROHIBITED[callee]) err(PROHIBITED[callee])
1075
+ if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
1076
+
1077
+ const mods = CALL_MODULES[callee]
1078
+ if (mods) {
1079
+ includeMods(...mods)
1080
+ if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
1081
+ return ['()', callee, ...args.filter(a => a != null).map(prep)]
1082
+ }
1083
+ }
1084
+
1085
+ const resolved = ctx.scope.chain[callee]
1086
+ if (resolved?.includes('.')) callee = resolved
1087
+ else if (resolved && ctx.func.list.some(f => f.name === resolved)) callee = resolved
1088
+ else if (resolved && !resolved.includes('.')) {
1089
+ if (!ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1090
+ }
1091
+ else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
1092
+ includeMods('core', 'fn')
1093
+ }
1094
+ } else if (Array.isArray(callee) && callee[0] === '.') {
1095
+ const [, obj, prop] = callee
1096
+ const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
1097
+ if (key && ctx.module.hostImports?.[obj]?.[prop]) {
1098
+ const spec = ctx.module.hostImports[obj][prop]
1099
+ const alias = `${obj}$${prop}`
1100
+ addHostImport(obj, prop, alias, spec)
1101
+ callee = alias
1102
+ } else if (key && CALL_MODULES[key]) {
1103
+ includeMods(...CALL_MODULES[key])
1104
+ callee = key
1105
+ } else if (GENERIC_METHOD_MODULES[prop]) {
1106
+ includeMods(...GENERIC_METHOD_MODULES[prop])
1107
+ callee = prep(callee)
1108
+ } else {
1109
+ const mod = ctx.scope.chain[obj]
1110
+ if (typeof obj === 'string' && mod && !mod.includes('.') && mods[MOD_ALIAS[mod] || mod]) {
1111
+ callee = (includeModule(mod), mod + '.' + prop)
1112
+ } else {
1113
+ callee = prep(callee)
1114
+ }
1115
+ }
1116
+ } else {
1117
+ includeMods('core', 'fn')
1118
+ callee = prep(callee)
1119
+ }
1120
+
1121
+ const preppedArgs = args.filter(a => a != null).map(prep)
1122
+ for (const a of preppedArgs) {
1123
+ if (typeof a === 'string' && ctx.func.list.some(f => f.name === a)) {
1124
+ includeMods('core', 'fn'); break
1125
+ }
1126
+ }
1127
+ const result = ['()', callee, ...preppedArgs]
1128
+
1129
+ if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
1130
+
1131
+ return result
1132
+ },
1133
+
1134
+ // Array literal/indexing — auto-include ptr + array modules
1135
+ '[]'(...args) {
1136
+ if (args.length === 1) {
1137
+ const inner = args[0]
1138
+ includeMods('core', 'array')
1139
+ if (inner == null) return ['[']
1140
+ if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); if (items.length && items[items.length - 1] === null) items.pop(); return ['[', ...items.map(item => item == null ? [, JZ_NULL] : prep(item))] }
1141
+ return ['[', prep(inner)]
1142
+ }
1143
+ if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
1144
+ includeMods('core', 'string')
1145
+ const key = prep(args[1])
1146
+ const exports = [...ctx.module.namespaces[args[0]].entries()]
1147
+ let fallback = [, undefined]
1148
+ for (let i = exports.length - 1; i >= 0; i--) {
1149
+ const [name, resolved] = exports[i]
1150
+ fallback = ['?:', ['==', key, ['str', name]], resolved, fallback]
1151
+ }
1152
+ return fallback
1153
+ }
1154
+ includeMods('core', 'array', 'collection')
1155
+ return ['[]', prep(args[0]), prep(args[1])]
1156
+ },
1157
+
1158
+ // Bare block statement: push scope for let/const shadowing
1159
+ '{'(inner) {
1160
+ scopes.push(new Map())
1161
+ const result = ['{', prep(inner)]
1162
+ scopes.pop()
1163
+ return result
1164
+ },
1165
+
1166
+ // Object literal - flatten comma, expand shorthand
1167
+ '{}'(inner) {
1168
+ // Detect block body vs object literal
1169
+ if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
1170
+ // Block body: push block scope for let/const shadowing
1171
+ scopes.push(new Map())
1172
+ const result = ['{}', prep(inner)]
1173
+ scopes.pop()
1174
+ return result
1175
+ }
1176
+
1177
+ includeMods('core', 'object')
1178
+ if (inner == null) return ['{}']
1179
+ // Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
1180
+ const prop = p => {
1181
+ if (typeof p === 'string') return [':', p, prep(p)]
1182
+ if (Array.isArray(p) && p[0] === ':') return [':', p[1], prep(p[2])]
1183
+ return prep(p)
1184
+ }
1185
+ const result = Array.isArray(inner) && inner[0] === ','
1186
+ ? ['{}', ...inner.slice(1).map(prop)]
1187
+ : ['{}', prop(inner)]
1188
+ // Register schema so property access works for function params (duck typing)
1189
+ const props = result.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
1190
+ if (props.length && ctx.schema.register) ctx.schema.register(props)
1191
+ return result
1192
+ },
1193
+
1194
+ // For loop
1195
+ 'for'(head, body) {
1196
+ scopes.push(new Map())
1197
+ let r
1198
+ if (Array.isArray(head) && head[0] === ';') {
1199
+ let [, init, cond, step] = head
1200
+ // Hoist .length / .size / .byteLength from for-condition:
1201
+ // `i < arr.length` → `let __len = arr.length; ... i < __len`
1202
+ // All three return i32 (TYPED/ARRAY/STRING/BUFFER/SET/MAP), so the hoisted
1203
+ // local stays i32 once exprType narrows it.
1204
+ if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
1205
+ const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
1206
+ if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
1207
+ (lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
1208
+ const lenVar = `${T}len${ctx.func.uniq++}`
1209
+ const lenDecl = ['let', ['=', lenVar, lenExpr]]
1210
+ init = init ? [';', init, lenDecl] : lenDecl
1211
+ if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
1212
+ else cond = [cond[0], lenVar, cond[2]]
1213
+ }
1214
+ }
1215
+ r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
1216
+ } else if (Array.isArray(head) && head[0] === 'of') {
1217
+ // for (let x of arr) → hoist arr (if non-trivial) and arr.length once, iterate by index.
1218
+ // Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
1219
+ // jz philosophy: explicit > implicit; mutation during iteration is a code smell.
1220
+ const [, decl, src] = head
1221
+ const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
1222
+ const idx = `${T}i${ctx.func.uniq++}`
1223
+ const lenVar = `${T}len${ctx.func.uniq++}`
1224
+ const trivial = typeof src === 'string'
1225
+ const arrVar = trivial ? src : `${T}arr${ctx.func.uniq++}`
1226
+ const decls = trivial
1227
+ ? ['let', ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1228
+ : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1229
+ const cond = ['<', idx, lenVar]
1230
+ const step = ['++', idx]
1231
+ const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
1232
+ r = prep(['for', [';', decls, cond, step], inner])
1233
+ } else if (Array.isArray(head) && head[0] === 'in') {
1234
+ // for (let k in obj) → unroll at compile time when schema known, else HASH runtime iteration
1235
+ const [, decl, src] = head
1236
+ const varName = Array.isArray(decl) && decl[0] === 'let' ? decl[1] : decl
1237
+ const srcName = typeof src === 'string' ? (ctx.scope.chain[src] || src) : null
1238
+ const sid = typeof srcName === 'string' && ctx.schema.vars.get(srcName)
1239
+ if (sid != null) {
1240
+ // Known schema → compile-time unrolling with string keys
1241
+ const keys = ctx.schema.list[sid]
1242
+ if (!keys || !keys.length) { scopes.pop(); return null }
1243
+ includeMods('core', 'string')
1244
+ const stmts = []
1245
+ for (let i = 0; i < keys.length; i++) {
1246
+ stmts.push(i === 0
1247
+ ? ['let', ['=', varName, [, keys[i]]]]
1248
+ : ['=', varName, [, keys[i]]])
1249
+ stmts.push(cloneNode(body))
1250
+ }
1251
+ r = prep([';', ...stmts])
1252
+ } else {
1253
+ // Dynamic object → HASH runtime iteration
1254
+ includeMods('core', 'string', 'collection')
1255
+ r = ['for-in', varName, prep(src), prep(body)]
1256
+ }
1257
+ } else {
1258
+ r = ['for', prep(head), prep(body)]
1259
+ }
1260
+ scopes.pop()
1261
+ return r
1262
+ },
1263
+
1264
+ // Property access - resolve namespaces or object/array properties
1265
+ '.'(obj, prop) {
1266
+ const mod = ctx.scope.chain[obj]
1267
+ // Only treat as module namespace if it's a known built-in module (not a mangled import name)
1268
+ if (typeof obj === 'string' && mod && !mod.includes('.') && mods[MOD_ALIAS[mod] || mod])
1269
+ return includeModule(mod), mod + '.' + prop
1270
+ // Source module namespace: import * as X → X.prop resolved to mangled name
1271
+ if (typeof obj === 'string' && ctx.module.namespaces?.[obj]) {
1272
+ const mangled = ctx.module.namespaces[obj].get(prop)
1273
+ if (mangled) return mangled
1274
+ }
1275
+ // Typed-array/buffer properties auto-include typedarray module
1276
+ if (prop === 'byteLength' || prop === 'byteOffset' || prop === 'buffer') {
1277
+ includeMods('core', 'typedarray')
1278
+ }
1279
+ // Narrowing: unambiguous property name limits module load; otherwise pessimistic.
1280
+ // Receiver-literal narrowing would still need collection for __dyn_get_expr fallback
1281
+ // on unknown props, so it saves nothing — stick with property-name narrowing only.
1282
+ if (typeof prop === 'string' && PROP_MODULES[prop]) includeMods(...PROP_MODULES[prop])
1283
+ else includeMods('core', 'object', 'array', 'string', 'collection')
1284
+ return ['.', prep(obj), prop]
1285
+ },
1286
+
1287
+ // new - auto-import modules, resolve constructors
1288
+ 'new'(ctor, ...args) {
1289
+ let name = ctor, ctorArgs = args
1290
+ if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
1291
+ // Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
1292
+ if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1293
+ ctorArgs = ctorArgs[0].slice(1)
1294
+
1295
+ // Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
1296
+ // expects callArgs as a single element (possibly comma-grouped).
1297
+ const wrapArgs = (args) => args.length === 0 ? [null]
1298
+ : args.length === 1 ? [prep(args[0])]
1299
+ : [[',', ...args.map(prep)]]
1300
+ // TypedArray / buffer constructors
1301
+ const typedArrays = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
1302
+ if (typedArrays.includes(name)) {
1303
+ includeMods('core', 'typedarray')
1304
+ return ['()', `new.${name}`, ...wrapArgs(ctorArgs)]
1305
+ }
1306
+ // Set/Map constructors
1307
+ if (name === 'Set' || name === 'Map') {
1308
+ includeMods('core', 'collection')
1309
+ return ['()', `new.${name}`, ...wrapArgs(ctorArgs)]
1310
+ }
1311
+
1312
+ const mod = ctx.scope.chain[name]
1313
+ if (typeof name === 'string' && mod && !mod.includes('.')) includeModule(mod)
1314
+ // Unknown constructor: treat as function call (jzify already strips new for known safe ones)
1315
+ if (typeof name === 'string') return ['()', name, ...ctorArgs.map(prep)]
1316
+ return ['new', prep(ctor), ...args.map(prep)]
1317
+ }
1318
+ }
1319
+
1320
+ // Namespace → module mapping (namespaces that share a module)
1321
+ const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string' }
1322
+ /** Auto-inclusion graph: loading a module also loads its listed prerequisites.
1323
+ * Not a strict ordering: module init() functions only register emitters/stdlib entries,
1324
+ * so relative init order does not affect correctness — emitters are looked up lazily
1325
+ * at compile time. Cycles (e.g. number ↔ string) are broken via the in-progress guard. */
1326
+ const MOD_DEPS = {
1327
+ number: ['core', 'string'],
1328
+ string: ['core', 'number'],
1329
+ array: ['core'],
1330
+ object: ['core'],
1331
+ collection: ['core', 'number'],
1332
+ symbol: ['core'],
1333
+ json: ['core', 'string', 'number', 'collection'],
1334
+ console: ['core', 'string', 'number'],
1335
+ regex: ['core', 'string', 'array'],
1336
+ }
1337
+
1338
+ const includeMods = (...names) => names.forEach(includeModule)
1339
+
1340
+ /** Register a module and its transitive deps. Idempotent; cycle-safe via early-mark. */
1341
+ function includeModule(name) {
1342
+ const modName = MOD_ALIAS[name] || name
1343
+ const init = mods[modName]
1344
+ if (!init) return err(`Module not found: ${name}`)
1345
+ if (ctx.module.modules[modName]) return
1346
+ ctx.module.modules[modName] = true // mark before deps so cycles terminate
1347
+ for (const dep of MOD_DEPS[modName] || []) includeModule(dep)
1348
+ init(ctx) // modules receive ctx explicitly instead of importing it
1349
+ }
1350
+
1351
+ /** Merge source schemas into target via Object.assign for compile-time schema inference. */
1352
+ function inferAssignSchema(callNode) {
1353
+ // After prep, args may be comma-grouped: ['()', callee, [',', target, s1, s2]]
1354
+ let assignArgs = callNode.slice(2)
1355
+ if (assignArgs.length === 1 && Array.isArray(assignArgs[0]) && assignArgs[0][0] === ',')
1356
+ assignArgs = assignArgs[0].slice(1)
1357
+ const [target, ...sources] = assignArgs
1358
+ if (typeof target !== 'string') return
1359
+ const existingId = ctx.schema.vars.get(target)
1360
+ const merged = existingId != null ? [...ctx.schema.list[existingId]] : []
1361
+ for (const src of sources) {
1362
+ let srcProps
1363
+ if (Array.isArray(src) && src[0] === '{}')
1364
+ srcProps = src.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
1365
+ else if (typeof src === 'string') {
1366
+ const srcId = ctx.schema.vars.get(src)
1367
+ if (srcId != null) srcProps = ctx.schema.list[srcId]
1368
+ }
1369
+ if (srcProps) for (const p of srcProps) if (!merged.includes(p)) merged.push(p)
1370
+ }
1371
+ if (merged.length) ctx.schema.vars.set(target, ctx.schema.register(merged))
1372
+ }
1373
+
1374
+ function defFunc(name, node) {
1375
+ if (!Array.isArray(node) || node[0] !== '=>') return false
1376
+ // Only extract top-level functions, not nested (closures stay as values)
1377
+ if (depth > 0) return false
1378
+ let [, rawParams, body] = node
1379
+ const raw = extractParams(rawParams)
1380
+
1381
+ // Extract param names and defaults via shared classifier.
1382
+ // Destructured params desugar to fresh tmp + let-binding prefix in body.
1383
+ const params = [], defaults = {}, hasRest = [], bodyPrefix = []
1384
+ for (const r of raw) {
1385
+ const c = classifyParam(r)
1386
+ if (c.kind === 'rest') { hasRest.push(c.name); params.push({ name: c.name, type: 'f64', rest: true }) }
1387
+ else if (c.kind === 'plain') params.push({ name: c.name, type: 'f64' })
1388
+ else if (c.kind === 'default') {
1389
+ params.push({ name: c.name, type: 'f64' })
1390
+ const defVal = prep(c.defValue)
1391
+ defaults[c.name] = defVal
1392
+ if (Array.isArray(defVal) && defVal[0] === '{}' && defVal.length > 1 && ctx.schema.register) {
1393
+ const props = defVal.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
1394
+ if (props.length) ctx.schema.vars.set(c.name, ctx.schema.register(props))
1395
+ }
1396
+ } else {
1397
+ const tmp = `${T}p${ctx.func.uniq++}`
1398
+ params.push({ name: tmp, type: 'f64' })
1399
+ if (c.kind === 'destruct-default') defaults[tmp] = prep(c.defValue)
1400
+ bodyPrefix.push(['let', ['=', c.pattern, tmp]])
1401
+ }
1402
+ }
1403
+
1404
+ // Prepend destructuring to body (body is already prepped, so prefix needs prep too)
1405
+ if (bodyPrefix.length) {
1406
+ const preppedPrefix = bodyPrefix.map(prep).filter(x => x != null)
1407
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
1408
+ body = ['{}', [';', ...preppedPrefix, ...body[1].slice(1)]]
1409
+ else if (Array.isArray(body) && body[0] === '{}')
1410
+ body = ['{}', [';', ...preppedPrefix, body[1]]]
1411
+ else
1412
+ body = ['{}', [';', ...preppedPrefix, ['return', body]]]
1413
+ }
1414
+
1415
+ const sig = { params, results: detectResults(body) }
1416
+ const hasDefaults = Object.keys(defaults).length > 0
1417
+ // Only main-module top-level exports become wasm-boundary exports.
1418
+ // Sub-module `export let X` is just a re-importable symbol — staying internal
1419
+ // unlocks treeshake + type specialization once main stops referencing it.
1420
+ const exported = !!ctx.func.exports[name] && ctx.module.moduleStack.length === 0
1421
+ const funcInfo = { name, body, exported, sig, ...(hasDefaults && { defaults }) }
1422
+ if (hasRest.length) funcInfo.rest = hasRest[0] // track rest param name
1423
+ ctx.func.list.push(funcInfo)
1424
+ return true
1425
+ }
1426
+
1427
+ // Multi-value threshold: ≤8 elements = tuple (multi-value return), >8 = memory array
1428
+ const MAX_MULTI = 8
1429
+
1430
+ /** Detect return arity from function body. */
1431
+ function detectResults(body) {
1432
+ // Expression body: [e1, e2, ...] → multi-return if ≤ threshold and no spreads
1433
+ if (Array.isArray(body) && body[0] === '[' && body.length > 2 && !body.some(e => Array.isArray(e) && e[0] === '...')) {
1434
+ const n = body.length - 1
1435
+ if (n <= MAX_MULTI) return Array(n).fill('f64')
1436
+ }
1437
+ // Block body: scan return statements
1438
+ if (Array.isArray(body) && body[0] === '{}') {
1439
+ const rets = []
1440
+ collectReturns(body, rets)
1441
+ if (rets.length) {
1442
+ const n = rets[0]
1443
+ if (n > 1 && n <= MAX_MULTI && rets.every(r => r === n)) return Array(n).fill('f64')
1444
+ }
1445
+ }
1446
+ return ['f64']
1447
+ }
1448
+
1449
+ /** Collect return value arities from block AST. */
1450
+ function collectReturns(node, out) {
1451
+ if (!Array.isArray(node)) return
1452
+ if (node[0] === 'return') {
1453
+ const val = node[1]
1454
+ // Array return: count elements, but only if no spreads (spreads → runtime array, not multi-value)
1455
+ if (Array.isArray(val) && val[0] === '[' && val.length > 2 && !val.some(e => Array.isArray(e) && e[0] === '...'))
1456
+ out.push(val.length - 1)
1457
+ else out.push(1)
1458
+ return
1459
+ }
1460
+ for (let i = 1; i < node.length; i++) collectReturns(node[i], out)
1461
+ }
1462
+
1463
+ const isLit = n => Array.isArray(n) && n[0] == null
1464
+
1465
+ /** Compile-time bundling: parse + prepare an imported module, collect exports. */
1466
+ function prepareModule(specifier, source) {
1467
+ includeModule('core')
1468
+ // Cycle detection
1469
+ if (ctx.module.moduleStack.includes(specifier))
1470
+ err(`Circular import: ${ctx.module.moduleStack.join(' -> ')} -> ${specifier}`)
1471
+ // Already resolved
1472
+ if (ctx.module.resolvedModules.has(specifier)) return ctx.module.resolvedModules.get(specifier)
1473
+
1474
+ ctx.module.moduleStack.push(specifier)
1475
+
1476
+ // Name mangling prefix: ./math.jz → _math_jz
1477
+ const prefix = specifier.replace(/[^a-zA-Z0-9]/g, '_')
1478
+
1479
+ // Save caller state
1480
+ const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
1481
+ const savedFuncCount = ctx.func.list.length // track new funcs from this module
1482
+ const savedModulePrefix = ctx.module.currentPrefix
1483
+ ctx.scope.chain = derive(savedScope) // inherit parent scope
1484
+ ctx.func.exports = {}
1485
+ ctx.module.currentPrefix = prefix
1486
+
1487
+ // Parse + prepare imported source (may trigger recursive imports)
1488
+ let ast = parse(source)
1489
+ if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
1490
+ const savedDepth = depth; depth = 0
1491
+ const moduleInit = prep(ast)
1492
+ depth = savedDepth
1493
+
1494
+ // Collect exports: rename exported funcs with prefix
1495
+ const moduleExports = new Map()
1496
+ for (const name of Object.keys(ctx.func.exports)) {
1497
+ const val = ctx.func.exports[name]
1498
+ // Default export alias: export default existingName → map 'default' to that name's mangled form
1499
+ if (name === 'default' && typeof val === 'string') {
1500
+ // Will resolve after all named exports are mangled
1501
+ continue
1502
+ }
1503
+ const mangled = `${prefix}$${name}`
1504
+ moduleExports.set(name, mangled)
1505
+ // Rename the function in ctx.func.list
1506
+ const func = ctx.func.list.find(f => f.name === name)
1507
+ if (func) func.name = mangled
1508
+ // Rename globals
1509
+ if (ctx.scope.globals.has(name)) {
1510
+ const wat = ctx.scope.globals.get(name).replace(`$${name}`, `$${mangled}`)
1511
+ ctx.scope.globals.delete(name)
1512
+ ctx.scope.globals.set(mangled, wat)
1513
+ if (ctx.scope.userGlobals.has(name)) { ctx.scope.userGlobals.delete(name); ctx.scope.userGlobals.add(mangled) }
1514
+ if (ctx.scope.globalTypes.has(name)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(name)); ctx.scope.globalTypes.delete(name) }
1515
+ }
1516
+ }
1517
+ // Resolve default export alias after named exports are mangled
1518
+ if (typeof ctx.func.exports['default'] === 'string') {
1519
+ const alias = ctx.func.exports['default']
1520
+ if (moduleExports.has(alias)) {
1521
+ // Already renamed as a named export
1522
+ moduleExports.set('default', moduleExports.get(alias))
1523
+ } else {
1524
+ // Not a named export — rename the function/global
1525
+ const mangled = `${prefix}$${alias}`
1526
+ moduleExports.set('default', mangled)
1527
+ const func = ctx.func.list.find(f => f.name === alias)
1528
+ if (func) func.name = mangled
1529
+ if (ctx.scope.globals.has(alias)) {
1530
+ const wat = ctx.scope.globals.get(alias).replace(`$${alias}`, `$${mangled}`)
1531
+ ctx.scope.globals.delete(alias)
1532
+ ctx.scope.globals.set(mangled, wat)
1533
+ if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ // Rename ALL non-exported functions created during this module's prep
1539
+ // (fn property assignments like f32.parse, internal helpers like cleanInt)
1540
+ for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
1541
+ const func = ctx.func.list[i]
1542
+ if (func.raw || func.name.startsWith(prefix + '$')) continue
1543
+ // Skip functions from sub-imports (already prefixed with another module's prefix)
1544
+ if (func.name.includes('__') && func.name.includes('$')) continue
1545
+ const mangled = `${prefix}$${func.name}`
1546
+ moduleExports.set(func.name, mangled)
1547
+ func.name = mangled
1548
+ }
1549
+
1550
+ // Add mangled non-exported globals to moduleExports for walk renaming
1551
+ // (e.g., module-level const/let used by functions declared before the global)
1552
+ for (const [mangled, wat] of ctx.scope.globals) {
1553
+ if (mangled.startsWith(prefix + '$')) {
1554
+ const original = mangled.slice(prefix.length + 1)
1555
+ if (!moduleExports.has(original)) moduleExports.set(original, mangled)
1556
+ }
1557
+ }
1558
+
1559
+ // Rename references in function bodies — walk ALL functions created during this module's prep
1560
+ if (moduleExports.size) {
1561
+ const walk = (node, skip) => {
1562
+ if (!Array.isArray(node)) return typeof node === 'string' && !skip?.has(node) && moduleExports.has(node) ? moduleExports.get(node) : node
1563
+ if (node[0] === 'str' || node[0] == null || node[0] === '`' || node[0] === '//') return node
1564
+ if (node[0] === ':') { node[2] = walk(node[2], skip); return node }
1565
+ if (node[0] === '=>') {
1566
+ node[2] = walk(node[2], collectParamNames(extractParams(node[1]), new Set(skip)))
1567
+ return node
1568
+ }
1569
+ for (let j = 0; j < node.length; j++) node[j] = walk(node[j], skip)
1570
+ return node
1571
+ }
1572
+ for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
1573
+ const func = ctx.func.list[i]
1574
+ if (!func.body) continue
1575
+ const funcParams = new Set(func.sig?.params?.map(p => p.name) || [])
1576
+ walk(func.body, funcParams)
1577
+ if (func.defaults) for (const [k, v] of Object.entries(func.defaults)) func.defaults[k] = walk(v, funcParams)
1578
+ }
1579
+ // Also rename init code AST
1580
+ if (moduleInit) walk(moduleInit)
1581
+ }
1582
+
1583
+ // Collect sub-module init code (variable initializations) for __start
1584
+ if (moduleInit) {
1585
+ if (!ctx.module.moduleInits) ctx.module.moduleInits = []
1586
+ ctx.module.moduleInits.push(moduleInit)
1587
+ }
1588
+
1589
+ // Restore caller state
1590
+ ctx.scope.chain = savedScope
1591
+ ctx.func.exports = savedExports
1592
+ ctx.module.currentPrefix = savedModulePrefix
1593
+ ctx.module.moduleStack.pop()
1594
+
1595
+ const result = { exports: moduleExports }
1596
+ ctx.module.resolvedModules.set(specifier, result)
1597
+ return result
1598
+ }