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/emit.js ADDED
@@ -0,0 +1,2095 @@
1
+ /**
2
+ * AST → WASM IR emission.
3
+ *
4
+ * # Stage contract
5
+ * IN: prepared AST node + ctx state (func.locals, func.repByLocal, types.typedElem, etc.)
6
+ * OUT: IR node (array) with `.type` ('i32' | 'f64' | 'void'). For statements, a flat
7
+ * list of WASM instructions (no type tag).
8
+ * NO-MUTATE: emit does not rewrite the AST. Side effects go to ctx.runtime.*,
9
+ * ctx.core.includes (via inc()), ctx.func.uniq (local naming), and ctx.features.*.
10
+ *
11
+ * # Dispatch
12
+ * `emit(node, expect?)` handles literals inline and routes arrays to ctx.core.emit[op].
13
+ * `emitFlat(node)` emits + drops any value (statement context; routes block bodies to emitBody).
14
+ * `emitBody(node)` unwraps a `{}` block and concatenates flat statement IR.
15
+ *
16
+ * The emitter table (`emitter` export) is copied into ctx.core.emit by reset();
17
+ * language modules add/override entries to extend dispatch.
18
+ *
19
+ * Low-level IR construction helpers (typed/asF64/allocPtr/readVar/…) live in compile.js
20
+ * and are imported below.
21
+ *
22
+ * @module emit
23
+ */
24
+
25
+ import { ctx, err, inc, PTR } from './ctx.js'
26
+ import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal } from './analyze.js'
27
+ import {
28
+ typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
29
+ NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
30
+ WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
31
+ mkPtrIR, ptrOffsetIR, ptrTypeIR,
32
+ isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64,
33
+ truthyIR, toBoolFromEmitted, isPostfix,
34
+ isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
35
+ temp, tempI32, tempI64, allocPtr,
36
+ boxedAddr, readVar, writeVar, isNullish,
37
+ multiCount, loopTop, flat,
38
+ reconstructArgsWithSpreads,
39
+ } from './ir.js'
40
+
41
+ // Current emission "expect" mode ('void' or null); set by emit(), read by compound-assignment emitters
42
+ // to decide whether to emit a value-returning or side-effect-only form.
43
+ let _expect = null
44
+
45
+ /** Emit typeof comparison: typeof x == typeCode → type-aware check. */
46
+ export function emitTypeofCmp(a, b, cmpOp) {
47
+ let typeofExpr, code
48
+ if (Array.isArray(a) && a[0] === 'typeof' && typeof b === 'number') { typeofExpr = a[1]; code = b }
49
+ else if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null) { typeofExpr = a[1]; code = b[1] }
50
+ else return null
51
+ if (typeof code !== 'number') return null
52
+
53
+ const t = temp()
54
+ const va = asF64(emit(typeofExpr))
55
+ const eq = cmpOp === 'eq'
56
+
57
+ if (code === -1) {
58
+ return typed(eq
59
+ ? ['f64.eq', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
60
+ : ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
61
+ }
62
+ if (code === -2) {
63
+ inc('__ptr_type')
64
+ const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
65
+ const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
66
+ const isStr = ['i32.or',
67
+ ['i32.eq', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
68
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]]
69
+ return typed(eq ? ['i32.and', isPtr, isStr]
70
+ : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isStr]], 'i32')
71
+ }
72
+ if (code === -3) {
73
+ const check = isNullish(va)
74
+ return typed(eq ? check : ['i32.eqz', check], 'i32')
75
+ }
76
+ if (code === -4) {
77
+ return typed(['i32.const', eq ? 0 : 1], 'i32')
78
+ }
79
+ if (code === -5) {
80
+ inc('__ptr_type')
81
+ const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
82
+ const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
83
+ const notStrFn = ['i32.and',
84
+ ['i32.and',
85
+ ['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
86
+ ['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
87
+ ['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]]
88
+ const notNullish = ['i32.eqz', isNullish(['local.get', `$${t}`])]
89
+ const check = ['i32.and', ['i32.and', isPtr, notStrFn], notNullish]
90
+ return typed(eq ? check : ['i32.eqz', check], 'i32')
91
+ }
92
+ if (code === -6) {
93
+ inc('__ptr_type')
94
+ const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
95
+ const isFn = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', PTR.CLOSURE]]
96
+ return typed(eq ? ['i32.and', isPtr, isFn] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isFn]], 'i32')
97
+ }
98
+ if (code >= 0) {
99
+ inc('__ptr_type')
100
+ const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
101
+ const check = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', code]]
102
+ return typed(eq ? ['i32.and', isPtr, check] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', check]], 'i32')
103
+ }
104
+ return null
105
+ }
106
+
107
+ const CMP_SET = new Set(['>', '<', '>=', '<=', '==', '!=', '!'])
108
+ const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
109
+
110
+ // Pointer kinds for which JS `==` / `!=` is pure reference equality — i.e. i64 bit
111
+ // compare of the NaN-box is equivalent to __eq. Excludes STRING (content compare for
112
+ // heap strings) and BIGINT (content compare).
113
+ const REF_EQ_KINDS = new Set([
114
+ VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
115
+ VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX,
116
+ ])
117
+
118
+ // === Flow-sensitive type refinement ===
119
+ // Map typeof code (from resolveTypeof in prepare.js) → VAL kind. Undef/boolean/object have no
120
+ // single VAL refinement, so they're excluded. String/number/function do.
121
+ const TYPEOF_CODE_TO_VAL = { [-1]: VAL.NUMBER, [-2]: VAL.STRING, [-6]: VAL.CLOSURE }
122
+
123
+ /** Extract refinements from a boolean condition AST.
124
+ * `sense`: true = refine for then-branch, false = refine for else-branch (i.e. cond inverted).
125
+ * Returns a Map<name, VAL>. Walks && / || / ! accordingly. */
126
+ function extractRefinements(cond, out, sense = true) {
127
+ if (!Array.isArray(cond)) return out
128
+ const op = cond[0]
129
+ // ! flips sense
130
+ if (op === '!') return extractRefinements(cond[1], out, !sense)
131
+ // && under positive sense refines with union of both branches.
132
+ // || under negative sense (De Morgan) similarly refines the else-branch.
133
+ if (op === '&&' && sense) { extractRefinements(cond[1], out, true); extractRefinements(cond[2], out, true); return out }
134
+ if (op === '||' && !sense) { extractRefinements(cond[1], out, false); extractRefinements(cond[2], out, false); return out }
135
+ // typeof x == 'number' | 'string' | 'function' — sense must be positive for "==", negative for "!="
136
+ if ((op === '==' || op === '===' || op === '!=' || op === '!==')) {
137
+ const eq = (op === '==' || op === '===')
138
+ const wantPositive = eq ? sense : !sense
139
+ if (!wantPositive) return out
140
+ const a = cond[1], b = cond[2]
141
+ const pair = Array.isArray(a) && a[0] === 'typeof' ? [a[1], b]
142
+ : Array.isArray(b) && b[0] === 'typeof' ? [b[1], a] : null
143
+ if (pair && typeof pair[0] === 'string' && Array.isArray(pair[1]) && pair[1][0] == null) {
144
+ const val = TYPEOF_CODE_TO_VAL[pair[1][1]]
145
+ if (val) out.set(pair[0], val)
146
+ }
147
+ return out
148
+ }
149
+ // Array.isArray(x) — only refines under positive sense.
150
+ // Callee may be the flattened string 'Array.isArray' or the raw ['.', 'Array', 'isArray'] pair.
151
+ if (op === '()' && sense && typeof cond[2] === 'string') {
152
+ const callee = cond[1]
153
+ const isArr = callee === 'Array.isArray'
154
+ || (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Array' && callee[2] === 'isArray')
155
+ if (isArr) { out.set(cond[2], VAL.ARRAY); return out }
156
+ }
157
+ return out
158
+ }
159
+
160
+ /** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
161
+ * Conservative over-reject: if unsure, treat as written.
162
+ * `let`/`const` declarations are NOT reassignments — only the initializer expressions
163
+ * inside them are scanned. (Treating `let g = ...` as a write of `g` would defeat A3.) */
164
+ export function isReassigned(body, name) {
165
+ if (!Array.isArray(body)) return false
166
+ const op = body[0]
167
+ if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%='
168
+ || op === '&=' || op === '|=' || op === '^=' || op === '<<=' || op === '>>=' || op === '>>>='
169
+ || op === '||=' || op === '&&=' || op === '??=') {
170
+ if (body[1] === name) return true
171
+ }
172
+ if ((op === '++' || op === '--') && body[1] === name) return true
173
+ if (op === 'let' || op === 'const') {
174
+ // Each decl item is either a bare name (string) or `['=', pattern, init]`.
175
+ // Only the init expression can contain real reassignments — recurse into it only.
176
+ for (let i = 1; i < body.length; i++) {
177
+ const d = body[i]
178
+ if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
179
+ }
180
+ return false
181
+ }
182
+ for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
183
+ return false
184
+ }
185
+
186
+ /** Does `body` contain a `continue` that targets THIS loop?
187
+ * A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
188
+ function hasOwnContinue(body) {
189
+ if (!Array.isArray(body)) return false
190
+ const op = body[0]
191
+ if (op === 'continue') return true
192
+ if (op === 'for' || op === 'while' || op === 'do') return false
193
+ for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
194
+ return false
195
+ }
196
+
197
+ function hasOwnBreakOrContinue(body) {
198
+ if (!Array.isArray(body)) return false
199
+ const op = body[0]
200
+ if (op === 'break' || op === 'continue') return true
201
+ if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
202
+ for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
203
+ return false
204
+ }
205
+
206
+ function containsNestedClosure(body) {
207
+ if (!Array.isArray(body)) return false
208
+ if (body[0] === '=>') return true
209
+ for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
210
+ return false
211
+ }
212
+
213
+ function containsNestedLoop(body) {
214
+ if (!Array.isArray(body)) return false
215
+ const op = body[0]
216
+ if (op === 'for' || op === 'while' || op === 'do') return true
217
+ if (op === '=>') return false
218
+ for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
219
+ return false
220
+ }
221
+
222
+ function containsDeclOf(body, name) {
223
+ if (!Array.isArray(body)) return false
224
+ const op = body[0]
225
+ if (op === '=>') return false
226
+ if (op === 'let' || op === 'const') {
227
+ for (let i = 1; i < body.length; i++) {
228
+ const d = body[i]
229
+ if (d === name) return true
230
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
231
+ }
232
+ }
233
+ for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
234
+ return false
235
+ }
236
+
237
+ function intLiteralValue(expr) {
238
+ let v = null
239
+ if (typeof expr === 'number') v = expr
240
+ else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
241
+ else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
242
+ else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
243
+ return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
244
+ }
245
+
246
+ function cloneWithSubst(node, name, value) {
247
+ if (node === name) return [null, value]
248
+ if (!Array.isArray(node)) return node
249
+ if (node[0] === '=>') return node
250
+ return node.map(x => cloneWithSubst(x, name, value))
251
+ }
252
+
253
+ const MAX_SMALL_FOR_UNROLL = 8
254
+
255
+ function unrollSmallConstFor(init, cond, step, body) {
256
+ if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
257
+ const decl = init[1]
258
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
259
+ const name = decl[1]
260
+ const start = intLiteralValue(decl[2])
261
+ if (start !== 0) return null
262
+
263
+ if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
264
+ const end = intLiteralValue(cond[2])
265
+ if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
266
+
267
+ const stepOk = Array.isArray(step) && (
268
+ (step[0] === '++' && step[1] === name) ||
269
+ (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
270
+ )
271
+ if (!stepOk) return null
272
+ if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsNestedLoop(body) || containsDeclOf(body, name)) return null
273
+ if (isReassigned(body, name)) return null
274
+
275
+ const out = []
276
+ for (let i = 0; i < end; i++) out.push(...emitFlat(cloneWithSubst(body, name, i)))
277
+ return out
278
+ }
279
+
280
+ /** Does `body` always exit the enclosing scope (return / throw / break / continue)?
281
+ * Used for early-return refinement: after `if (!guard) return`, `guard` holds for the rest. */
282
+ function isTerminator(body) {
283
+ if (!Array.isArray(body)) return false
284
+ const op = body[0]
285
+ if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
286
+ // Block body: {} or ; — terminator if it ends with a terminator statement.
287
+ if (op === '{}' || op === ';') {
288
+ for (let i = body.length - 1; i >= 1; i--) {
289
+ const s = body[i]
290
+ if (s == null) continue
291
+ return isTerminator(s)
292
+ }
293
+ return false
294
+ }
295
+ return false
296
+ }
297
+
298
+ /** Emit pending `finally` cleanups for an abrupt control-flow exit.
299
+ * Inner cleanups run before outer cleanups. While emitting each cleanup, remove
300
+ * it from the active stack so `return` inside `finally` does not re-enter it. */
301
+ function emitFinalizers() {
302
+ const stack = ctx.func.finallyStack || []
303
+ if (stack.length === 0) return []
304
+ const saved = stack.slice()
305
+ const out = []
306
+ for (let i = saved.length - 1; i >= 0; i--) {
307
+ ctx.func.finallyStack = saved.slice(0, i)
308
+ out.push(...emitFlat(saved[i]))
309
+ }
310
+ ctx.func.finallyStack = saved
311
+ return out
312
+ }
313
+
314
+ function withFinallyStack(stack, fn) {
315
+ const prev = ctx.func.finallyStack || []
316
+ ctx.func.finallyStack = stack
317
+ try { return fn() }
318
+ finally { ctx.func.finallyStack = prev }
319
+ }
320
+
321
+ /** Apply refinements for the duration of `fn()`. Restores prior state on return/throw. */
322
+ function withRefinements(refs, body, fn) {
323
+ if (!refs || refs.size === 0) return fn()
324
+ const cur = ctx.func.refinements
325
+ // Drop names that are reassigned in the body — refinement would be unsound.
326
+ const saved = []
327
+ for (const [name, val] of refs) {
328
+ if (isReassigned(body, name)) continue
329
+ saved.push([name, cur.get(name)])
330
+ cur.set(name, val)
331
+ }
332
+ try { return fn() }
333
+ finally {
334
+ for (const [name, prev] of saved) {
335
+ if (prev === undefined) cur.delete(name); else cur.set(name, prev)
336
+ }
337
+ }
338
+ }
339
+
340
+ /** Coerce an AST node to an i32 boolean, folding && / || at the boolean boundary. */
341
+ export function toBool(node) {
342
+ const op = Array.isArray(node) ? node[0] : null
343
+ if (CMP_SET.has(op)) return emit(node)
344
+ if (op === '&&') {
345
+ const la = toBool(node[1]), lb = toBool(node[2])
346
+ if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.and', la, lb], 'i32')
347
+ return typed(['if', ['result', 'i32'], la, ['then', lb], ['else', ['i32.const', 0]]], 'i32')
348
+ }
349
+ if (op === '||') {
350
+ const la = toBool(node[1]), lb = toBool(node[2])
351
+ if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.or', la, lb], 'i32')
352
+ return typed(['if', ['result', 'i32'], la, ['then', ['i32.const', 1]], ['else', lb]], 'i32')
353
+ }
354
+ return toBoolFromEmitted(emit(node))
355
+ }
356
+
357
+ /** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
358
+ * i32 offset), else falls back to numeric WASM type coercion. */
359
+ function emitArgForParam(ir, param) {
360
+ if (param?.ptrKind != null) return ptrOffsetIR(ir, param.ptrKind)
361
+ return asParamType(ir, param?.type)
362
+ }
363
+
364
+ /**
365
+ * Materialize a multi-value function call as a heap array.
366
+ * Call → store each result in temp → copy to allocated array → return pointer.
367
+ */
368
+ export function materializeMulti(callNode) {
369
+ const name = callNode[1]
370
+ const func = ctx.func.map.get(name)
371
+ const n = func.sig.results.length
372
+ const rawArgs = callNode.slice(2)
373
+ const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
374
+ ? rawArgs[0].slice(1) : rawArgs
375
+ const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
376
+ while (emittedArgs.length < func.sig.params.length)
377
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
378
+ const temps = Array.from({ length: n }, () => temp())
379
+ const out = allocPtr({ type: 1, len: n, tag: 'marr' })
380
+ const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
381
+ for (let k = n - 1; k >= 0; k--) ir.push(['local.set', `$${temps[k]}`])
382
+ for (let k = 0; k < n; k++)
383
+ ir.push(['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.const', k * 8]], ['local.get', `$${temps[k]}`]])
384
+ ir.push(out.ptr)
385
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
386
+ }
387
+
388
+ /** Emit let/const initializations as typed local.set instructions. */
389
+ export function emitDecl(...inits) {
390
+ const result = []
391
+ for (let ii = 0; ii < inits.length; ii++) {
392
+ const i = inits[ii]
393
+ if (typeof i === 'string') {
394
+ const undef = nullExpr()
395
+ if (ctx.func.boxed.has(i)) {
396
+ const cell = ctx.func.boxed.get(i)
397
+ ctx.func.locals.set(cell, 'i32')
398
+ result.push(
399
+ ['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
400
+ ['f64.store', ['local.get', `$${cell}`], undef])
401
+ continue
402
+ }
403
+ if (isGlobal(i)) {
404
+ if (!ctx.scope.globalTypes.has(i)) result.push(['global.set', `$${i}`, undef])
405
+ continue
406
+ }
407
+ result.push(['local.set', `$${i}`, undef])
408
+ continue
409
+ }
410
+ if (!Array.isArray(i) || i[0] !== '=') continue
411
+ const [, name, init] = i
412
+ if (typeof name !== 'string' || init == null) continue
413
+
414
+ // Multi-value ephemeral destructuring — skip heap alloc when temp is
415
+ // assigned from a multi-value call then immediately destructured element-by-element.
416
+ if (name.startsWith(T) && Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string'
417
+ && ctx.func.names?.has(init[1])) {
418
+ const func = ctx.func.map.get(init[1])
419
+ const n = func?.sig.results.length
420
+ if (n > 1) {
421
+ const targets = []
422
+ let match = true
423
+ for (let k = 0; k < n && match; k++) {
424
+ const next = inits[ii + 1 + k]
425
+ if (!Array.isArray(next) || next[0] !== '=' || typeof next[1] !== 'string') { match = false; break }
426
+ const rhs = next[2]
427
+ if (!Array.isArray(rhs) || rhs[0] !== '[]' || rhs[1] !== name) { match = false; break }
428
+ const idx = rhs[2]
429
+ if (!Array.isArray(idx) || idx[0] != null || idx[1] !== k) { match = false; break }
430
+ if (ctx.func.boxed.has(next[1]) || isGlobal(next[1])) { match = false; break }
431
+ targets.push(next[1])
432
+ }
433
+ if (match && targets.length === n) {
434
+ const rawArgs = init.slice(2)
435
+ const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
436
+ ? rawArgs[0].slice(1) : rawArgs
437
+ const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
438
+ while (emittedArgs.length < func.sig.params.length)
439
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
440
+ result.push(['call', `$${init[1]}`, ...emittedArgs])
441
+ for (let k = n - 1; k >= 0; k--)
442
+ result.push(['local.set', `$${targets[k]}`])
443
+ ii += n
444
+ continue
445
+ }
446
+ }
447
+ }
448
+ const isObjLit = Array.isArray(init) && init[0] === '{}'
449
+ if (isObjLit) ctx.schema.targetStack.push(name)
450
+ const val = emit(init)
451
+ if (isObjLit) ctx.schema.targetStack.pop()
452
+ // Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
453
+ // Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
454
+ // isReassigned is conservative across nested arrow shadows — we miss the optimization
455
+ // rather than emit a wrong direct call.
456
+ if (val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
457
+ && ctx.func.body && !isReassigned(ctx.func.body, name)) {
458
+ if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
459
+ ctx.func.directClosures.set(name, val.closureBodyName)
460
+ }
461
+ if (ctx.func.boxed.has(name)) {
462
+ const cell = ctx.func.boxed.get(name)
463
+ ctx.func.locals.set(cell, 'i32')
464
+ result.push(
465
+ ['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
466
+ ['f64.store', ['local.get', `$${cell}`], asF64(val)])
467
+ continue
468
+ }
469
+ if (isGlobal(name)) {
470
+ // Unboxed pointer const globals carry the raw i32 offset; init coerces via asPtrOffset.
471
+ const grep = repOfGlobal(name)
472
+ if (grep?.ptrKind != null) {
473
+ result.push(['global.set', `$${name}`, asPtrOffset(val, grep.ptrKind)])
474
+ continue
475
+ }
476
+ // Pre-folded numeric const globals have their init baked into the decl — skip.
477
+ if (ctx.scope.globalTypes.has(name)) continue
478
+ result.push(['global.set', `$${name}`, asF64(val)])
479
+ continue
480
+ }
481
+ const localType = ctx.func.locals.get(name) || 'f64'
482
+ let ptrKind = repOf(name)?.ptrKind
483
+ // Inherit ptrKind from a pointer-ABI RHS: destructure temps (`__d0 = v`) and other
484
+ // fresh let-bindings whose init is already an unboxed pointer. Without this, readVar
485
+ // returns an untyped i32 local.get and later `asF64` emits a numeric convert instead
486
+ // of a ptr-rebox. Safe because emitDecl runs once per let/const binding.
487
+ if (ptrKind == null && val.ptrKind != null && localType === 'i32' && !ctx.func.boxed?.has(name)) {
488
+ updateRep(name, { ptrKind: val.ptrKind })
489
+ ptrKind = val.ptrKind
490
+ if (val.ptrAux != null) {
491
+ updateRep(name, { ptrAux: val.ptrAux })
492
+ // OBJECT-only: aux *is* the schemaId; mirror to ctx.schema.vars + rep.schemaId so
493
+ // .prop slot resolution sees a precise binding. TYPED/CLOSURE aux carries other
494
+ // semantics (elem code / funcIdx) and must not leak into schema lookups.
495
+ if (val.ptrKind === VAL.OBJECT && !ctx.schema.vars?.has(name)) {
496
+ ctx.schema.vars.set(name, val.ptrAux)
497
+ updateRep(name, { schemaId: val.ptrAux })
498
+ }
499
+ }
500
+ }
501
+ let coerced
502
+ if (ptrKind != null) {
503
+ // Unboxed pointer local — extract i32 offset from NaN-boxed f64 via reinterpret, not numeric trunc.
504
+ // CLOSURE init carries funcIdx in val.closureFuncIdx; persist it on the rep so a later
505
+ // asF64 (escape: store, return, indirect-call rebox) reconstructs the correct table slot.
506
+ if (ptrKind === VAL.CLOSURE && val.closureFuncIdx != null && repOf(name)?.ptrAux == null)
507
+ updateRep(name, { ptrAux: val.closureFuncIdx })
508
+ coerced = val.ptrKind === ptrKind ? val
509
+ : typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
510
+ } else {
511
+ coerced = localType === 'f64' ? asF64(val) : asI32(val)
512
+ }
513
+ if (!(isLit(coerced) && coerced[1] === 0 && !ctx.func.stack.length))
514
+ result.push(['local.set', `$${name}`, coerced])
515
+
516
+ const schemaId = ctx.schema.idOf?.(name)
517
+ if (ctx.func.localProps?.has(name) && schemaId != null) {
518
+ const schema = ctx.schema.resolve(name)
519
+ if (schema?.[0] === '__inner__') {
520
+ inc('__alloc', '__mkptr')
521
+ const bt = `${T}bx${ctx.func.uniq++}`
522
+ ctx.func.locals.set(bt, 'i32')
523
+ const innerName = `${name}${T}inner`
524
+ ctx.func.locals.set(innerName, 'f64')
525
+ result.push(
526
+ ['local.set', `$${innerName}`, ['local.get', `$${name}`]],
527
+ ['local.set', `$${bt}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]],
528
+ ['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
529
+ ...schema.slice(1).map((_, j) =>
530
+ ['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
531
+ ['local.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
532
+ }
533
+ }
534
+ }
535
+ return result.length === 0 ? null : result.length === 1 ? result[0] : result
536
+ }
537
+
538
+ /**
539
+ * Build an array from items, handling ['__spread', expr] markers.
540
+ * Split into sections (normal arrays and spreads), then copy all into result.
541
+ */
542
+ export function buildArrayWithSpreads(items) {
543
+ const spreads = []
544
+ for (let i = 0; i < items.length; i++) {
545
+ if (Array.isArray(items[i]) && items[i][0] === '__spread') {
546
+ spreads.push({ pos: i, expr: items[i][1] })
547
+ }
548
+ }
549
+
550
+ if (spreads.length === 0) {
551
+ return emit(['[', ...items])
552
+ }
553
+
554
+ const sections = []
555
+ let currentArray = []
556
+
557
+ for (let i = 0; i < items.length; i++) {
558
+ if (Array.isArray(items[i]) && items[i][0] === '__spread') {
559
+ if (currentArray.length > 0) {
560
+ sections.push({ type: 'array', items: currentArray })
561
+ currentArray = []
562
+ }
563
+ sections.push({ type: 'spread', expr: items[i][1] })
564
+ } else {
565
+ currentArray.push(items[i])
566
+ }
567
+ }
568
+ if (currentArray.length > 0) {
569
+ sections.push({ type: 'array', items: currentArray })
570
+ }
571
+
572
+ if (sections.length === 1) {
573
+ const sec = sections[0]
574
+ return emit(sec.type === 'array' ? ['[', ...sec.items] : sec.expr)
575
+ }
576
+
577
+ const len = tempI32('len')
578
+ const pos = tempI32('pos')
579
+ const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
580
+ const result = out.local
581
+
582
+ const ir = [
583
+ ['local.set', `$${len}`, ['i32.const', 0]],
584
+ ]
585
+
586
+ inc('__len', '__ptr_offset')
587
+ for (const sec of sections) {
588
+ if (sec.type === 'spread') {
589
+ sec.local = `${T}sp${ctx.func.uniq++}`
590
+ ctx.func.locals.set(sec.local, 'f64')
591
+ sec.lenLocal = `${T}spl${ctx.func.uniq++}`
592
+ ctx.func.locals.set(sec.lenLocal, 'i32')
593
+ sec.vt = valTypeOf(sec.expr)
594
+ // ARRAY-known source: hoist data base and inline len/load (skip per-iter dispatch).
595
+ if (sec.vt === VAL.ARRAY && !multiCount(sec.expr)) {
596
+ sec.baseLocal = `${T}spb${ctx.func.uniq++}`
597
+ ctx.func.locals.set(sec.baseLocal, 'i32')
598
+ }
599
+ const n = multiCount(sec.expr)
600
+ ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
601
+ if (sec.baseLocal) {
602
+ ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['local.get', `$${sec.local}`]]])
603
+ ir.push(['local.set', `$${sec.lenLocal}`, ['i32.load', ['i32.sub', ['local.get', `$${sec.baseLocal}`], ['i32.const', 8]]]])
604
+ } else {
605
+ // Cache __len once per spread; reused below for total-len sum and inner copy bound.
606
+ ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['local.get', `$${sec.local}`]]])
607
+ }
608
+ }
609
+ }
610
+
611
+ for (const sec of sections) {
612
+ if (sec.type === 'array') {
613
+ ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
614
+ } else {
615
+ ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['local.get', `$${sec.lenLocal}`]]])
616
+ }
617
+ }
618
+
619
+ ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
620
+
621
+ for (const sec of sections) {
622
+ if (sec.type === 'array') {
623
+ for (let i = 0; i < sec.items.length; i++) {
624
+ ir.push(
625
+ ['f64.store',
626
+ ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
627
+ asF64(emit(sec.items[i]))],
628
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
629
+ )
630
+ }
631
+ } else {
632
+ const slen = sec.lenLocal, sidx = `${T}sidx${ctx.func.uniq++}`
633
+ ctx.func.locals.set(sidx, 'i32')
634
+ const loopId = ctx.func.uniq++
635
+ const elemLoad = sec.baseLocal
636
+ ? ['f64.load', ['i32.add', ['local.get', `$${sec.baseLocal}`], ['i32.shl', ['local.get', `$${sidx}`], ['i32.const', 3]]]]
637
+ : ctx.module.modules['string']
638
+ ? ['if', ['result', 'f64'],
639
+ ['i32.or',
640
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.STRING]],
641
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.SSO]]],
642
+ ['then', (inc('__str_idx'), ['call', '$__str_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])],
643
+ ['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])]]
644
+ : (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])
645
+ ir.push(
646
+ ['local.set', `$${sidx}`, ['i32.const', 0]],
647
+ ['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
648
+ ['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${slen}`]]],
649
+ ['f64.store',
650
+ ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
651
+ elemLoad],
652
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
653
+ ['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
654
+ ['br', `$loop${loopId}`]]]
655
+ )
656
+ }
657
+ }
658
+
659
+ ir.push(out.ptr)
660
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
661
+ }
662
+
663
+ /** Check if node is a block body (statement list, not object literal/expression) */
664
+ const isBlockBody = n => Array.isArray(n) && n[0] === '{}' && n.length === 2 && Array.isArray(n[1]) && STMT_OPS.has(n[1]?.[0])
665
+
666
+ /** Emit node in void context: emit + drop any value. Block bodies route through emitBody. */
667
+ export function emitFlat(node) {
668
+ if (isBlockBody(node)) return emitBody(node)
669
+ const ir = emit(node, 'void')
670
+ const items = flat(ir)
671
+ if (ir?.type && ir.type !== 'void') items.push('drop')
672
+ return items
673
+ }
674
+
675
+ /** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitFlat per statement.
676
+ * Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
677
+ * rest of the enclosing block. Refinements added here are rolled back on block exit. */
678
+ export function emitBody(node) {
679
+ const inner = node[1]
680
+ const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
681
+ const out = []
682
+ const accumulated = []
683
+ for (let i = 0; i < stmts.length; i++) {
684
+ const s = stmts[i]
685
+ if (s == null || typeof s === 'number') continue
686
+ out.push(...emitFlat(s))
687
+ // After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
688
+ // Skip names that are reassigned later — refinement would be unsound past the assignment.
689
+ if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
690
+ const refs = extractRefinements(s[1], new Map(), false)
691
+ for (const [name, val] of refs) {
692
+ let reassigned = false
693
+ for (let j = i + 1; j < stmts.length; j++)
694
+ if (isReassigned(stmts[j], name)) { reassigned = true; break }
695
+ if (reassigned) continue
696
+ accumulated.push([name, ctx.func.refinements.get(name)])
697
+ ctx.func.refinements.set(name, val)
698
+ }
699
+ }
700
+ }
701
+ // Restore prior refinements on block exit.
702
+ for (let i = accumulated.length - 1; i >= 0; i--) {
703
+ const [name, prev] = accumulated[i]
704
+ if (prev === undefined) ctx.func.refinements.delete(name); else ctx.func.refinements.set(name, prev)
705
+ }
706
+ return out
707
+ }
708
+
709
+ /** Comparison op factory with constant folding. */
710
+ const cmpOp = (i32op, f64op, fn) => (a, b) => {
711
+ const va = emit(a), vb = emit(b)
712
+ if (isLit(va) && isLit(vb)) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
713
+ const ai = intConstValue(a), bi = intConstValue(b)
714
+ if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
715
+ if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
716
+ return va.type === 'i32' && vb.type === 'i32'
717
+ ? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
718
+ }
719
+
720
+ function intConstValue(expr) {
721
+ if (typeof expr === 'number' && Number.isInteger(expr)) return expr
722
+ if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
723
+ if (typeof expr === 'string') {
724
+ const v = repOf(expr)?.intConst
725
+ if (v != null) return v
726
+ }
727
+ return null
728
+ }
729
+
730
+ /** Compound assignment: read → op → write back (via readVar/writeVar). */
731
+ function compoundAssign(name, val, f64op, i32op) {
732
+ if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
733
+ const void_ = _expect === 'void'
734
+ const va = readVar(name), vb = emit(val)
735
+ if (i32op && va.type === 'i32' && vb.type === 'i32')
736
+ return writeVar(name, i32op(va, vb), void_)
737
+ return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
738
+ }
739
+
740
+ /**
741
+ * Core emitter table. Maps AST ops to WASM IR generators.
742
+ * ctx.core.emit is seeded with a flat copy of this object on reset;
743
+ * modules add or override ops on ctx.core.emit directly.
744
+ * @type {Record<string, (...args: any[]) => Array>}
745
+ */
746
+ export const emitter = {
747
+ // === Spread operator ===
748
+ // Note: spread is handled specially in call contexts; this catches stray uses
749
+ '...': () => err('Spread (...) can only be used in function/method calls or array literals'),
750
+
751
+ // === Statements ===
752
+
753
+ ';': (...args) => {
754
+ const out = []
755
+ for (const a of args) {
756
+ const r = emit(a, 'void')
757
+ if (r == null) continue
758
+ out.push(...flat(r))
759
+ if (r?.type && r.type !== 'void') out.push('drop')
760
+ }
761
+ return out
762
+ },
763
+ '{': (...args) => args.map(emit).filter(x => x != null),
764
+ ',': (...args) => {
765
+ const results = args.map(emit).filter(x => x != null)
766
+ if (results.length === 0) return null
767
+ if (results.length === 1) return results[0]
768
+ const last = results[results.length - 1]
769
+ // Flatten: multi-instruction arrays (from ';') need spreading, typed nodes need drop
770
+ const spread = r => Array.isArray(r) && Array.isArray(r[0]) ? r : [r]
771
+ const dropSpread = r => r.type ? [['drop', r]] : spread(r)
772
+ // If last expression is void (store, etc.), add explicit return value
773
+ if (!last.type) {
774
+ return typed(['block', ['result', 'f64'],
775
+ ...results.flatMap(dropSpread),
776
+ ['f64.const', 0]], 'f64')
777
+ }
778
+ return typed(['block', ['result', last.type],
779
+ ...results.slice(0, -1).flatMap(dropSpread), last], last.type)
780
+ },
781
+ 'let': emitDecl,
782
+ 'const': emitDecl,
783
+ 'export': () => null,
784
+ // 'block' can appear from jzify transforming labeled blocks or as WASM block IR
785
+ 'block': (...args) => {
786
+ // WASM block IR: first arg is ['result', type] → pass through, preserve type
787
+ if (Array.isArray(args[0]) && args[0][0] === 'result')
788
+ return typed(['block', ...args], args[0][1])
789
+ const inner = args.length === 1 ? args[0] : [';', ...args]
790
+ return emitFlat(['{}', inner])
791
+ },
792
+
793
+ 'throw': expr => {
794
+ ctx.runtime.throws = true
795
+ const thrown = temp()
796
+ return typed(['block',
797
+ ['local.set', `$${thrown}`, asF64(emit(expr))],
798
+ ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${thrown}`]]],
799
+ ['throw', '$__jz_err', ['local.get', `$${thrown}`]]], 'void')
800
+ },
801
+
802
+ 'catch': (body, errName, handler) => {
803
+ ctx.runtime.throws = true
804
+ const id = ctx.func.uniq++
805
+ ctx.func.locals.set(errName, 'f64')
806
+ const prev = ctx.func.inTry; ctx.func.inTry = true
807
+ let bodyIR; try { bodyIR = emitFlat(body) } finally { ctx.func.inTry = prev }
808
+ const handlerIR = emitFlat(handler)
809
+ return typed(['block', `$outer${id}`, ['result', 'f64'],
810
+ ['block', `$catch${id}`, ['result', 'f64'],
811
+ ['try_table', ['catch', '$__jz_err', `$catch${id}`],
812
+ ...bodyIR],
813
+ ['f64.const', 0],
814
+ ['br', `$outer${id}`]],
815
+ ['local.set', `$${errName}`],
816
+ ...handlerIR,
817
+ ['f64.const', 0]], 'f64')
818
+ },
819
+
820
+ 'finally': (body, cleanup) => {
821
+ ctx.runtime.throws = true
822
+ const id = ctx.func.uniq++
823
+ const errLocal = temp('err')
824
+ const parentStack = ctx.func.finallyStack || []
825
+ const activeStack = parentStack.concat([cleanup])
826
+
827
+ const prevTry = ctx.func.inTry
828
+ ctx.func.inTry = true
829
+ const bodyIR = withFinallyStack(activeStack, () => {
830
+ try { return emitFlat(body) }
831
+ finally { ctx.func.inTry = prevTry }
832
+ })
833
+ const normalCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
834
+ const throwCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
835
+
836
+ return ['block', `$fin_done${id}`,
837
+ ['block', `$fin_catch${id}`, ['result', 'f64'],
838
+ ['try_table', ['catch', '$__jz_err', `$fin_catch${id}`],
839
+ ...bodyIR],
840
+ ...normalCleanup,
841
+ ['br', `$fin_done${id}`]],
842
+ ['local.set', `$${errLocal}`],
843
+ ...throwCleanup,
844
+ ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${errLocal}`]]],
845
+ ['throw', '$__jz_err', ['local.get', `$${errLocal}`]]]
846
+ },
847
+
848
+ 'return': expr => {
849
+ const finalizers = emitFinalizers()
850
+ if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
851
+ const vals = expr.slice(1).map(e => asF64(emit(e)))
852
+ if (finalizers.length === 0) return typed(['return', ...vals], 'void')
853
+ const names = vals.map(() => temp('ret'))
854
+ return [
855
+ ...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
856
+ ...finalizers,
857
+ typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
858
+ ]
859
+ }
860
+ if (expr == null) return [...finalizers, typed(['return', NULL_IR], 'void')]
861
+ const rt = ctx.func.current?.results[0] || 'f64'
862
+ const pk = ctx.func.current?.ptrKind
863
+ const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
864
+ if (!ctx.func.inTry && !ctx.transform.noTailCall &&
865
+ Array.isArray(ir) && ir[0] === 'call' && typeof ir[1] === 'string')
866
+ return typed(['return_call', ...ir.slice(1)], 'void')
867
+ if (finalizers.length > 0) {
868
+ const ty = pk != null ? 'i32' : rt
869
+ const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
870
+ return [
871
+ ['local.set', `$${name}`, ir],
872
+ ...finalizers,
873
+ typed(['return', ['local.get', `$${name}`]], 'void'),
874
+ ]
875
+ }
876
+ return typed(['return', ir], 'void')
877
+ },
878
+
879
+ // === Assignment ===
880
+
881
+ '=': (name, val) => {
882
+ if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
883
+ // Array index assignment: arr[i] = x
884
+ if (Array.isArray(name) && name[0] === '[]') {
885
+ const [, arr, idx] = name
886
+ const keyType = keyValType(idx)
887
+ const useRuntimeKeyDispatch = keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING)
888
+ const keyExpr = asF64(emit(idx))
889
+ const valueExpr = asF64(emit(val))
890
+ const storeArrayValue = (arrExpr, idxNode, persist) => {
891
+ const arrTmp = `${T}asi${ctx.func.uniq++}`
892
+ const idxTmp = `${T}asj${ctx.func.uniq++}`
893
+ const valTmp = `${T}asv${ctx.func.uniq++}`
894
+ ctx.func.locals.set(arrTmp, 'f64')
895
+ ctx.func.locals.set(idxTmp, 'i32')
896
+ ctx.func.locals.set(valTmp, 'f64')
897
+ inc('__arr_set_idx_ptr')
898
+ const body = [
899
+ ['local.set', `$${arrTmp}`, arrExpr],
900
+ ['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
901
+ ['local.set', `$${valTmp}`, valueExpr],
902
+ ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${arrTmp}`], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
903
+ ]
904
+ if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
905
+ body.push(['local.get', `$${valTmp}`])
906
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
907
+ }
908
+ const setDyn = () => {
909
+ inc('__dyn_set')
910
+ return typed(['call', '$__dyn_set', asF64(emit(arr)), keyExpr, valueExpr], 'f64')
911
+ }
912
+ const dispatchKey = (numericIR) => {
913
+ const keyTmp = temp()
914
+ return typed(['block', ['result', 'f64'],
915
+ ['local.set', `$${keyTmp}`, keyExpr],
916
+ ['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
917
+ ['then', ['call', '$__dyn_set', asF64(emit(arr)), ['local.get', `$${keyTmp}`], valueExpr]],
918
+ ['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
919
+ }
920
+ // Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
921
+ const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1] : null
922
+ if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
923
+ const slot = ctx.schema.find(arr, litKey, true)
924
+ if (slot >= 0) {
925
+ const t = temp()
926
+ return typed(['block', ['result', 'f64'],
927
+ ['local.set', `$${t}`, valueExpr],
928
+ ['f64.store',
929
+ ['i32.add', ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), ['i32.const', slot * 8]],
930
+ ['local.get', `$${t}`]],
931
+ ['local.get', `$${t}`]], 'f64')
932
+ }
933
+ }
934
+ if (keyType === VAL.STRING) return setDyn()
935
+ if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
936
+ lookupValType(arr) === 'typed') {
937
+ const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val)
938
+ if (r) return r
939
+ }
940
+ if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
941
+ const inner = ctx.schema.emitInner(arr)
942
+ const arrVT = lookupValType(arr) || VAL.OBJECT
943
+ const storeNumeric = keyNode => storeArrayValue(inner, keyNode, ptr =>
944
+ ['f64.store', ptrOffsetIR(asF64(emit(arr)), arrVT), ptr])
945
+ if (useRuntimeKeyDispatch) {
946
+ inc('__dyn_set', '__is_str_key')
947
+ return dispatchKey(storeNumeric)
948
+ }
949
+ return typed(storeNumeric(keyExpr), 'f64')
950
+ }
951
+ const va = emit(arr), vi = asI32(emit(idx)), vv = valueExpr, t = temp()
952
+ if (typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
953
+ const persist = ptr => {
954
+ if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
955
+ if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
956
+ return ['local.set', `$${arr}`, ptr]
957
+ }
958
+ if (useRuntimeKeyDispatch) {
959
+ inc('__dyn_set', '__is_str_key')
960
+ return dispatchKey(keyNode => storeArrayValue(asF64(va), keyNode, persist))
961
+ }
962
+ return storeArrayValue(asF64(va), keyExpr, persist)
963
+ }
964
+ // arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
965
+ const arrVT = (typeof arr === 'string' ? lookupValType(arr) : null) || VAL.OBJECT
966
+ if (useRuntimeKeyDispatch) {
967
+ inc('__dyn_set', '__is_str_key')
968
+ return dispatchKey(keyNode => {
969
+ const keyI32 = asI32(typed(keyNode, 'f64'))
970
+ return ['block', ['result', 'f64'],
971
+ ['local.set', `$${t}`, vv],
972
+ ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', keyI32, ['i32.const', 3]]], ['local.get', `$${t}`]],
973
+ ['local.get', `$${t}`]]
974
+ })
975
+ }
976
+ return typed(['block', ['result', 'f64'],
977
+ ['local.set', `$${t}`, vv],
978
+ ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
979
+ ['local.get', `$${t}`]], 'f64')
980
+ }
981
+ // Object property assignment: obj.prop = x
982
+ if (Array.isArray(name) && name[0] === '.') {
983
+ const [, obj, prop] = name
984
+ // Schema-based object → f64.store at fixed offset.
985
+ // safe=true: skip structural subtyping when variable's type is unknown,
986
+ // otherwise a slot write could clobber an array/string's payload.
987
+ if (typeof obj === 'string' && ctx.schema.find) {
988
+ const idx = ctx.schema.find(obj, prop, true)
989
+ if (idx >= 0) {
990
+ const va = emit(obj), vv = asF64(emit(val)), t = temp()
991
+ const shadow = needsDynShadow(obj)
992
+ if (shadow) inc('__dyn_set')
993
+ const stmts = [
994
+ ['local.set', `$${t}`, vv],
995
+ ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]], ['local.get', `$${t}`]],
996
+ ]
997
+ if (shadow)
998
+ stmts.push(['drop', ['call', '$__dyn_set', asF64(va), asF64(emit(['str', prop])), ['local.get', `$${t}`]]])
999
+ stmts.push(['local.get', `$${t}`])
1000
+ return typed(['block', ['result', 'f64'], ...stmts], 'f64')
1001
+ }
1002
+ }
1003
+ if (typeof obj === 'string') {
1004
+ const objType = keyValType(obj)
1005
+ if (usesDynProps(objType)) {
1006
+ inc('__dyn_set')
1007
+ return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1008
+ }
1009
+ if (objType == null) ctx.features.external = true
1010
+ inc('__hash_set')
1011
+ const setCall = typed(['call', '$__hash_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1012
+ if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
1013
+ ['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
1014
+ return typed(['local.tee', `$${obj}`, setCall], 'f64')
1015
+ }
1016
+ ctx.features.external = true
1017
+ inc('__dyn_set')
1018
+ return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1019
+ }
1020
+ if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
1021
+ const void_ = _expect === 'void'
1022
+ return writeVar(name, emit(val), void_)
1023
+ },
1024
+
1025
+ // Compound assignments: read-modify-write with type coercion
1026
+ '+=': (name, val) => {
1027
+ // String concatenation: desugar to name = name + val (+ handler knows about strings)
1028
+ const vt = typeof name === 'string' ? keyValType(name) : null
1029
+ const vtB = keyValType(val)
1030
+ if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
1031
+ return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
1032
+ },
1033
+ ...Object.fromEntries([
1034
+ ['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
1035
+ ].map(([op, fn]) => [op, (name, val) => compoundAssign(name, val,
1036
+ (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1037
+ fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1038
+ )])),
1039
+ '%=': (name, val) => compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32')),
1040
+
1041
+ // Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
1042
+ ...Object.fromEntries([
1043
+ ['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
1044
+ ['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
1045
+ ].map(([op, fn]) => [op, (name, val) => {
1046
+ if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
1047
+ const void_ = _expect === 'void'
1048
+ const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
1049
+ return writeVar(name, result, void_)
1050
+ }
1051
+ return compoundAssign(name, val,
1052
+ (a, b) => asF64(typed([`i32.${fn}`, toI32(a), toI32(b)], 'i32')),
1053
+ (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1054
+ )
1055
+ }])),
1056
+
1057
+ // Logical compound assignments: a ||= b → a = a || b, a &&= b → a = a && b
1058
+ // Logical/nullish compound assignments: read → check → conditionally write
1059
+ // For complex LHS (obj.prop, arr[i]): emit as check(read(lhs)) ? write(lhs, val) : read(lhs)
1060
+ ...Object.fromEntries(['||=', '&&=', '??='].map(op => [op, (name, val) => {
1061
+ // Complex LHS → desugar (side-effect-safe since obj/arr/idx are locals)
1062
+ if (typeof name !== 'string') {
1063
+ const baseOp = op.slice(0, -1) // '||', '&&', '??'
1064
+ return emit([baseOp, name, ['=', name, val]])
1065
+ }
1066
+ if (isConst(name)) err(`Assignment to const '${name}'`)
1067
+ const void_ = _expect === 'void'
1068
+ const t = temp()
1069
+ const va = readVar(name)
1070
+ // Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
1071
+ const cond = op === '??='
1072
+ ? isNullish(['local.tee', `$${t}`, asF64(va)])
1073
+ : ['i32.and',
1074
+ ['f64.eq', ['local.tee', `$${t}`, asF64(va)], ['local.get', `$${t}`]],
1075
+ ['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]]]
1076
+ // &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
1077
+ const [thenExpr, elseExpr] = op === '||='
1078
+ ? [['local.get', `$${t}`], asF64(emit(val))]
1079
+ : [asF64(emit(val)), ['local.get', `$${t}`]]
1080
+ const result = typed(['if', ['result', 'f64'], cond, ['then', thenExpr], ['else', elseExpr]], 'f64')
1081
+ // Write back (handles boxed/global/local)
1082
+ if (ctx.func.boxed?.has(name)) {
1083
+ const bt = temp()
1084
+ return typed(['block', ['result', 'f64'],
1085
+ ['local.set', `$${bt}`, result],
1086
+ ['f64.store', boxedAddr(name), ['local.get', `$${bt}`]],
1087
+ ['local.get', `$${bt}`]], 'f64')
1088
+ }
1089
+ return writeVar(name, result, void_)
1090
+ }])),
1091
+
1092
+ // === Increment/Decrement ===
1093
+ // Postfix resolved in prepare: i++ → (++i) - 1
1094
+
1095
+ ...Object.fromEntries([['++', 'add'], ['--', 'sub']].map(([op, fn]) => [op, name => {
1096
+ if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1097
+ const void_ = _expect === 'void'
1098
+ const v = readVar(name)
1099
+ const one = v.type === 'i32' ? ['i32.const', 1] : ['f64.const', 1]
1100
+ return writeVar(name, typed([`${v.type}.${fn}`, v, one], v.type), void_)
1101
+ }])),
1102
+
1103
+ // === Arithmetic (type-preserving) ===
1104
+
1105
+ // Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
1106
+ '+': (a, b) => {
1107
+ if (_expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
1108
+ // String concatenation: pure string operands skip generic ToString coercion.
1109
+ const vtA = keyValType(a)
1110
+ const vtB = keyValType(b)
1111
+ if (vtA === VAL.STRING && vtB === VAL.STRING) {
1112
+ inc('__str_concat_raw')
1113
+ return typed(['call', '$__str_concat_raw', asF64(emit(a)), asF64(emit(b))], 'f64')
1114
+ }
1115
+ if (vtA === VAL.STRING || vtB === VAL.STRING) {
1116
+ inc('__str_concat')
1117
+ return typed(['call', '$__str_concat', asF64(emit(a)), asF64(emit(b))], 'f64')
1118
+ }
1119
+ if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
1120
+ return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
1121
+ // Runtime string dispatch when at least one side could be a string. When one side has
1122
+ // a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
1123
+ // chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
1124
+ // operand needs the runtime check.
1125
+ if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
1126
+ const tA = temp('add'), tB = temp('add')
1127
+ inc('__str_concat', '__is_str_key')
1128
+ const checkA = vtA == null ? ['call', '$__is_str_key', ['local.tee', `$${tA}`, asF64(emit(a))]] : null
1129
+ const checkB = vtB == null ? ['call', '$__is_str_key', ['local.tee', `$${tB}`, asF64(emit(b))]] : null
1130
+ const concat = ['call', '$__str_concat', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1131
+ const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1132
+ if (checkA && checkB) {
1133
+ return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
1134
+ }
1135
+ // Exactly one side is checked. Pre-eval the known side first, then the if branches on the unknown.
1136
+ const preEval = vtA == null ? ['local.set', `$${tB}`, asF64(emit(b))] : ['local.set', `$${tA}`, asF64(emit(a))]
1137
+ return typed(['block', ['result', 'f64'],
1138
+ preEval,
1139
+ ['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
1140
+ ], 'f64')
1141
+ }
1142
+ const va = emit(a), vb = emit(b)
1143
+ if (isLit(va) && isLit(vb)) return emitNum(litVal(va) + litVal(vb))
1144
+ if (isLit(vb) && litVal(vb) === 0) return va
1145
+ if (isLit(va) && litVal(va) === 0) return vb
1146
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.add', va, vb], 'i32')
1147
+ return typed(['f64.add', asF64(va), asF64(vb)], 'f64')
1148
+ },
1149
+ '-': (a, b) => {
1150
+ if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
1151
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1152
+ return b === undefined
1153
+ ? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1154
+ : fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
1155
+ if (b === undefined) { const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64') }
1156
+ const va = emit(a), vb = emit(b)
1157
+ if (isLit(va) && isLit(vb)) return emitNum(litVal(va) - litVal(vb))
1158
+ if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1159
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.sub', va, vb], 'i32')
1160
+ return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1161
+ },
1162
+ 'u+': a => {
1163
+ if (valTypeOf(a) === VAL.BIGINT)
1164
+ return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
1165
+ inc('__to_num')
1166
+ return typed(['call', '$__to_num', asF64(emit(a))], 'f64')
1167
+ },
1168
+ 'u-': a => {
1169
+ if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1170
+ const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64')
1171
+ },
1172
+ '*': (a, b) => {
1173
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1174
+ return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
1175
+ const va = emit(a), vb = emit(b)
1176
+ if (isLit(va) && isLit(vb)) return emitNum(litVal(va) * litVal(vb))
1177
+ if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1178
+ if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
1179
+ if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
1180
+ if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
1181
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.mul', va, vb], 'i32')
1182
+ return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1183
+ },
1184
+ '/': (a, b) => {
1185
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1186
+ return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
1187
+ const va = emit(a), vb = emit(b)
1188
+ if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) / litVal(vb))
1189
+ if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1190
+ return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1191
+ },
1192
+ '%': (a, b) => {
1193
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1194
+ return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
1195
+ const va = emit(a), vb = emit(b)
1196
+ if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) % litVal(vb))
1197
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.rem_s', va, vb], 'i32')
1198
+ return f64rem(toNumF64(a, va), toNumF64(b, vb))
1199
+ },
1200
+
1201
+ // === Comparisons (always i32 result) ===
1202
+
1203
+ '==': (a, b) => {
1204
+ // JS loose nullish equality: x == null / x == undefined.
1205
+ // If the non-literal side has a known non-null VAL type, fold to 0.
1206
+ if (isNullishLit(a)) {
1207
+ if (valTypeOf(b)) return emitNum(0)
1208
+ return isNullish(asF64(emit(b)))
1209
+ }
1210
+ if (isNullishLit(b)) {
1211
+ if (valTypeOf(a)) return emitNum(0)
1212
+ return isNullish(asF64(emit(a)))
1213
+ }
1214
+ // typeof x == 'string' → compile-time type check (prepare rewrites string to type code)
1215
+ const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
1216
+ const va = emit(a), vb = emit(b)
1217
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
1218
+ // Both sides known-pure NUMBER → f64.eq (skip __eq's pointer-identity/string path).
1219
+ // valTypeOf handles literals/arithmetic exprs; lookupValType covers typed locals/params.
1220
+ const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1221
+ const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
1222
+ if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1223
+ // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1224
+ // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
1225
+ // STRING needs __eq (heap strings can be equal by content but different pointers).
1226
+ // BIGINT needs __eq (heap-allocated, content compare).
1227
+ if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1228
+ return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1229
+ }
1230
+ inc('__eq')
1231
+ return typed(['call', '$__eq', asF64(va), asF64(vb)], 'i32')
1232
+ },
1233
+ '!=': (a, b) => {
1234
+ if (isNullishLit(a)) {
1235
+ if (valTypeOf(b)) return emitNum(1)
1236
+ return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
1237
+ }
1238
+ if (isNullishLit(b)) {
1239
+ if (valTypeOf(a)) return emitNum(1)
1240
+ return typed(['i32.eqz', isNullish(asF64(emit(a)))], 'i32')
1241
+ }
1242
+ const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
1243
+ const va = emit(a), vb = emit(b)
1244
+ if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1245
+ const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1246
+ const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
1247
+ if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1248
+ if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1249
+ return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1250
+ }
1251
+ inc('__eq')
1252
+ return typed(['i32.eqz', ['call', '$__eq', asF64(va), asF64(vb)]], 'i32')
1253
+ },
1254
+ '<': cmpOp('lt_s', 'lt', (a, b) => a < b),
1255
+ '>': cmpOp('gt_s', 'gt', (a, b) => a > b),
1256
+ '<=': cmpOp('le_s', 'le', (a, b) => a <= b),
1257
+ '>=': cmpOp('ge_s', 'ge', (a, b) => a >= b),
1258
+
1259
+ // === Logical ===
1260
+
1261
+ '!': a => {
1262
+ const v = emit(a)
1263
+ if (v.type === 'i32') return typed(['i32.eqz', v], 'i32')
1264
+ // Unboxed pointer offsets: falsy iff zero offset.
1265
+ if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
1266
+ // Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
1267
+ // Pointers are never 0 / NaN / false / empty-string in the boxed form.
1268
+ const vt = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1269
+ if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT) {
1270
+ return isNullish(asF64(v))
1271
+ }
1272
+ inc('__is_truthy')
1273
+ return typed(['i32.eqz', ['call', '$__is_truthy', asF64(v)]], 'i32')
1274
+ },
1275
+
1276
+ '?:': (a, b, c) => {
1277
+ // Constant condition → emit only the live branch
1278
+ const ca = emit(a)
1279
+ if (isLit(ca)) { const v = litVal(ca); return (v !== 0 && v === v) ? emit(b) : emit(c) }
1280
+ const cond = toBoolFromEmitted(ca)
1281
+ // Flow-sensitive refinement: each arm sees narrowing consistent with `a` being truthy / falsy.
1282
+ const thenRefs = extractRefinements(a, new Map(), true)
1283
+ const elseRefs = extractRefinements(a, new Map(), false)
1284
+ const vb = withRefinements(thenRefs, b, () => emit(b))
1285
+ const vc = withRefinements(elseRefs, c, () => emit(c))
1286
+ // L: Use WASM select for pure ternaries — branchless, smaller bytecode
1287
+ if (vb.type === 'i32' && vc.type === 'i32') {
1288
+ // Propagate matching ptrKind/ptrAux so a downstream asF64 takes the NaN-rebox
1289
+ // path instead of `f64.convert_i32_s`. Mismatched kinds drop both — caller's
1290
+ // asF64 will treat the i32 as numeric, which is correct for non-pointer i32s.
1291
+ // ptrKind matches but ptrAux differs (e.g. polymorphic OBJECT with two
1292
+ // distinct schemaIds, or TYPED with two element types) — fall through to
1293
+ // the f64 path. There each arm reboxes independently, preserving its own
1294
+ // aux in the NaN-box. The single-i32 path can only carry one aux on the
1295
+ // result, so `boxPtrIR` would default to 0 and lose the runtime schema /
1296
+ // elemType bits needed by downstream lookups (e.g. __dyn_get's OBJECT-
1297
+ // schema fallback uses receiver aux to resolve `.prop`).
1298
+ const auxMismatch = vb.ptrKind != null && vb.ptrKind === vc.ptrKind
1299
+ && (vb.ptrAux ?? null) !== (vc.ptrAux ?? null)
1300
+ if (!auxMismatch) {
1301
+ const tagPtr = (n) => {
1302
+ if (vb.ptrKind != null && vb.ptrKind === vc.ptrKind) {
1303
+ n.ptrKind = vb.ptrKind
1304
+ if (vb.ptrAux != null && vb.ptrAux === vc.ptrAux) n.ptrAux = vb.ptrAux
1305
+ }
1306
+ return n
1307
+ }
1308
+ if (isPureIR(vb) && isPureIR(vc))
1309
+ return tagPtr(typed(['select', vb, vc, cond], 'i32'))
1310
+ return tagPtr(typed(['if', ['result', 'i32'], cond, ['then', vb], ['else', vc]], 'i32'))
1311
+ }
1312
+ }
1313
+ const fb = asF64(vb), fc = asF64(vc)
1314
+ if (isPureIR(fb) && isPureIR(fc))
1315
+ return typed(['select', fb, fc, cond], 'f64')
1316
+ return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
1317
+ },
1318
+
1319
+ '&&': (a, b) => {
1320
+ const va = emit(a)
1321
+ if (isLit(va)) { const v = litVal(va); return (v !== 0 && v === v) ? emit(b) : va }
1322
+ // i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
1323
+ // skip f64 round-trip and __is_truthy call entirely.
1324
+ if (va.type === 'i32') {
1325
+ const vb = emit(b)
1326
+ const t = tempI32()
1327
+ if (vb.type === 'i32') {
1328
+ return typed(['if', ['result', 'i32'],
1329
+ ['local.tee', `$${t}`, va],
1330
+ ['then', vb],
1331
+ ['else', ['local.get', `$${t}`]]], 'i32')
1332
+ }
1333
+ return typed(['if', ['result', 'f64'],
1334
+ ['local.tee', `$${t}`, va],
1335
+ ['then', asF64(vb)],
1336
+ ['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
1337
+ }
1338
+ const t = temp()
1339
+ const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1340
+ return typed(['if', ['result', 'f64'],
1341
+ toBoolFromEmitted(teed),
1342
+ ['then', asF64(emit(b))],
1343
+ ['else', ['local.get', `$${t}`]]], 'f64')
1344
+ },
1345
+
1346
+ '||': (a, b) => {
1347
+ const va = emit(a)
1348
+ if (isLit(va)) { const v = litVal(va); return (v !== 0 && v === v) ? va : emit(b) }
1349
+ if (va.type === 'i32') {
1350
+ const vb = emit(b)
1351
+ const t = tempI32()
1352
+ if (vb.type === 'i32') {
1353
+ return typed(['if', ['result', 'i32'],
1354
+ ['local.tee', `$${t}`, va],
1355
+ ['then', ['local.get', `$${t}`]],
1356
+ ['else', vb]], 'i32')
1357
+ }
1358
+ return typed(['if', ['result', 'f64'],
1359
+ ['local.tee', `$${t}`, va],
1360
+ ['then', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')],
1361
+ ['else', asF64(vb)]], 'f64')
1362
+ }
1363
+ const t = temp()
1364
+ const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1365
+ return typed(['if', ['result', 'f64'],
1366
+ toBoolFromEmitted(teed),
1367
+ ['then', ['local.get', `$${t}`]],
1368
+ ['else', asF64(emit(b))]], 'f64')
1369
+ },
1370
+
1371
+ // a ?? b: returns b only if a is nullish
1372
+ '??': (a, b) => {
1373
+ const va = emit(a)
1374
+ const t = temp()
1375
+ return typed(['if', ['result', 'f64'],
1376
+ // Check: is a NOT nullish?
1377
+ ['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
1378
+ ['then', ['local.get', `$${t}`]],
1379
+ ['else', asF64(emit(b))]], 'f64')
1380
+ },
1381
+
1382
+ 'void': a => {
1383
+ const v = emit(a)
1384
+ const dropAndUndef = (instr) => typed(['block', ['result', 'f64'], instr, 'drop', undefExpr()], 'f64')
1385
+ if (v == null) return undefExpr()
1386
+ const op = Array.isArray(v) ? v[0] : null
1387
+ const wasmVoid = op === 'local.set' || (typeof op === 'string' && op.endsWith('.store'))
1388
+ || op === 'memory.copy' || op === 'global.set'
1389
+ if (wasmVoid)
1390
+ return typed(['block', ['result', 'f64'], v, undefExpr()], 'f64')
1391
+ if (v.type && v.type !== 'void')
1392
+ return dropAndUndef(v)
1393
+ return typed(['block', ['result', 'f64'], ...flat(v), undefExpr()], 'f64')
1394
+ },
1395
+
1396
+ '(': a => emit(a),
1397
+
1398
+ // === Bitwise (i32 for numbers, i64 for BigInt) ===
1399
+
1400
+ '~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v), typed(['i32.const', -1], 'i32')], 'i32') },
1401
+ ...Object.fromEntries([
1402
+ ['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
1403
+ ].map(([op, fn]) => [op, (a, b) => {
1404
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1405
+ return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
1406
+ const va = emit(a), vb = emit(b)
1407
+ if (isLit(va) && isLit(vb)) {
1408
+ const la = litVal(va), lb = litVal(vb)
1409
+ if (op === '&') return emitNum(la & lb); if (op === '|') return emitNum(la | lb)
1410
+ if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
1411
+ if (op === '>>') return emitNum(la >> lb)
1412
+ }
1413
+ return typed([`i32.${fn}`, toI32(va), toI32(vb)], 'i32')
1414
+ }])),
1415
+ '>>>': (a, b) => {
1416
+ const va = emit(a), vb = emit(b)
1417
+ if (isLit(va) && isLit(vb)) return emitNum(litVal(va) >>> litVal(vb))
1418
+ // F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
1419
+ // [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
1420
+ // signed for negative-high-bit s values, flipping sign and breaking the
1421
+ // canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
1422
+ const node = typed(['i32.shr_u', toI32(va), toI32(vb)], 'i32')
1423
+ node.unsigned = true
1424
+ return node
1425
+ },
1426
+
1427
+ // === Control flow ===
1428
+
1429
+ 'if': (cond, then, els) => {
1430
+ // Dead branch elimination: constant condition → emit only the live branch
1431
+ const ce = emit(cond)
1432
+ if (isLit(ce)) {
1433
+ const v = litVal(ce), truthy = v !== 0 && v === v
1434
+ if (truthy) return emitFlat(then)
1435
+ if (els != null) return emitFlat(els)
1436
+ return null
1437
+ }
1438
+ const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
1439
+ // Flow-sensitive type refinement: narrow types within each branch based on the guard.
1440
+ const thenRefs = extractRefinements(cond, new Map(), true)
1441
+ const elseRefs = extractRefinements(cond, new Map(), false)
1442
+ const thenBody = withRefinements(thenRefs, then, () => emitFlat(then))
1443
+ if (els != null) {
1444
+ const elseBody = withRefinements(elseRefs, els, () => emitFlat(els))
1445
+ return ['if', c, ['then', ...thenBody], ['else', ...elseBody]]
1446
+ }
1447
+ return ['if', c, ['then', ...thenBody]]
1448
+ },
1449
+
1450
+ 'for': (init, cond, step, body) => {
1451
+ if (body === undefined) return err('for-in/for-of not supported')
1452
+ if (!ctx.transform.optimize || ctx.transform.optimize.smallConstForUnroll !== false) {
1453
+ const unrolled = unrollSmallConstFor(init, cond, step, body)
1454
+ if (unrolled) return unrolled
1455
+ }
1456
+ const id = ctx.func.uniq++
1457
+ const brk = `$brk${id}`, loop = `$loop${id}`
1458
+ // The cont wrapper is only needed if the body has a `continue` AND there is a step
1459
+ // expression — `continue` must jump to before the step. Without a step, `continue`
1460
+ // can target the loop label directly, saving a redundant `block`.
1461
+ const needsCont = step && hasOwnContinue(body)
1462
+ const cont = needsCont ? `$cont${id}` : loop
1463
+ ctx.func.stack.push({ brk, loop: cont })
1464
+ const result = []
1465
+ if (init != null) result.push(...emitFlat(init))
1466
+ const loopBody = []
1467
+ if (cond) loopBody.push(['br_if', brk, ['i32.eqz', toBool(cond)]])
1468
+ if (needsCont) loopBody.push(['block', cont, ...emitFlat(body)])
1469
+ else loopBody.push(...emitFlat(body))
1470
+ if (step) loopBody.push(...emitFlat(step))
1471
+ loopBody.push(['br', loop])
1472
+ result.push(['block', brk, ['loop', loop, ...loopBody]])
1473
+ ctx.func.stack.pop()
1474
+ return result.length === 1 ? result[0] : result
1475
+ },
1476
+
1477
+ 'switch': (discriminant, ...cases) => {
1478
+ const disc = `${T}disc${ctx.func.uniq++}`
1479
+ ctx.func.locals.set(disc, 'f64')
1480
+
1481
+ const result = [['local.set', `$${disc}`, asF64(emit(discriminant))]]
1482
+
1483
+ for (const c of cases) {
1484
+ if (c[0] === 'case') {
1485
+ const [, test, body] = c
1486
+ const skip = `$skip${ctx.func.uniq++}`
1487
+ // Block: skip if discriminant != test, otherwise execute body
1488
+ result.push(['block', skip,
1489
+ ['br_if', skip, typed(['f64.ne', typed(['local.get', `$${disc}`], 'f64'), asF64(emit(test))], 'i32')],
1490
+ ...emitFlat(body)])
1491
+ } else if (c[0] === 'default') {
1492
+ result.push(...emitFlat(c[1]))
1493
+ }
1494
+ }
1495
+
1496
+ return result
1497
+ },
1498
+
1499
+ 'while': (cond, body) => emitter['for'](null, cond, null, body),
1500
+ 'break': () => [...emitFinalizers(), ['br', loopTop().brk]],
1501
+ 'continue': () => [...emitFinalizers(), ['br', loopTop().loop]],
1502
+
1503
+ // === Call ===
1504
+
1505
+ // Arrow as value → closure
1506
+ '=>': (rawParams, body) => {
1507
+ if (!ctx.closure.make) err('Closures require fn module (auto-included)')
1508
+
1509
+ const raw = extractParams(rawParams)
1510
+ const params = [], defaults = {}
1511
+ let restParam = null, bodyPrefix = []
1512
+ for (const r of raw) {
1513
+ const c = classifyParam(r)
1514
+ if (c.kind === 'rest') { restParam = c.name; params.push(c.name) }
1515
+ else if (c.kind === 'plain') params.push(c.name)
1516
+ else if (c.kind === 'default') { params.push(c.name); defaults[c.name] = c.defValue }
1517
+ else {
1518
+ const tmp = `${T}p${ctx.func.uniq++}`
1519
+ params.push(tmp)
1520
+ if (c.kind === 'destruct-default') defaults[tmp] = c.defValue
1521
+ bodyPrefix.push(['let', ['=', c.pattern, tmp]])
1522
+ }
1523
+ }
1524
+
1525
+ // Prepend destructuring to body (if any destructured params)
1526
+ if (bodyPrefix.length) {
1527
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
1528
+ body = ['{}', [';', ...bodyPrefix, ...body[1].slice(1)]]
1529
+ else if (Array.isArray(body) && body[0] === '{}')
1530
+ body = ['{}', [';', ...bodyPrefix, body[1]]]
1531
+ else body = ['{}', [';', ...bodyPrefix, ['return', body]]]
1532
+ }
1533
+
1534
+ // Find free variables in body that aren't params → captures
1535
+ const paramSet = new Set(params)
1536
+ const captures = []
1537
+ findFreeVars(body, paramSet, captures)
1538
+ for (const def of Object.values(defaults)) findFreeVars(def, paramSet, captures)
1539
+
1540
+ // Pass closure info including rest param and defaults
1541
+ const closureInfo = { params, body, captures, restParam }
1542
+ if (Object.keys(defaults).length) closureInfo.defaults = defaults
1543
+ return ctx.closure.make(closureInfo)
1544
+ },
1545
+
1546
+ '()': (callee, callArgs) => {
1547
+ let argList = Array.isArray(callArgs)
1548
+ ? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
1549
+ : callArgs ? [callArgs] : []
1550
+
1551
+ // Helper: expand spread arguments into flat list of normal arguments + spread markers
1552
+ // Returns { normal: [...], spreads: [(pos, expr), ...] }
1553
+ const parseArgs = (args) => {
1554
+ const normal = []
1555
+ const spreads = []
1556
+ for (let i = 0; i < args.length; i++) {
1557
+ const arg = args[i]
1558
+ if (Array.isArray(arg) && arg[0] === '...') {
1559
+ spreads.push({ pos: normal.length, expr: arg[1] })
1560
+ } else {
1561
+ normal.push(arg)
1562
+ }
1563
+ }
1564
+ return { normal, spreads, hasSpread: spreads.length > 0 }
1565
+ }
1566
+
1567
+ const parsed = parseArgs(argList)
1568
+
1569
+ // Method call: obj.method(args) → type-aware dispatch
1570
+ if (Array.isArray(callee) && callee[0] === '.') {
1571
+ const [, obj, method] = callee
1572
+
1573
+ // Function property call: fn.prop(args) → direct call to fn$prop
1574
+ if (typeof obj === 'string' && ctx.func.names.has(obj)) {
1575
+ const fname = `${obj}$${method}`
1576
+ if (ctx.func.names.has(fname)) {
1577
+ const func = ctx.func.map.get(fname)
1578
+ const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
1579
+ while (emittedArgs.length < func.sig.params.length)
1580
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
1581
+ const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
1582
+ if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
1583
+ if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
1584
+ return callIR
1585
+ }
1586
+ }
1587
+
1588
+ const vt = keyValType(obj)
1589
+
1590
+ // Helper to call method with arguments (handles spread expansion)
1591
+ const callMethod = (objArg, methodEmitter) => {
1592
+ if (!parsed.hasSpread) {
1593
+ return methodEmitter(objArg, ...parsed.normal)
1594
+ }
1595
+
1596
+ // Bulk push fast path: `obj.push(...src)` — single spread, no normal args, named obj.
1597
+ // The generic single-spread loop below calls methodEmitter per iteration, which expands
1598
+ // to a full .push (grow check + ptr_offset + store + set_len) every step. Amortising the
1599
+ // grow + set_len across the whole spread eliminates ~3 stdlib calls per byte in watr's
1600
+ // hot `out.push(...HANDLER[op](...))` path (~24M bytes/iter on raycast).
1601
+ if (method === 'push' && parsed.normal.length === 0 &&
1602
+ parsed.spreads.length === 1 && typeof objArg === 'string') {
1603
+ const spreadExpr = parsed.spreads[0].expr
1604
+ inc('__len'); inc('__arr_grow'); inc('__set_len'); inc('__ptr_offset')
1605
+ const o = `${T}po${ctx.func.uniq++}`,
1606
+ sa = `${T}psa${ctx.func.uniq++}`,
1607
+ sl = `${T}psl${ctx.func.uniq++}`,
1608
+ ol = `${T}pol${ctx.func.uniq++}`,
1609
+ si = `${T}psi${ctx.func.uniq++}`,
1610
+ base = `${T}pb${ctx.func.uniq++}`
1611
+ ctx.func.locals.set(o, 'f64'); ctx.func.locals.set(sa, 'f64')
1612
+ ctx.func.locals.set(sl, 'i32'); ctx.func.locals.set(ol, 'i32')
1613
+ ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
1614
+
1615
+ const objIsArr = lookupValType(objArg) === VAL.ARRAY
1616
+ // Spread source: if statically known ARRAY, inline len/load via hoisted srcBase
1617
+ // (skip per-iteration __arr_idx call + dispatch).
1618
+ const srcVT = valTypeOf(spreadExpr)
1619
+ const srcIsArr = !multiCount(spreadExpr) && srcVT === VAL.ARRAY
1620
+ const srcBase = srcIsArr ? `${T}psb${ctx.func.uniq++}` : null
1621
+ if (srcIsArr) ctx.func.locals.set(srcBase, 'i32')
1622
+ const n = multiCount(spreadExpr)
1623
+ const ir = []
1624
+ ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
1625
+ ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
1626
+ if (srcIsArr) {
1627
+ ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${sa}`]]])
1628
+ ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
1629
+ } else {
1630
+ ir.push(['local.set', `$${sl}`, ['call', '$__len', ['local.get', `$${sa}`]]])
1631
+ }
1632
+ // Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
1633
+ if (objIsArr) {
1634
+ ir.push(['local.set', `$${ol}`,
1635
+ ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${o}`]], ['i32.const', 8]]]])
1636
+ } else {
1637
+ ir.push(['local.set', `$${ol}`, ['call', '$__len', ['local.get', `$${o}`]]])
1638
+ }
1639
+ // Single grow for the full spread (vs per-element grow check in the generic loop).
1640
+ ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['local.get', `$${o}`],
1641
+ ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
1642
+ // base captured AFTER grow (grow may relocate the array).
1643
+ ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${o}`]]])
1644
+ // Tight store loop.
1645
+ ir.push(['local.set', `$${si}`, ['i32.const', 0]])
1646
+ const loopId = ctx.func.uniq++
1647
+ const srcLoad = srcIsArr
1648
+ ? ['f64.load', ['i32.add', ['local.get', `$${srcBase}`], ['i32.shl', ['local.get', `$${si}`], ['i32.const', 3]]]]
1649
+ : asF64(emit(['[]', sa, si]))
1650
+ ir.push(['block', `$break${loopId}`, ['loop', `$continue${loopId}`,
1651
+ ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${si}`], ['local.get', `$${sl}`]]],
1652
+ ['f64.store',
1653
+ ['i32.add', ['local.get', `$${base}`],
1654
+ ['i32.shl', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${si}`]], ['i32.const', 3]]],
1655
+ srcLoad],
1656
+ ['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
1657
+ ['br', `$continue${loopId}`]]])
1658
+ // Single set_len for the full spread.
1659
+ ir.push(['call', '$__set_len', ['local.get', `$${o}`],
1660
+ ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
1661
+ // Update source variable: grow may have moved the pointer.
1662
+ if (ctx.func.boxed?.has(objArg)) {
1663
+ ir.push(['f64.store', ['local.get', `$${ctx.func.boxed.get(objArg)}`], ['local.get', `$${o}`]])
1664
+ } else if (ctx.scope.globals.has(objArg) && !ctx.func.locals?.has(objArg)) {
1665
+ ir.push(['global.set', `$${objArg}`, ['local.get', `$${o}`]])
1666
+ } else {
1667
+ ir.push(['local.set', `$${objArg}`, ['local.get', `$${o}`]])
1668
+ }
1669
+ ir.push(['f64.convert_i32_s', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
1670
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
1671
+ }
1672
+
1673
+ // Single spread at end: call method with normal args, then loop spread elements
1674
+ if (parsed.spreads.length === 1 && parsed.spreads[0].pos === parsed.normal.length) {
1675
+ const spreadExpr = parsed.spreads[0].expr
1676
+ const acc = `${T}acc${ctx.func.uniq++}`, arr = `${T}sp${ctx.func.uniq++}`, len = `${T}splen${ctx.func.uniq++}`, idx = `${T}spidx${ctx.func.uniq++}`
1677
+ ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
1678
+ ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
1679
+ const spreadVT = valTypeOf(spreadExpr)
1680
+ if (spreadVT) updateRep(arr, { val: spreadVT })
1681
+
1682
+ // In-place spread methods modify target; accumulating methods (concat) return new values
1683
+ const inPlace = SPREAD_MUTATORS.has(method)
1684
+ // unshift prepends each arg to the front — iterating forward reverses the
1685
+ // intended order, so walk the spread from end to start.
1686
+ const reverseIter = method === 'unshift'
1687
+ const ir = []
1688
+ ir.push(['local.set', `$${acc}`, asF64(emit(objArg))])
1689
+ if (parsed.normal.length > 0) {
1690
+ const r = asF64(methodEmitter(objArg, ...parsed.normal))
1691
+ ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
1692
+ }
1693
+
1694
+ inc('__len')
1695
+ const n = multiCount(spreadExpr)
1696
+ ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
1697
+ ir.push(['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${arr}`]]])
1698
+ ir.push(['local.set', `$${idx}`,
1699
+ reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
1700
+ const loopId = ctx.func.uniq++
1701
+ const loopBody = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
1702
+ ir.push(['block', `$break${loopId}`,
1703
+ ['loop', `$continue${loopId}`,
1704
+ ['br_if', `$break${loopId}`,
1705
+ reverseIter
1706
+ ? ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]
1707
+ : ['i32.ge_u', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
1708
+ inPlace ? ['drop', loopBody] : ['local.set', `$${acc}`, loopBody],
1709
+ ['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['i32.const', reverseIter ? -1 : 1]]],
1710
+ ['br', `$continue${loopId}`]]])
1711
+
1712
+ ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
1713
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
1714
+ }
1715
+
1716
+ // General spread case: iterate args in original order, batch contiguous normal
1717
+ // args into a single call, emit a per-element loop for each spread.
1718
+ //
1719
+ // inPlace methods (push/unshift/add/set): call methodEmitter(objArg, ...) each
1720
+ // time so the source variable's local gets updated (else heap grow/realloc
1721
+ // wouldn't be visible to subsequent uses of the variable). Final value is objArg.
1722
+ //
1723
+ // non-inPlace (concat, etc.): chain via temp acc since return value is the new
1724
+ // collection.
1725
+ const inPlaceG = SPREAD_MUTATORS.has(method)
1726
+ const combinedG = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1727
+ inc('__len')
1728
+
1729
+ if (inPlaceG) {
1730
+ const irG = []
1731
+ let batch = []
1732
+ const flushBatch = () => {
1733
+ if (!batch.length) return
1734
+ irG.push(['drop', asF64(methodEmitter(objArg, ...batch))])
1735
+ batch = []
1736
+ }
1737
+ for (const item of combinedG) {
1738
+ if (Array.isArray(item) && item[0] === '__spread') {
1739
+ flushBatch()
1740
+ const spreadExpr = item[1]
1741
+ const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
1742
+ ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
1743
+ const spreadVT = valTypeOf(spreadExpr)
1744
+ if (spreadVT) updateRep(arrL, { val: spreadVT })
1745
+ const n = multiCount(spreadExpr)
1746
+ irG.push(
1747
+ ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
1748
+ ['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
1749
+ ['local.set', `$${idxL}`, ['i32.const', 0]])
1750
+ const loopId = ctx.func.uniq++
1751
+ const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
1752
+ irG.push(['block', `$break${loopId}`,
1753
+ ['loop', `$continue${loopId}`,
1754
+ ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
1755
+ ['drop', loopBody],
1756
+ ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
1757
+ ['br', `$continue${loopId}`]]])
1758
+ } else {
1759
+ batch.push(item)
1760
+ }
1761
+ }
1762
+ flushBatch()
1763
+ irG.push(asF64(emit(objArg)))
1764
+ return typed(['block', ['result', 'f64'], ...irG], 'f64')
1765
+ }
1766
+
1767
+ const accG = `${T}acc${ctx.func.uniq++}`
1768
+ ctx.func.locals.set(accG, 'f64')
1769
+ const irG = [['local.set', `$${accG}`, asF64(emit(objArg))]]
1770
+ let batch = []
1771
+ const flushBatch = () => {
1772
+ if (!batch.length) return
1773
+ irG.push(['local.set', `$${accG}`, asF64(methodEmitter(accG, ...batch))])
1774
+ batch = []
1775
+ }
1776
+ for (const item of combinedG) {
1777
+ if (Array.isArray(item) && item[0] === '__spread') {
1778
+ flushBatch()
1779
+ const spreadExpr = item[1]
1780
+ const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
1781
+ ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
1782
+ const spreadVT = valTypeOf(spreadExpr)
1783
+ if (spreadVT) updateRep(arrL, { val: spreadVT })
1784
+ const n = multiCount(spreadExpr)
1785
+ irG.push(
1786
+ ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
1787
+ ['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
1788
+ ['local.set', `$${idxL}`, ['i32.const', 0]])
1789
+ const loopId = ctx.func.uniq++
1790
+ const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
1791
+ irG.push(['block', `$break${loopId}`,
1792
+ ['loop', `$continue${loopId}`,
1793
+ ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
1794
+ ['local.set', `$${accG}`, loopBody],
1795
+ ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
1796
+ ['br', `$continue${loopId}`]]])
1797
+ } else {
1798
+ batch.push(item)
1799
+ }
1800
+ }
1801
+ flushBatch()
1802
+ irG.push(['local.get', `$${accG}`])
1803
+ return typed(['block', ['result', 'f64'], ...irG], 'f64')
1804
+ }
1805
+
1806
+ // Boxed object: delegate method to inner value (slot 0)
1807
+ if (typeof obj === 'string' && ctx.schema.isBoxed?.(obj)) {
1808
+ const innerVt = repOf(obj)?.val
1809
+ const emitter = ctx.core.emit[`.${innerVt}:${method}`] || ctx.core.emit[`.${method}`]
1810
+ if (emitter) {
1811
+ const innerName = `${obj}${T}inner`
1812
+ if (!ctx.func.locals.has(innerName)) ctx.func.locals.set(innerName, 'f64')
1813
+ const boxBase = tempI32('bb')
1814
+ // Load current inner value from boxed object's slot 0 (may have been updated by prior mutations)
1815
+ // Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
1816
+ const loadInner = [
1817
+ ['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
1818
+ ['local.set', `$${innerName}`, ['f64.load', ['local.get', `$${boxBase}`]]]]
1819
+ const result = callMethod(innerName, emitter)
1820
+ // Mutating methods may reallocate; writeback inner value to boxed slot
1821
+ if (BOXED_MUTATORS.has(method)) {
1822
+ const wb = ['f64.store', ['local.get', `$${boxBase}`], ['local.get', `$${innerName}`]]
1823
+ return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
1824
+ }
1825
+ // Non-mutating: just load inner and call
1826
+ return typed(['block', ['result', 'f64'], ...loadInner, asF64(result)], 'f64')
1827
+ }
1828
+ }
1829
+
1830
+ // Known type → static dispatch
1831
+ if (vt && ctx.core.emit[`.${vt}:${method}`]) {
1832
+ return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
1833
+ }
1834
+
1835
+ // Unknown / guessed-array type, both string + generic exist → runtime dispatch by ptr type.
1836
+ // analyze.js defaults untyped `.slice()` results to VAL.ARRAY, which is a guess, not a proof;
1837
+ // runtime dispatch resolves whether the operand is actually a string or an array.
1838
+ // Concretely-typed non-string values (BUFFER, TYPED, MAP, …) fall through to the generic
1839
+ // emitter which already knows how to handle them.
1840
+ const strKey = `.string:${method}`, genKey = `.${method}`
1841
+ if ((!vt || vt === VAL.ARRAY) && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
1842
+ const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
1843
+ ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
1844
+ const strEmitter = ctx.core.emit[strKey]
1845
+ const genEmitter = ctx.core.emit[genKey]
1846
+ return typed(['block', ['result', 'f64'],
1847
+ ['local.set', `$${t}`, asF64(emit(obj))],
1848
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]],
1849
+ ['if', ['result', 'f64'],
1850
+ ['i32.or',
1851
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
1852
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
1853
+ ['then', callMethod(t, strEmitter)],
1854
+ ['else', callMethod(t, genEmitter)]]], 'f64')
1855
+ }
1856
+
1857
+ // Schema property function call: x.prop(args) where prop is a closure in boxed schema
1858
+ if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
1859
+ const idx = ctx.schema.find(obj, method)
1860
+ if (idx >= 0) {
1861
+ const propRead = typed(['f64.load', ['i32.add', ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
1862
+ return ctx.closure.call(propRead, parsed.normal)
1863
+ }
1864
+ }
1865
+
1866
+ // Generic only
1867
+ if (ctx.core.emit[genKey]) {
1868
+ return callMethod(obj, ctx.core.emit[genKey])
1869
+ }
1870
+
1871
+ // Dynamic property function call on non-external values.
1872
+ if (ctx.closure.call) {
1873
+ if (ctx.transform.strict)
1874
+ err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type pulls dynamic dispatch stdlib. Annotate the receiver type or pass { strict: false }.`)
1875
+ const objTmp = `${T}mobj${ctx.func.uniq++}`
1876
+ ctx.func.locals.set(objTmp, 'f64')
1877
+ const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1878
+ const arrayIR = buildArrayWithSpreads(combined)
1879
+ const propRead = typed(['call', '$__dyn_get_expr', ['local.get', `$${objTmp}`], asF64(emit(['str', method]))], 'f64')
1880
+ if (usesDynProps(vt)) {
1881
+ inc('__dyn_get_expr')
1882
+ return typed(['block', ['result', 'f64'],
1883
+ ['local.set', `$${objTmp}`, asF64(emit(obj))],
1884
+ ctx.closure.call(propRead, [arrayIR], true)], 'f64')
1885
+ }
1886
+ inc('__dyn_get_expr', '__ext_call')
1887
+ ctx.features.external = true
1888
+ return typed(['block', ['result', 'f64'],
1889
+ ['local.set', `$${objTmp}`, asF64(emit(obj))],
1890
+ ['if', ['result', 'f64'],
1891
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${objTmp}`]], ['i32.const', PTR.EXTERNAL]],
1892
+ ['then', ['call', '$__ext_call', ['local.get', `$${objTmp}`], asF64(emit(['str', method])), arrayIR]],
1893
+ ['else', ctx.closure.call(propRead, [arrayIR], true)]]], 'f64')
1894
+ }
1895
+
1896
+ // Unknown callee - assume external method
1897
+ if (ctx.transform.strict)
1898
+ err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
1899
+ inc('__ext_call')
1900
+ ctx.features.external = true
1901
+ const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1902
+ const arrayIR = buildArrayWithSpreads(combined)
1903
+ return typed(['call', '$__ext_call', asF64(emit(obj)), asF64(emit(['str', method])), arrayIR], 'f64');
1904
+ }
1905
+
1906
+ if (ctx.core.emit[callee]) {
1907
+ // Pass spread args through to emitter (e.g. Math.max(...arr))
1908
+ if (parsed.hasSpread) {
1909
+ const allArgs = []
1910
+ let ni = 0
1911
+ for (const s of parsed.spreads) {
1912
+ while (ni < s.pos) allArgs.push(parsed.normal[ni++])
1913
+ allArgs.push(['...', s.expr])
1914
+ }
1915
+ while (ni < parsed.normal.length) allArgs.push(parsed.normal[ni++])
1916
+ return ctx.core.emit[callee](...allArgs)
1917
+ }
1918
+ return ctx.core.emit[callee](...parsed.normal)
1919
+ }
1920
+
1921
+ // Direct call if callee is a known top-level function
1922
+ if (typeof callee === 'string' && ctx.func.names.has(callee)) {
1923
+ const func = ctx.func.map.get(callee)
1924
+
1925
+ // Rest param case: collect all args (including expanded spreads) into array
1926
+ if (func?.rest) {
1927
+ const fixedParamCount = func.sig.params.length - 1
1928
+ const fixedArgs = parsed.normal.slice(0, fixedParamCount)
1929
+ // Pad missing fixed args with sentinel for defaults
1930
+ const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
1931
+ while (emittedFixed.length < fixedParamCount)
1932
+ emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
1933
+
1934
+ // Reconstruct with spreads, then take rest args
1935
+ const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1936
+ const restArgsFinal = combined.slice(fixedParamCount)
1937
+
1938
+ // Build array: emit code for normal args + code to expand spreads
1939
+ const arrayIR = buildArrayWithSpreads(restArgsFinal)
1940
+ const callIR = typed(['call', `$${callee}`,
1941
+ ...emittedFixed,
1942
+ arrayIR], func.sig.results[0])
1943
+ if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
1944
+ if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
1945
+ return callIR
1946
+ }
1947
+
1948
+ // Regular function call without rest params
1949
+ if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
1950
+ // Pad missing args with canonical NaN (triggers default param init)
1951
+ const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
1952
+ const expected = func?.sig.params.length || args.length
1953
+ while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
1954
+ // Multi-value return: materialize as heap array (caller expects single pointer)
1955
+ if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
1956
+ const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
1957
+ if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
1958
+ if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
1959
+ return callIR
1960
+ }
1961
+
1962
+ // A3: const-bound, non-escaping closure → direct call to body (skip call_indirect).
1963
+ // emitDecl registered name → bodyName when it saw the closure.make IR. Body signature
1964
+ // is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64. We pass the closure
1965
+ // NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
1966
+ if (typeof callee === 'string' && !parsed.hasSpread
1967
+ && ctx.func.directClosures?.has(callee)) {
1968
+ const bodyName = ctx.func.directClosures.get(callee)
1969
+ const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
1970
+ const n = parsed.normal.length
1971
+ if (n <= W) {
1972
+ const slots = parsed.normal.map(a => asF64(emit(a)))
1973
+ while (slots.length < W) slots.push(undefExpr())
1974
+ return typed(['call', `$${bodyName}`,
1975
+ asF64(emit(callee)),
1976
+ typed(['i32.const', n], 'i32'),
1977
+ ...slots], 'f64')
1978
+ }
1979
+ }
1980
+
1981
+ // Closure call: callee is a variable holding a NaN-boxed closure pointer
1982
+ // Uniform convention: fn.call packs all args into an array
1983
+ if (ctx.closure.call) {
1984
+ if (parsed.hasSpread) {
1985
+ // Spread: build the args array directly (handles __spread markers)
1986
+ const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1987
+ const arrayIR = buildArrayWithSpreads(combined)
1988
+ // Pass pre-built array as single already-emitted arg
1989
+ return ctx.closure.call(emit(callee), [arrayIR], true)
1990
+ }
1991
+ return ctx.closure.call(emit(callee), parsed.normal)
1992
+ }
1993
+
1994
+ // Unknown callee — assume direct call
1995
+ return typed(['call', `$${callee}`, ...argList.map(a => asF64(emit(a)))], 'f64')
1996
+ },
1997
+ }
1998
+
1999
+ // === Emit dispatch ===
2000
+
2001
+ /**
2002
+ * Emit single AST node to typed WASM IR.
2003
+ * Every returned node has .type = 'i32' | 'f64'.
2004
+ * @param {import('./prepare.js').ASTNode} node
2005
+ * @returns {Array} typed WASM S-expression
2006
+ */
2007
+ export function emit(node, expect) {
2008
+ _expect = expect || null
2009
+ if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
2010
+ if (node == null) return null
2011
+ if (node === true) return typed(['i32.const', 1], 'i32')
2012
+ if (node === false) return typed(['i32.const', 0], 'i32')
2013
+ if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
2014
+ return nullExpr()
2015
+ if (typeof node === 'bigint') {
2016
+ // Wrap to unsigned i64 range — emit as positive hex so downstream BigInt() parsers
2017
+ // (e.g. watr's optimize.js getConst) don't choke on "-0x..." strings.
2018
+ const n = node & 0xFFFFFFFFFFFFFFFFn
2019
+ return typed(['f64.reinterpret_i64', ['i64.const', '0x' + n.toString(16)]], 'f64')
2020
+ }
2021
+ if (typeof node === 'number') {
2022
+ if (Number.isInteger(node) && node >= -2147483648 && node <= 2147483647)
2023
+ return typed(['i32.const', node], 'i32')
2024
+ return typed(['f64.const', node], 'f64')
2025
+ }
2026
+ if (typeof node === 'string') {
2027
+ // Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
2028
+ if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node))
2029
+ return readVar(node)
2030
+ // Top-level function used as value → wrap as closure pointer for call_indirect
2031
+ if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
2032
+ // Trampoline signature: uniform closure ABI (env f64, argc i32, a0..a{MAX-1} f64) → f64.
2033
+ // Forwards the first N inline slots to $func where N = func's fixed param count.
2034
+ const func = ctx.func.map.get(node)
2035
+ const sigParams = func?.sig.params || []
2036
+ if (sigParams.length > MAX_CLOSURE_ARITY) err(`Function ${node} used as closure value has ${sigParams.length} params, exceeds MAX_CLOSURE_ARITY=${MAX_CLOSURE_ARITY}`)
2037
+ const trampolineName = `${T}tramp_${node}`
2038
+ if (!ctx.core.stdlib[trampolineName]) {
2039
+ const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
2040
+ const paramDecls = ['(param $__env f64)', '(param $__argc i32)']
2041
+ for (let i = 0; i < W; i++) paramDecls.push(`(param $__a${i} f64)`)
2042
+ // Forward fixed slots; if func expects i32, convert via trunc_sat
2043
+ const fwd = sigParams.map((p, i) =>
2044
+ p.type === 'i32'
2045
+ ? `(i32.trunc_sat_f64_s (local.get $__a${i}))`
2046
+ : `(local.get $__a${i})`).join(' ')
2047
+ if ((func?.sig.results.length || 1) > 1) {
2048
+ const n = func.sig.results.length
2049
+ const arr = `${T}retarr`
2050
+ const temps = Array.from({ length: n }, (_, i) => `${T}ret${i}`)
2051
+ const tempLocals = temps.map(name => `(local $${name} f64)`).join(' ')
2052
+ const stores = temps.map((name, i) =>
2053
+ `(f64.store (i32.add (local.get $${arr}) (i32.const ${i * 8})) (local.get $${name}))`
2054
+ ).join(' ')
2055
+ const capture = temps.slice().reverse().map(name => `(local.set $${name})`).join(' ')
2056
+ ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (local $${arr} i32) ${tempLocals} (call $${node} ${fwd}) ${capture} (local.set $${arr} (call $__alloc (i32.const ${n * 8 + 8}))) (i32.store (local.get $${arr}) (i32.const ${n})) (i32.store (i32.add (local.get $${arr}) (i32.const 4)) (i32.const ${n})) (local.set $${arr} (i32.add (local.get $${arr}) (i32.const 8))) ${stores} (call $__mkptr (i32.const 1) (i32.const 0) (local.get $${arr})))`
2057
+ inc(trampolineName, '__alloc', '__mkptr')
2058
+ } else {
2059
+ ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (call $${node} ${fwd}))`
2060
+ inc(trampolineName)
2061
+ }
2062
+ }
2063
+ let idx = ctx.closure.table.indexOf(trampolineName)
2064
+ if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
2065
+ return mkPtrIR(PTR.CLOSURE, idx, 0)
2066
+ }
2067
+ // Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
2068
+ if (node.includes('.') && ctx.core.emit[node]) return ctx.core.emit[node]()
2069
+ // Auto-import known host globals (WebAssembly, globalThis, etc.)
2070
+ const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
2071
+ if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
2072
+ ctx.features.external = true
2073
+ ctx.scope.globals.set(node, null)
2074
+ ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, ['mut', 'f64']]])
2075
+ return typed(['global.get', `$${node}`], 'f64')
2076
+ }
2077
+ const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
2078
+ return typed(['local.get', `$${node}`], t)
2079
+ }
2080
+ if (!Array.isArray(node)) return typed(['f64.const', 0], 'f64')
2081
+
2082
+ const [op, ...args] = node
2083
+ // WASM IR passthrough: internally-generated IR nodes (from statement flattening) pass through
2084
+ if (typeof op === 'string' && !ctx.core.emit[op] && (op.includes('.') || WASM_OPS.has(op))) return node
2085
+
2086
+ // Literal node [, value] — handle null/undefined values
2087
+ if (op == null && args.length === 1) {
2088
+ const v = args[0]
2089
+ return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
2090
+ }
2091
+
2092
+ const handler = ctx.core.emit[op]
2093
+ if (!handler) err(`Unknown op: ${op}`)
2094
+ return handler(...args)
2095
+ }