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