jz 0.5.1 → 0.6.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.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/emit.js DELETED
@@ -1,3040 +0,0 @@
1
- /**
2
- * AST → WASM IR emission.
3
- *
4
- * # Stage contract
5
- * IN: prepared AST node + ctx state (func.locals, func.localReps, 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 {
27
- T, VAL, nonNegIntLiteral, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey,
28
- // AST predicates (formerly in src/ast.js)
29
- isReassigned, hasOwnContinue, hasOwnBreakOrContinue, containsNestedClosure,
30
- containsNestedLoop, nestedSmallLoopBudget, containsDeclOf, cloneWithSubst,
31
- containsKnownTypedArrayIndex, smallConstForTripCount, isTerminator,
32
- inBoundsCharCodeAt,
33
- MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
34
- } from './analyze.js'
35
- import {
36
- typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
37
- NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
38
- WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
39
- mkPtrIR, ptrOffsetIR, ptrTypeIR,
40
- isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64,
41
- truthyIR, toBoolFromEmitted, isPostfix,
42
- isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
43
- temp, tempI32, tempI64, allocPtr,
44
- boxedAddr, readVar, writeVar, isNullish, isBoolAtom,
45
- isLiteralStr, resolveValType, isFuncRef,
46
- multiCount, loopTop, flat,
47
- reconstructArgsWithSpreads, tcoTailRewrite,
48
- } from './ir.js'
49
- import { typeofPredicate } from './infer.js'
50
-
51
- // Current emission "expect" mode ('void' or null); set by emit(), read by compound-assignment emitters
52
- // to decide whether to emit a value-returning or side-effect-only form.
53
- let _expect = null
54
-
55
- // A genuine i32 *number* — safe for the i32 fast path in arithmetic/bitwise
56
- // operators. An unboxed pointer (object/array/string/closure local kept as a
57
- // raw i32 handle) is *also* i32-typed but carries `.ptrKind`; treating it as a
58
- // number would compute on raw pointer bits. A ptrKind-carrying operand must
59
- // instead route through ToNumber (`toNumF64`), which performs ToPrimitive.
60
- const isI32Num = (v) => v.type === 'i32' && v.ptrKind == null
61
-
62
- // An operand whose uint32 value can be *observed as a JS number* — a `>>>`
63
- // result, an `unsignedResult` call, or an unsigned i32.const. Its magnitude can
64
- // exceed signed-i32 range, so wrapping i32 arithmetic would corrupt it; widen to
65
- // f64 instead. A `.wrapSafe` operand is also unsigned but is a `narrowUint32`
66
- // accumulator read proven to be re-truncated by a `>>>` (ToUint32) sink at every
67
- // use — wrapping is exactly its intended semantics, so it stays on the i32 path.
68
- const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
69
-
70
- const FIRST_CLASS_UNARY_MATH = {
71
- 'math.abs': 'f64.abs',
72
- 'math.sqrt': 'f64.sqrt',
73
- 'math.ceil': 'f64.ceil',
74
- 'math.floor': 'f64.floor',
75
- 'math.trunc': 'f64.trunc',
76
- }
77
-
78
- function builtinFunctionValue(name) {
79
- const op = FIRST_CLASS_UNARY_MATH[name]
80
- if (!op) err(`Builtin function '${name}' cannot be used as a first-class value`)
81
- if (!ctx.closure.table) err(`Builtin function '${name}' used as value requires closure support`)
82
- const fn = `${T}builtin_${name.replace(/\W/g, '_')}`
83
- if (!ctx.core.stdlib[fn]) {
84
- const width = ctx.closure.width ?? MAX_CLOSURE_ARITY
85
- const params = ['(param $__env f64)', '(param $__argc i32)']
86
- for (let i = 0; i < width; i++) params.push(`(param $__a${i} f64)`)
87
- ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) (${op} (local.get $__a0)))`
88
- inc(fn)
89
- }
90
- let idx = ctx.closure.table.indexOf(fn)
91
- if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(fn) }
92
- const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
93
- ir.closureFuncIdx = idx
94
- return ir
95
- }
96
-
97
- /** Emit unary negation: constant-fold, or i32 sub from 0 / f64.neg. */
98
- const emitNeg = (a) => {
99
- if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
100
- const v = emit(a)
101
- return isLit(v) ? emitNum(-litVal(v)) : isI32Num(v)
102
- ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32')
103
- : typed(['f64.neg', toNumF64(a, v)], 'f64')
104
- }
105
-
106
- /** Try constant-folding binary arith: returns emitNum(result) or null. */
107
- // `.unsigned` literals carry a uint32 value whose i32 `litVal` is its *signed* bit
108
- // pattern (e.g. `-1` for 4294967295), so folding them through `fn` numerically would
109
- // be wrong. Bail to the runtime path — the arithmetic handlers widen unsigned operands
110
- // to f64 (convert_i32_u), reproducing the JS-spec result.
111
- const foldConst = (va, vb, fn, guard) =>
112
- isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned && (!guard || guard(litVal(vb)))
113
- ? emitNum(fn(litVal(va), litVal(vb))) : null
114
-
115
- // JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
116
- // Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
117
- // exact product stays f64-exact, i.e. |product| <= 2^53. Two i32 operands can
118
- // reach 2^62, so `i32.mul` is sound only when one side is a literal small
119
- // enough that, against the full i32 range (2^31) of the other, the product
120
- // holds within 2^53 — i.e. |literal| <= 2^22. Keeps index arithmetic (`i*4`,
121
- // `row*16`) on `i32.mul` while routing hash-mix-scale products to `f64.mul`.
122
- const mulFitsI32 = (va, vb) =>
123
- (isLit(va) && Math.abs(litVal(va)) <= 0x400000) ||
124
- (isLit(vb) && Math.abs(litVal(vb)) <= 0x400000)
125
-
126
- /** Emit typeof comparison: typeof x == typeCode → type-aware check. */
127
- export function emitTypeofCmp(a, b, cmpOp) {
128
- let typeofExpr, code
129
- if (Array.isArray(a) && a[0] === 'typeof' && typeof b === 'number') { typeofExpr = a[1]; code = b }
130
- else if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null) { typeofExpr = a[1]; code = b[1] }
131
- else return null
132
- if (typeof code !== 'number') return null
133
-
134
- const t = temp()
135
- const va = asF64(emit(typeofExpr))
136
- const eq = cmpOp === 'eq'
137
-
138
- if (code === -1) {
139
- // A VAL.BOOL value rides in the f64 0/1 carrier, so it would pass the
140
- // `v===v` number test — but it is typeof "boolean", not "number".
141
- if (resolveValType(typeofExpr, valTypeOf, lookupValType) === VAL.BOOL)
142
- return typed(['i32.const', eq ? 0 : 1], 'i32')
143
- return typed(eq
144
- ? ['f64.eq', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
145
- : ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
146
- }
147
- if (code === -2) {
148
- inc('__ptr_type')
149
- const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
150
- const isStr = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]]
151
- return typed(eq ? ['i32.and', isPtr, isStr]
152
- : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isStr]], 'i32')
153
- }
154
- if (code === -3) {
155
- const check = isNullish(va)
156
- return typed(eq ? check : ['i32.eqz', check], 'i32')
157
- }
158
- if (code === -4) {
159
- // boolean. Statically VAL.BOOL → true; any other known type → false.
160
- const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
161
- if (vt) return typed(['i32.const', (vt === VAL.BOOL) === eq ? 1 : 0], 'i32')
162
- // Unknown static type: boolean iff the carrier is a boxed-boolean atom.
163
- const isBool = isBoolAtom(['local.tee', `$${t}`, va])
164
- return typed(eq ? isBool : ['i32.eqz', isBool], 'i32')
165
- }
166
- if (code === -5) {
167
- inc('__ptr_type')
168
- const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
169
- const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
170
- const notStrFn = ['i32.and',
171
- ['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]], ['i32.const', PTR.STRING]],
172
- ['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]]
173
- const notNullish = ['i32.eqz', isNullish(['local.get', `$${t}`])]
174
- const check = ['i32.and', ['i32.and', isPtr, notStrFn], notNullish]
175
- return typed(eq ? check : ['i32.eqz', check], 'i32')
176
- }
177
- if (code === -6) {
178
- inc('__ptr_type')
179
- const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
180
- const isFn = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.CLOSURE]]
181
- return typed(eq ? ['i32.and', isPtr, isFn] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isFn]], 'i32')
182
- }
183
- if (code === -7) {
184
- const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
185
- if (vt) return typed(['i32.const', (vt === VAL.BIGINT) === eq ? 1 : 0], 'i32')
186
- const n = ['local.tee', `$${t}`, va]
187
- const isBigInt = ['i32.and',
188
- ['f64.eq', n, ['local.get', `$${t}`]],
189
- ['i32.and',
190
- ['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]],
191
- ['f64.lt', ['f64.abs', ['local.get', `$${t}`]], ['f64.const', 2.2250738585072014e-308]]]]
192
- return typed(eq ? isBigInt : ['i32.eqz', isBigInt], 'i32')
193
- }
194
- if (code >= 0) {
195
- inc('__ptr_type')
196
- const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
197
- const check = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', code]]
198
- return typed(eq ? ['i32.and', isPtr, check] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', check]], 'i32')
199
- }
200
- return null
201
- }
202
-
203
- /** Stringify a VAL.BOOL operand to "true"/"false" (f64 string pointer). The
204
- * boolean rides the cheap 0/1 carrier, so we runtime-select between the two
205
- * interned literals; a constant operand folds to a single literal downstream. */
206
- export const emitBoolStr = (node) =>
207
- typed(['select', asF64(emit(['str', 'true'])), asF64(emit(['str', 'false'])), truthyIR(emit(node))], 'f64')
208
-
209
- const CMP_SET = new Set(['>', '<', '>=', '<=', '==', '!=', '!'])
210
- const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
211
-
212
- // Map/Set methods whose generic (`.${method}`) emitter assumes a collection
213
- // receiver and dereferences a key/value argument. Every one needs ≥1 argument
214
- // (`.get(k)` / `.has(v)` / `.add(v)` / `.delete(v)` / `.set(k[,v])`), so a
215
- // zero-arg call on a not-proven-collection receiver cannot be the collection
216
- // op — it is a user/closure method and must not reach the collection emitter.
217
- const COLLECTION_METHODS = new Set(['get', 'set', 'has', 'add', 'delete'])
218
-
219
- // Pointer kinds for which JS `==` / `!=` is pure reference equality — i.e. i64 bit
220
- // compare of the NaN-box is equivalent to __eq. Excludes STRING (content compare for
221
- // heap strings) and BIGINT (content compare).
222
- const REF_EQ_KINDS = new Set([
223
- VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
224
- VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX, VAL.DATE,
225
- ])
226
-
227
- function stringLiteral(node) {
228
- if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
229
- if (Array.isArray(node) && node[0] == null && typeof node[1] === 'string') return node[1]
230
- return null
231
- }
232
-
233
- // Index expressions where peepholing `s[k] === 'X'` to char-byte compare is
234
- // semantics-preserving: must produce a non-negative *integer* at run time so
235
- // `__str_byteLen u> k` bounds-checks the same range JS would. Out-of-range
236
- // (negative or ≥ len) falls into the `else 0` arm — matches `undefined === 'X'`.
237
- function intIndexIR(key) {
238
- const lit = nonNegIntLiteral(key)
239
- if (lit != null) return ['i32.const', lit]
240
- // intCertain name: forward-prop says every defining RHS is integer-shaped.
241
- // Captures loop variables (`for(let i=0;;i++)`), `let k = j + 1`, etc.
242
- if (typeof key === 'string' && repOf(key)?.intCertain) return asI32(emit(key))
243
- // intCertain schema slot read `o.x`: every observed write is integer-shaped,
244
- // so the loaded f64 represents an int — fold into the byte-compare fast path.
245
- if (Array.isArray(key) && key[0] === '.' && typeof key[1] === 'string' && typeof key[2] === 'string' &&
246
- ctx.schema.slotIntCertainAt?.(key[1], key[2]) === true) return asI32(emit(key))
247
- return null
248
- }
249
-
250
- function emitSingleCharIndexCmp(a, b, negate = false) {
251
- const leftLit = stringLiteral(a)
252
- const rightLit = stringLiteral(b)
253
- const aIdx = Array.isArray(a) && a[0] === '[]'
254
- const bIdx = Array.isArray(b) && b[0] === '[]'
255
- let indexed, lit
256
- if (bIdx && leftLit != null) { indexed = b; lit = leftLit }
257
- else if (aIdx && rightLit != null) { indexed = a; lit = rightLit }
258
- else return null
259
-
260
- if (lit.length === 0) return null
261
- if ([...lit].some(c => c.charCodeAt(0) > 0x7F)) return null
262
-
263
- const [, obj, key] = indexed
264
- const idxIR = intIndexIR(key)
265
- if (idxIR == null) return null
266
-
267
- const vt = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
268
- if (vt && vt !== VAL.STRING) return null
269
-
270
- const finish = expr => negate ? ['i32.eqz', expr] : expr
271
-
272
- // Known STRING: s[i] always returns 1-char SSO. Multi-char literal → always false.
273
- if (vt === VAL.STRING && lit.length > 1) return emitNum(negate ? 1 : 0)
274
-
275
- // Single-char literal: compare byte directly, skipping __str_idx allocation.
276
- if (lit.length !== 1 || !ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
277
-
278
- // Stash the index in a local when it isn't a constant — bounds + load both reference it.
279
- const isConstIdx = Array.isArray(idxIR) && idxIR[0] === 'i32.const'
280
- let idxRefIR = idxIR, idxBindIR = null
281
- if (!isConstIdx) {
282
- const idxTmp = tempI32('si')
283
- idxBindIR = ['local.set', `$${idxTmp}`, idxIR]
284
- idxRefIR = ['local.get', `$${idxTmp}`]
285
- }
286
-
287
- const ptr = temp('sc')
288
- inc('__str_byteLen', '__char_at')
289
- const charEq = ['if', ['result', 'i32'],
290
- ['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], idxRefIR],
291
- ['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR], ['i32.const', lit.charCodeAt(0)]]],
292
- ['else', ['i32.const', 0]]]
293
-
294
- const prelude = idxBindIR ? [['local.set', `$${ptr}`, asF64(emit(obj))], idxBindIR] : [['local.set', `$${ptr}`, asF64(emit(obj))]]
295
-
296
- if (vt === VAL.STRING) {
297
- return typed(['block', ['result', 'i32'], ...prelude, finish(charEq)], 'i32')
298
- }
299
-
300
- inc('__ptr_type', '__typed_idx', '__eq')
301
- const genericEq = ['call', '$__eq',
302
- ['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR]],
303
- asI64(emit(['str', lit]))]
304
- const cmp = ['if', ['result', 'i32'],
305
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], ['i32.const', PTR.STRING]],
306
- ['then', charEq],
307
- ['else', genericEq]]
308
- return typed(['block', ['result', 'i32'], ...prelude, finish(cmp)], 'i32')
309
- }
310
-
311
- // `<str>.{substr,substring,slice}(...) === <other>` whose substring is consumed
312
- // only by the equality: materialising it (an __alloc + byte copy) is pure waste.
313
- // Fuse to __str_{substring,slice}_eq, which clamp the range like the method then
314
- // byte-compare it against `other` in place. Sibling to emitSingleCharIndexCmp,
315
- // tried at the same `==`/`!=` sites. Motivating hot path: the parser keyword
316
- // scan, `cur.substr(i,l) === keyword`.
317
- function emitSubstringEqCmp(a, b, negate = false) {
318
- // Post-prepare a multi-arg call keeps its args as one comma list; a single
319
- // arg sits bare. Normalise either (and a flat tail, defensively) to a list.
320
- const callInfo = node => {
321
- if (!Array.isArray(node) || node[0] !== '()') return null
322
- const callee = node[1]
323
- if (!Array.isArray(callee) || callee[0] !== '.') return null
324
- const method = callee[2]
325
- if (method !== 'substr' && method !== 'substring' && method !== 'slice') return null
326
- let args = node.slice(2)
327
- if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') args = args[0].slice(1)
328
- while (args.length && args[args.length - 1] == null) args = args.slice(0, -1)
329
- return { recv: callee[1], method, args }
330
- }
331
-
332
- let info = callInfo(a), other = b, callIsLeft = true
333
- if (!info) { info = callInfo(b); other = a; callIsLeft = false }
334
- if (!info) return null
335
- const { recv, method, args } = info
336
- if (args.length > 2) return null
337
- if (!ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
338
-
339
- // The receiver must be a string. `substr`/`substring` name string-only methods,
340
- // so an unknown receiver is safe — the normal `.substr`/`.substring` emitter
341
- // assumes a string too. `slice` is also Array.prototype.slice — require a
342
- // statically-known STRING there. A known non-string receiver bails always.
343
- const vt = resolveValType(recv, valTypeOf, lookupValType)
344
- if (vt && vt !== VAL.STRING) return null
345
- if (method === 'slice' && vt !== VAL.STRING) return null
346
-
347
- const helper = method === 'slice' ? '__str_slice_eq' : '__str_substring_eq'
348
- inc(helper)
349
-
350
- // Absent end → byteLen: pass i32 max — every clamp arm floors it to the length.
351
- const TO_END = ['i32.const', 0x7FFFFFFF]
352
- let startIR, endIR
353
- if (method === 'substr' && args[1] != null) {
354
- // substr's 2nd arg is a length: end = start + length, so start reads twice.
355
- const s = tempI32('subS')
356
- startIR = ['local.tee', `$${s}`, args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))]
357
- endIR = ['i32.add', ['local.get', `$${s}`], asI32(emit(args[1]))]
358
- } else {
359
- startIR = args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))
360
- endIR = args[1] == null ? TO_END : asI32(emit(args[1]))
361
- }
362
-
363
- const finish = expr => negate ? ['i32.eqz', expr] : expr
364
-
365
- if (callIsLeft)
366
- return typed(finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR, asI64(emit(other))]), 'i32')
367
-
368
- // `other` is the source-left operand — evaluate it first to preserve order.
369
- const o = temp('subO')
370
- return typed(['block', ['result', 'i32'],
371
- ['local.set', `$${o}`, asF64(emit(other))],
372
- finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR,
373
- ['i64.reinterpret_f64', ['local.get', `$${o}`]]])], 'i32')
374
- }
375
-
376
- // === Flow-sensitive type refinement ===
377
- // Map typeof code (from resolveTypeof in prepare.js) → VAL kind. Undef/boolean/object have no
378
- // single VAL refinement, so they're excluded. String/number/function do.
379
- const TYPEOF_CODE_TO_VAL = { [-1]: VAL.NUMBER, [-2]: VAL.STRING, [-6]: VAL.CLOSURE }
380
-
381
- /** Extract refinements from a boolean condition AST.
382
- * `sense`: true = refine for then-branch, false = refine for else-branch (i.e. cond inverted).
383
- * Returns a Map<name, {val?: VAL, notString?: true}>. Walks && / || / ! accordingly. */
384
- function extractRefinements(cond, out, sense = true) {
385
- if (!Array.isArray(cond)) return out
386
- const op = cond[0]
387
- // ! flips sense
388
- if (op === '!') return extractRefinements(cond[1], out, !sense)
389
- // && under positive sense refines with union of both branches.
390
- // || under negative sense (De Morgan) similarly refines the else-branch.
391
- if (op === '&&' && sense) { extractRefinements(cond[1], out, true); extractRefinements(cond[2], out, true); return out }
392
- if (op === '||' && !sense) { extractRefinements(cond[1], out, false); extractRefinements(cond[2], out, false); return out }
393
- // typeof x == 'number' | 'string' | 'function' — sense must be positive for "==", negative for "!="
394
- if ((op === '==' || op === '===' || op === '!=' || op === '!==')) {
395
- const tp = typeofPredicate(cond)
396
- if (tp) {
397
- const wantPositive = tp.eq ? sense : !sense
398
- if (wantPositive) {
399
- const val = TYPEOF_CODE_TO_VAL[tp.code]
400
- if (val) mergeRefinement(out, tp.name, { val })
401
- } else if (tp.code === 'string' || tp.code === -2) {
402
- // Negative branch of typeof-string guard (e.g. post `if (typeof x === 'string') return`)
403
- // proves the binding is not a primitive string in the suffix scope — feeds B4's
404
- // length / subscript dispatch elision the same way write-shape evidence does.
405
- mergeRefinement(out, tp.name, { notString: true })
406
- }
407
- }
408
- return out
409
- }
410
- // Type-predicate calls under positive sense — refine by the asserted VAL.
411
- // Callee may be the flattened string 'Array.isArray' or the raw ['.', 'Array',
412
- // 'isArray'] pair; __is_map / __is_set / __is_typed come from jzify's
413
- // instanceof lowering as a bare string callee.
414
- if (op === '()' && sense && typeof cond[2] === 'string') {
415
- const callee = cond[1]
416
- const val = predicateRefinement(callee)
417
- if (val != null) { mergeRefinement(out, cond[2], { val }); return out }
418
- }
419
- return out
420
- }
421
-
422
- /** Map a call-callee shape to the VAL kind it asserts under positive sense, or null. */
423
- function predicateRefinement(callee) {
424
- if (callee === 'Array.isArray') return VAL.ARRAY
425
- if (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Array' && callee[2] === 'isArray')
426
- return VAL.ARRAY
427
- if (callee === '__is_map') return VAL.MAP
428
- if (callee === '__is_set') return VAL.SET
429
- if (callee === '__is_typed') return VAL.TYPED
430
- return null
431
- }
432
-
433
- /** Merge a refinement fact into the per-name slot. Later facts override; non-overlapping
434
- * fields union. Keeps the call-side simple (always assign through this). */
435
- function mergeRefinement(out, name, fact) {
436
- const cur = out.get(name)
437
- out.set(name, cur ? { ...cur, ...fact } : fact)
438
- }
439
-
440
- function unrollSmallConstFor(init, cond, step, body) {
441
- const end = smallConstForTripCount(init, cond, step)
442
- if (end == null) return null
443
- const name = init[1][1]
444
- if (containsNestedLoop(body)) {
445
- const nestedMode = ctx.transform.optimize?.nestedSmallConstForUnroll
446
- if (nestedMode !== true && (nestedMode !== 'auto' || !containsKnownTypedArrayIndex(body))) return null
447
- if (end * nestedSmallLoopBudget(body) > MAX_NESTED_FOR_UNROLL) return null
448
- }
449
- if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsDeclOf(body, name)) return null
450
- if (isReassigned(body, name)) return null
451
-
452
- const out = []
453
- for (let i = 0; i < end; i++) out.push(...emitFlat(cloneWithSubst(body, name, i)))
454
- return out
455
- }
456
-
457
- function canThrow(body, seen = new Set()) {
458
- if (!Array.isArray(body)) return false
459
- const op = body[0]
460
- if (op === 'throw') return true
461
- if (op === '=>') return false
462
- if (op === '()') {
463
- const callee = body[1]
464
- // A call can throw unless we can see the whole callee and prove it can't:
465
- // only direct calls into a resolvable, non-raw function body are traceable.
466
- // Indirect/method/builtin calls (callee not a plain name, or a name we can't
467
- // resolve) are conservatively throwing — a user `try` must wrap them.
468
- if (typeof callee !== 'string') return true
469
- const bodyName = ctx.func.directClosures?.get(callee)
470
- const f = ctx.func.map?.get(bodyName || callee)
471
- if (!f?.body || f.raw) return true
472
- if (!seen.has(f.name)) {
473
- seen.add(f.name)
474
- if (canThrow(f.body, seen)) return true
475
- }
476
- }
477
- for (let i = 1; i < body.length; i++) if (canThrow(body[i], seen)) return true
478
- return false
479
- }
480
-
481
- const isBoundName = name =>
482
- ctx.func.locals?.has(name) || ctx.func.current?.params?.some(p => p.name === name)
483
-
484
- // A source-defined function (carries a body) — as opposed to an imported name,
485
- // which `ctx.func.names` also holds but which has no body and may legitimately
486
- // share a name with a built-in emitter (e.g. an imported `parseInt`).
487
- const isUserFunc = name => !!ctx.func.map.get(name)?.body
488
-
489
- /** Emit pending `finally` cleanups for an abrupt control-flow exit.
490
- * Inner cleanups run before outer cleanups. While emitting each cleanup, remove
491
- * it from the active stack so `return` inside `finally` does not re-enter it. */
492
- function emitFinalizers() {
493
- const stack = ctx.func.finallyStack || []
494
- if (stack.length === 0) return []
495
- const saved = stack.slice()
496
- const out = []
497
- for (let i = saved.length - 1; i >= 0; i--) {
498
- ctx.func.finallyStack = saved.slice(0, i)
499
- out.push(...emitFlat(saved[i]))
500
- }
501
- ctx.func.finallyStack = saved
502
- return out
503
- }
504
-
505
- function withFinallyStack(stack, fn) {
506
- const prev = ctx.func.finallyStack || []
507
- ctx.func.finallyStack = stack
508
- try { return fn() }
509
- finally { ctx.func.finallyStack = prev }
510
- }
511
-
512
- /** Apply refinements for the duration of `fn()`. Restores prior state on return/throw. */
513
- function withRefinements(refs, body, fn) {
514
- if (!refs || refs.size === 0) return fn()
515
- const cur = ctx.func.refinements
516
- // Drop names that are reassigned in the body — refinement would be unsound.
517
- const saved = []
518
- for (const [name, val] of refs) {
519
- if (isReassigned(body, name)) continue
520
- saved.push([name, cur.get(name)])
521
- cur.set(name, val)
522
- }
523
- try { return fn() }
524
- finally {
525
- for (const [name, prev] of saved) {
526
- if (prev === undefined) cur.delete(name); else cur.set(name, prev)
527
- }
528
- }
529
- }
530
-
531
- /** Coerce an AST node to an i32 boolean, folding && / || at the boolean boundary. */
532
- export function toBool(node) {
533
- const op = Array.isArray(node) ? node[0] : null
534
- if (CMP_SET.has(op)) return emit(node)
535
- if (op === '&&') {
536
- const la = toBool(node[1]), lb = toBool(node[2])
537
- if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.and', la, lb], 'i32')
538
- return typed(['if', ['result', 'i32'], la, ['then', lb], ['else', ['i32.const', 0]]], 'i32')
539
- }
540
- if (op === '||') {
541
- const la = toBool(node[1]), lb = toBool(node[2])
542
- if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.or', la, lb], 'i32')
543
- return typed(['if', ['result', 'i32'], la, ['then', ['i32.const', 1]], ['else', lb]], 'i32')
544
- }
545
- return toBoolFromEmitted(emit(node))
546
- }
547
-
548
- /** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
549
- * i32 offset), else falls back to numeric WASM type coercion. */
550
- function emitArgForParam(ir, param) {
551
- if (param?.ptrKind != null) return ptrOffsetIR(ir, param.ptrKind)
552
- return asParamType(ir, param?.type)
553
- }
554
-
555
- /**
556
- * Materialize a multi-value function call as a heap array.
557
- * Call → store each result in temp → copy to allocated array → return pointer.
558
- */
559
- export function materializeMulti(callNode) {
560
- const name = callNode[1]
561
- const func = ctx.func.map.get(name)
562
- const n = func.sig.results.length
563
- const rawArgs = callNode.slice(2)
564
- const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
565
- ? rawArgs[0].slice(1) : rawArgs
566
- const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
567
- while (emittedArgs.length < func.sig.params.length)
568
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
569
- const temps = Array.from({ length: n }, () => temp())
570
- const out = allocPtr({ type: 1, len: n, tag: 'marr' })
571
- const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
572
- for (let k = n - 1; k >= 0; k--) ir.push(['local.set', `$${temps[k]}`])
573
- for (let k = 0; k < n; k++)
574
- ir.push(['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.const', k * 8]], ['local.get', `$${temps[k]}`]])
575
- ir.push(out.ptr)
576
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
577
- }
578
-
579
- /** Emit let/const initializations as typed local.set instructions. */
580
- export function emitDecl(...inits) {
581
- const result = []
582
- for (let ii = 0; ii < inits.length; ii++) {
583
- const i = inits[ii]
584
- if (typeof i === 'string') {
585
- const undef = undefExpr()
586
- if (ctx.func.boxed.has(i)) {
587
- const cell = ctx.func.boxed.get(i)
588
- ctx.func.locals.set(cell, 'i32')
589
- if (!ctx.func.preboxed?.has(i))
590
- result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
591
- result.push(['f64.store', ['local.get', `$${cell}`], undef])
592
- continue
593
- }
594
- if (isGlobal(i)) {
595
- if (!ctx.scope.globalTypes.has(i)) result.push(['global.set', `$${i}`, undef])
596
- continue
597
- }
598
- result.push(['local.set', `$${i}`, undef])
599
- continue
600
- }
601
- if (!Array.isArray(i) || i[0] !== '=') continue
602
- const [, name, init] = i
603
- if (typeof name !== 'string' || init == null) continue
604
-
605
- // SRoA flat object: `let o = {a:1, b:2}` — dissolve fields into `o#i`
606
- // locals, no heap alloc. Each field local ← asF64(value). Reads/writes are
607
- // rewritten by the `.`/`[]` flat hooks. See scanFlatObjects (analyze.js).
608
- // Monotonic-extension fields (`o.newProp = …`) carry no literal value —
609
- // they init to undefined so a read before the write matches JS.
610
- const flatDecl = ctx.func.flatObjects?.get(name)
611
- if (flatDecl && Array.isArray(init) && init[0] === '{}') {
612
- for (let j = 0; j < flatDecl.names.length; j++)
613
- result.push(['local.set', `$${name}#${j}`,
614
- flatDecl.values[j] === undefined ? undefExpr() : asF64(emit(flatDecl.values[j]))])
615
- continue
616
- }
617
-
618
- // Multi-value ephemeral destructuring — skip heap alloc when temp is
619
- // assigned from a multi-value call then immediately destructured element-by-element.
620
- if (name.startsWith(T) && Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string'
621
- && ctx.func.names?.has(init[1])) {
622
- const func = ctx.func.map.get(init[1])
623
- const n = func?.sig.results.length
624
- if (n > 1) {
625
- const targets = []
626
- let match = true
627
- for (let k = 0; k < n && match; k++) {
628
- const next = inits[ii + 1 + k]
629
- if (!Array.isArray(next) || next[0] !== '=' || typeof next[1] !== 'string') { match = false; break }
630
- const rhs = next[2]
631
- if (!Array.isArray(rhs) || rhs[0] !== '[]' || rhs[1] !== name) { match = false; break }
632
- const idx = rhs[2]
633
- if (!Array.isArray(idx) || idx[0] != null || idx[1] !== k) { match = false; break }
634
- if (ctx.func.boxed.has(next[1]) || isGlobal(next[1])) { match = false; break }
635
- targets.push(next[1])
636
- }
637
- if (match && targets.length === n) {
638
- const rawArgs = init.slice(2)
639
- const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
640
- ? rawArgs[0].slice(1) : rawArgs
641
- const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
642
- while (emittedArgs.length < func.sig.params.length)
643
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
644
- result.push(['call', `$${init[1]}`, ...emittedArgs])
645
- for (let k = n - 1; k >= 0; k--)
646
- result.push(['local.set', `$${targets[k]}`])
647
- ii += n
648
- continue
649
- }
650
- }
651
- }
652
- // No-copy slice view: `let t = s.slice(...)` whose result scanSliceViews
653
- // proved never escapes — lower the initializer to a SLICE_BIT view instead
654
- // of a copying slice. Everything downstream treats `t` as an ordinary
655
- // string. Gated here (not in the analysis) on a statically-known STRING
656
- // receiver — param types are settled only by emit time — and on plain-local
657
- // carriers (boxed/global escape); any miss falls back to the copying slice.
658
- let viewInit = null
659
- if (ctx.func.sliceViews?.has(name) && !ctx.func.boxed.has(name) && !isGlobal(name)
660
- && Array.isArray(init) && init[0] === '()'
661
- && Array.isArray(init[1]) && init[1][0] === '.' && init[1][2] === 'slice') {
662
- const recv = init[1][1]
663
- const recvVt = typeof recv === 'string' ? keyValType(recv) : valTypeOf(recv)
664
- if (recvVt === VAL.STRING) {
665
- const raw = init[2]
666
- const sa = raw == null ? [] : Array.isArray(raw) && raw[0] === ',' ? raw.slice(1) : [raw]
667
- viewInit = ctx.core.emit['.string:slice#view'](recv, sa[0], sa[1])
668
- }
669
- }
670
-
671
- const isObjLit = Array.isArray(init) && init[0] === '{}'
672
- if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
673
- const val = viewInit || emit(init)
674
- if (isObjLit) ctx.schema.targetStack.pop()
675
- // Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
676
- // Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
677
- // isReassigned is conservative across nested arrow shadows — we miss the optimization
678
- // rather than emit a wrong direct call.
679
- if (Array.isArray(init) && init[0] === '=>' && val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
680
- && ctx.func.body && !isReassigned(ctx.func.body, name)) {
681
- if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
682
- ctx.func.directClosures.set(name, val.closureBodyName)
683
- }
684
- if (ctx.func.boxed.has(name)) {
685
- const cell = ctx.func.boxed.get(name)
686
- ctx.func.locals.set(cell, 'i32')
687
- if (!ctx.func.preboxed?.has(name))
688
- result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
689
- result.push(['f64.store', ['local.get', `$${cell}`], asF64(val)])
690
- continue
691
- }
692
- if (isGlobal(name)) {
693
- // Unboxed pointer const globals carry the raw i32 offset; init coerces via asPtrOffset.
694
- const grep = repOfGlobal(name)
695
- if (grep?.ptrKind != null) {
696
- result.push(['global.set', `$${name}`, asPtrOffset(val, grep.ptrKind)])
697
- continue
698
- }
699
- // Pre-folded numeric const globals have their init baked into the decl — skip.
700
- if (ctx.scope.globalTypes.has(name)) continue
701
- result.push(['global.set', `$${name}`, asF64(val)])
702
- continue
703
- }
704
- const localType = ctx.func.locals.get(name) || 'f64'
705
- let ptrKind = repOf(name)?.ptrKind
706
- // Emit-time rep mutation (lifecycle: analysis → emit transition).
707
- // Inherit ptrKind from a pointer-ABI RHS: destructure temps (`__d0 = v`) and other
708
- // fresh let-bindings whose init is already an unboxed pointer. Without this, readVar
709
- // returns an untyped i32 local.get and later `asF64` emits a numeric convert instead
710
- // of a ptr-rebox. Safe because emitDecl runs once per let/const binding — no prior
711
- // emit-time read could have observed the unset rep.
712
- if (ptrKind == null && val.ptrKind != null && localType === 'i32' && !ctx.func.boxed?.has(name)) {
713
- updateRep(name, { ptrKind: val.ptrKind })
714
- ptrKind = val.ptrKind
715
- if (val.ptrAux != null) {
716
- updateRep(name, { ptrAux: val.ptrAux })
717
- // OBJECT-only: aux *is* the schemaId; mirror to ctx.schema.vars + rep.schemaId so
718
- // .prop slot resolution sees a precise binding. TYPED/CLOSURE aux carries other
719
- // semantics (elem code / funcIdx) and must not leak into schema lookups.
720
- if (val.ptrKind === VAL.OBJECT && !ctx.schema.vars?.has(name)) {
721
- ctx.schema.vars.set(name, val.ptrAux)
722
- updateRep(name, { schemaId: val.ptrAux })
723
- }
724
- }
725
- }
726
- let coerced
727
- if (ptrKind != null) {
728
- // Unboxed pointer local — extract i32 offset from NaN-boxed f64 via reinterpret, not numeric trunc.
729
- // CLOSURE init carries funcIdx in val.closureFuncIdx; persist it on the rep so a later
730
- // asF64 (escape: store, return, indirect-call rebox) reconstructs the correct table slot.
731
- // Emit-time mutation — analyzeValTypes never sees closureFuncIdx.
732
- if (ptrKind === VAL.CLOSURE && val.closureFuncIdx != null && repOf(name)?.ptrAux == null)
733
- updateRep(name, { ptrAux: val.closureFuncIdx })
734
- coerced = val.ptrKind === ptrKind ? val
735
- : typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
736
- } else {
737
- coerced = localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
738
- }
739
- if (!(isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length))
740
- result.push(['local.set', `$${name}`, coerced])
741
-
742
- const schemaId = ctx.schema.idOf?.(name)
743
- if (ctx.func.localProps?.has(name) && schemaId != null) {
744
- const schema = ctx.schema.resolve(name)
745
- if (schema?.[0] === '__inner__') {
746
- inc('__alloc_hdr', '__mkptr')
747
- const bt = `${T}bx${ctx.func.uniq++}`
748
- ctx.func.locals.set(bt, 'i32')
749
- const innerName = `${name}${T}inner`
750
- ctx.func.locals.set(innerName, 'f64')
751
- result.push(
752
- ['local.set', `$${innerName}`, ['local.get', `$${name}`]],
753
- ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
754
- ['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
755
- ...schema.slice(1).map((_, j) =>
756
- ['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
757
- ['local.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
758
- }
759
- }
760
- }
761
- return result.length === 0 ? null : result.length === 1 ? result[0] : result
762
- }
763
-
764
- /**
765
- * Copy a spread source's elements into a destination array.
766
- *
767
- * `dest` is the destination data-base i32 local; `posLocal` the element index to
768
- * start writing at — advanced by the source length on exit. An ARRAY source is a
769
- * contiguous block of f64 NaN-boxes, so it copies with a single `memory.copy`; a
770
- * string/typed source needs a per-element decode. The source's *type* is
771
- * loop-invariant — it cannot change while the spread runs — so when it is not
772
- * statically known it is resolved exactly once (one `__ptr_type`) and branched,
773
- * never re-checked per element. Returns a list of IR instructions.
774
- */
775
- function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
776
- const srcI64 = () => ['i64.reinterpret_f64', ['local.get', `$${srcLocal}`]]
777
- const destAddr = idx => ['i32.add', ['local.get', `$${dest}`], ['i32.shl', idx, ['i32.const', 3]]]
778
- const arrCopy = () => (inc('__ptr_offset'),
779
- ['memory.copy', destAddr(['local.get', `$${posLocal}`]),
780
- ['call', '$__ptr_offset', srcI64()],
781
- ['i32.shl', ['local.get', `$${srcLenLocal}`], ['i32.const', 3]]])
782
- const scalarLoop = () => {
783
- const sidx = `${T}sidx${ctx.func.uniq++}`
784
- ctx.func.locals.set(sidx, 'i32')
785
- const loopId = ctx.func.uniq++
786
- const elem = ctx.module.modules['string']
787
- ? ['if', ['result', 'f64'],
788
- ['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
789
- ['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
790
- ['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]]
791
- : (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
792
- // Reset the counter on each entry — WASM zeroes locals once at function
793
- // entry, but this loop re-executes when the spread sits inside a JS loop;
794
- // a stale `sidx` (= prior srcLen) would skip the copy entirely.
795
- return ['block', `$break${loopId}`,
796
- ['local.set', `$${sidx}`, ['i32.const', 0]],
797
- ['loop', `$loop${loopId}`,
798
- ['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
799
- ['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
800
- ['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
801
- ['br', `$loop${loopId}`]]]
802
- }
803
- const advance = ['local.set', `$${posLocal}`,
804
- ['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
805
- if (staticVT === VAL.ARRAY) return [arrCopy(), advance]
806
- if (staticVT === VAL.STRING || staticVT === VAL.TYPED) return [scalarLoop(), advance]
807
- inc('__ptr_type')
808
- return [['if',
809
- ['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.ARRAY]],
810
- ['then', arrCopy()],
811
- ['else', scalarLoop()]], advance]
812
- }
813
-
814
- /**
815
- * Build an array from items, handling ['__spread', expr] markers.
816
- * Split into sections (normal arrays and spreads), then copy all into result.
817
- */
818
- export function buildArrayWithSpreads(items) {
819
- const spreads = []
820
- for (let i = 0; i < items.length; i++) {
821
- if (Array.isArray(items[i]) && items[i][0] === '__spread') {
822
- spreads.push({ pos: i, expr: items[i][1] })
823
- }
824
- }
825
-
826
- if (spreads.length === 0) {
827
- return emit(['[', ...items])
828
- }
829
-
830
- const sections = []
831
- let currentArray = []
832
-
833
- for (let i = 0; i < items.length; i++) {
834
- if (Array.isArray(items[i]) && items[i][0] === '__spread') {
835
- if (currentArray.length > 0) {
836
- sections.push({ type: 'array', items: currentArray })
837
- currentArray = []
838
- }
839
- sections.push({ type: 'spread', expr: items[i][1] })
840
- } else {
841
- currentArray.push(items[i])
842
- }
843
- }
844
- if (currentArray.length > 0) {
845
- sections.push({ type: 'array', items: currentArray })
846
- }
847
-
848
- // A single all-normal section is a plain literal — defer to the `[` emitter.
849
- // A single *spread* section is NOT shortcut to `emit(sec.expr)`: that would
850
- // alias the source, but `[...x]` must yield a fresh array. It falls through
851
- // to the alloc + emitSpreadCopy path below, which copies.
852
- if (sections.length === 1 && sections[0].type === 'array') {
853
- return emit(['[', ...sections[0].items])
854
- }
855
-
856
- const len = tempI32('len')
857
- const pos = tempI32('pos')
858
- const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
859
- const result = out.local
860
-
861
- const ir = []
862
- inc('__len')
863
-
864
- // Pass 1 — evaluate every section IN SOURCE ORDER into temps. JS spread keeps
865
- // strict left-to-right order: a later spread whose source mutates an earlier
866
- // element's input must still observe the pre-mutation value. Array items
867
- // become per-item f64 temps; spreads become a ptr temp + a cached __len.
868
- for (const sec of sections) {
869
- if (sec.type === 'array') {
870
- sec.itemLocals = []
871
- for (let i = 0; i < sec.items.length; i++) {
872
- const it = `${T}ai${ctx.func.uniq++}`
873
- ctx.func.locals.set(it, 'f64')
874
- sec.itemLocals.push(it)
875
- ir.push(['local.set', `$${it}`, asF64(emit(sec.items[i]))])
876
- }
877
- } else {
878
- sec.local = `${T}sp${ctx.func.uniq++}`
879
- ctx.func.locals.set(sec.local, 'f64')
880
- sec.lenLocal = `${T}spl${ctx.func.uniq++}`
881
- ctx.func.locals.set(sec.lenLocal, 'i32')
882
- // A materialized multi-value is not a statically-typed pointer — let
883
- // emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
884
- sec.val = multiCount(sec.expr) ? undefined : valTypeOf(sec.expr)
885
- const n = multiCount(sec.expr)
886
- ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
887
- // Cache __len once per spread; reused below for total-len sum and the copy.
888
- ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
889
- }
890
- }
891
-
892
- // Pass 2 — total length (array sections statically sized, spreads cached above).
893
- ir.push(['local.set', `$${len}`, ['i32.const', 0]])
894
- for (const sec of sections) {
895
- if (sec.type === 'array') {
896
- ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
897
- } else {
898
- ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['local.get', `$${sec.lenLocal}`]]])
899
- }
900
- }
901
-
902
- // Pass 3 — allocate exact, then store the pre-evaluated temps.
903
- ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
904
- for (const sec of sections) {
905
- if (sec.type === 'array') {
906
- for (const it of sec.itemLocals) {
907
- ir.push(
908
- ['f64.store',
909
- ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
910
- ['local.get', `$${it}`]],
911
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
912
- )
913
- }
914
- } else {
915
- ir.push(...emitSpreadCopy(result, pos, sec.local, sec.lenLocal, sec.val))
916
- }
917
- }
918
-
919
- ir.push(out.ptr)
920
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
921
- }
922
-
923
- /** Check if node is a block body (statement list, not object literal/expression) */
924
- const isBlockBody = n => Array.isArray(n) && n[0] === '{}' && n.length === 2 && Array.isArray(n[1]) && STMT_OPS.has(n[1]?.[0])
925
-
926
- /** Emit node in void context: emit + drop any value. Block bodies route through emitBody. */
927
- export function emitFlat(node) {
928
- if (isBlockBody(node)) return emitBody(node)
929
- const ir = emit(node, 'void')
930
- const items = flat(ir)
931
- if (ir?.type && ir.type !== 'void') items.push('drop')
932
- return items
933
- }
934
-
935
- /** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitFlat per statement.
936
- * Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
937
- * rest of the enclosing block. Refinements added here are rolled back on block exit. */
938
- export function emitBody(node) {
939
- const inner = node[1]
940
- const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
941
- const out = []
942
- const accumulated = []
943
- for (let i = 0; i < stmts.length; i++) {
944
- const s = stmts[i]
945
- if (s == null || typeof s === 'number') continue
946
- out.push(...emitFlat(s))
947
- // After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
948
- // Skip names that are reassigned later — refinement would be unsound past the assignment.
949
- if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
950
- const refs = extractRefinements(s[1], new Map(), false)
951
- for (const [name, fact] of refs) {
952
- let reassigned = false
953
- for (let j = i + 1; j < stmts.length; j++)
954
- if (isReassigned(stmts[j], name)) { reassigned = true; break }
955
- if (reassigned) continue
956
- const cur = ctx.func.refinements.get(name)
957
- accumulated.push([name, cur])
958
- // Merge so sibling early-returns layering on the same name compose
959
- // (e.g. `if (typeof x === 'string') return; if (Array.isArray(x)) return;`
960
- // leaves both `notString: true` and would-be array exclusion stacked).
961
- ctx.func.refinements.set(name, cur ? { ...cur, ...fact } : fact)
962
- }
963
- }
964
- }
965
- // Restore prior refinements on block exit.
966
- for (let i = accumulated.length - 1; i >= 0; i--) {
967
- const [name, prev] = accumulated[i]
968
- if (prev === undefined) ctx.func.refinements.delete(name); else ctx.func.refinements.set(name, prev)
969
- }
970
- return out
971
- }
972
-
973
- // A VAL.BOOL value rides the cheap 0/1 numeric carrier, and `ToNumber(bool)` is
974
- // exactly that carrier — so for relational / loose-equality coercion a boolean
975
- // behaves identically to a number. Normalize it before the type-directed compare
976
- // dispatch (the BOOL fact still drives typeof / String / boundary boxing; only
977
- // these arithmetic-shaped operators read it as numeric).
978
- const numericVal = vt => vt === VAL.BOOL ? VAL.NUMBER : vt
979
-
980
- // Primitive value-type classes for strict-equality type-mismatch folding. Two
981
- // operands of different known classes — when at least one is a primitive — can
982
- // never be `===` (number/boolean/string/bigint don't cross-coerce under `===`).
983
- // Two *reference* kinds (array vs object, …) fall through to the shared ref-eq
984
- // path instead, which already resolves distinct pointers to `false`.
985
- const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
986
-
987
- /**
988
- * Strict `===`/`!==`. Unlike loose `==`, no coercion: a statically-known type
989
- * mismatch folds to a constant (`true === 1` → false, `"1" === 1` → false). When
990
- * the types match — or one side is statically unknown — the result is bit-for-bit
991
- * identical to loose `==` on same-type operands, so we delegate to it.
992
- *
993
- * Two carrier-level limitations remain (documented gaps, not regressions):
994
- * • booleans and numbers share the 0/1 carrier, so `1 === trueDynamic` can only
995
- * be told apart when the boolean's type is statically known;
996
- * • jz unifies `null` and `undefined` into one NaN-boxed sentinel, so
997
- * `null === undefined` is `true` (same as `==`).
998
- */
999
- function emitStrictEq(a, b, negate) {
1000
- // `typeof x === 'type'` (prepare rewrote the literal to a numeric code) — typeof
1001
- // always yields a string, so strict and loose agree; reuse the loose lowering.
1002
- const tc = emitTypeofCmp(a, b, negate ? 'ne' : 'eq'); if (tc) return tc
1003
- // Known, differing primitive classes can never be strictly equal.
1004
- const rawA = resolveValType(a, valTypeOf, lookupValType)
1005
- const rawB = resolveValType(b, valTypeOf, lookupValType)
1006
- if (rawA && rawB && rawA !== rawB && (STRICT_PRIM.has(rawA) || STRICT_PRIM.has(rawB)))
1007
- return emitNum(negate ? 1 : 0)
1008
- // Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
1009
- return emitter[negate ? '!=' : '=='](a, b)
1010
- }
1011
-
1012
- /** Comparison op factory with constant folding. */
1013
- const cmpOp = (i32op, f64op, fn) => (a, b) => {
1014
- const va = emit(a), vb = emit(b)
1015
- // Skip the const-fold for `.unsigned` operands: `litVal` is the signed bit pattern
1016
- // (-1, not 4294967295), so folding the order would be wrong. Fall through to the
1017
- // f64 widen path below, which converts each operand by its own signedness.
1018
- if (isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
1019
- // String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
1020
- // (always false), so without this the spec-correct `"a" < "b"` returns 0.
1021
- // Route both-STRING operands through __str_cmp's three-way result, then apply
1022
- // the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
1023
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1024
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1025
- if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
1026
- const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
1027
- return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
1028
- }
1029
- if (vta === VAL.STRING && vtb === VAL.STRING) {
1030
- return typed([`i32.${i32op}`, ctx.abi.string.ops.cmp(asF64(va), asF64(vb), ctx), ['i32.const', 0]], 'i32')
1031
- }
1032
- // Exactly one operand is a known string; the other has no static type, so it
1033
- // may hold a string pointer at runtime (e.g. `c >= '0'` where `c` came from
1034
- // `s[i]` on an untyped receiver). JS relational compare is lexicographic only
1035
- // when *both* sides are strings, else it ToNumbers both. The f64 path below
1036
- // would compare the unknown side's NaN-boxed string bits as a float (NaN ⇒
1037
- // always false), so dispatch at runtime on the unknown side: string → __str_cmp
1038
- // three-way; else ToNumber both. Mirrors `+`'s __is_str_key string dispatch.
1039
- // Gated on a *known-string* counterpart, so numeric loops (`i < n`) never pay
1040
- // the check — comparing against a string literal signals string intent.
1041
- if (((vta === VAL.STRING && vtb == null) || (vtb === VAL.STRING && vta == null)) && ctx.abi.string?.ops?.cmp) {
1042
- const unkIsA = vta == null
1043
- const ta = temp('cmp'), tb = temp('cmp')
1044
- inc('__is_str_key')
1045
- const getA = typed(['local.get', `$${ta}`], 'f64'), getB = typed(['local.get', `$${tb}`], 'f64')
1046
- const check = ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${unkIsA ? ta : tb}`]]]
1047
- const strCmp = [`i32.${i32op}`, ctx.abi.string.ops.cmp(getA, getB, ctx), ['i32.const', 0]]
1048
- const numCmp = [`f64.${f64op}`, toNumF64(a, getA), toNumF64(b, getB)]
1049
- return typed(['block', ['result', 'i32'],
1050
- ['local.set', `$${ta}`, asF64(va)],
1051
- ['local.set', `$${tb}`, asF64(vb)],
1052
- ['if', ['result', 'i32'], check, ['then', strCmp], ['else', numCmp]]], 'i32')
1053
- }
1054
- if (vta === VAL.DATE || vtb === VAL.DATE) {
1055
- const dateNum = (node, v, vt) => {
1056
- if (vt !== VAL.DATE) return toNumF64(node, v)
1057
- const ptr = v.ptrKind === VAL.DATE
1058
- ? v
1059
- : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
1060
- return typed(['f64.load', ptr], 'f64')
1061
- }
1062
- return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
1063
- }
1064
- if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
1065
- return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
1066
- if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
1067
- return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
1068
- // An `.unsigned` i32 operand ([0, 2^32)) can't share a signed i32 compare with a
1069
- // possibly-signed one: mixed sign inverts the order (3 < 0xFFFFFFFF unsigned, but
1070
- // 3 > -1 signed). Widen to f64, where asF64 converts each operand by its own
1071
- // signedness (convert_i32_u for unsigned, _s otherwise) to its true numeric value.
1072
- if (!va.unsigned && !vb.unsigned) {
1073
- const ai = intConstValue(a), bi = intConstValue(b)
1074
- if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
1075
- if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
1076
- if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${i32op}`, va, vb], 'i32')
1077
- }
1078
- return typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
1079
- }
1080
-
1081
- function needsRelationalToNumber(expr, vt) {
1082
- if (vt === VAL.STRING) return true
1083
- if (vt != null) return false
1084
- return mayReadBoxedValue(expr)
1085
- }
1086
-
1087
- function needsLooseEqualityToNumber(expr, vt) {
1088
- if (vt === VAL.STRING) return true
1089
- if (vt != null) return false
1090
- return mayReadBoxedValue(expr)
1091
- }
1092
-
1093
- function looseNumberEq(numIR, otherNode, otherIR, negate = false) {
1094
- const t = temp('eq')
1095
- const other = typed(['local.get', `$${t}`], 'f64')
1096
- const cmp = ['f64.eq', asF64(numIR), toNumF64(otherNode, other)]
1097
- return typed(['block', ['result', 'i32'],
1098
- ['local.set', `$${t}`, asF64(otherIR)],
1099
- ['if', ['result', 'i32'], isNullish(other),
1100
- ['then', ['i32.const', negate ? 1 : 0]],
1101
- ['else', negate ? ['i32.eqz', cmp] : cmp]]], 'i32')
1102
- }
1103
-
1104
- function mayReadBoxedValue(expr) {
1105
- return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
1106
- }
1107
-
1108
- function intConstValue(expr) {
1109
- if (typeof expr === 'number' && Number.isInteger(expr)) return expr
1110
- if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
1111
- if (typeof expr === 'string') {
1112
- const v = repOf(expr)?.intConst
1113
- if (v != null) return v
1114
- }
1115
- return null
1116
- }
1117
-
1118
- function bigintUnsignedBound(expr) {
1119
- const n = bigintConstValue(expr)
1120
- return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
1121
- }
1122
-
1123
- function bigintConstValue(expr) {
1124
- if (typeof expr === 'bigint') return expr
1125
- if (!Array.isArray(expr)) return null
1126
- if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
1127
- if (expr[0] === 'u-') {
1128
- const n = bigintConstValue(expr[1])
1129
- return n == null ? null : -n
1130
- }
1131
- return null
1132
- }
1133
-
1134
- function arrayIndexKey(key) {
1135
- const n = Number(key)
1136
- const u = n >>> 0
1137
- return String(u) === key && u !== 0xffffffff ? u : null
1138
- }
1139
-
1140
- /** Compound assignment: read → op → write back (via readVar/writeVar). */
1141
- function compoundAssign(name, val, f64op, i32op) {
1142
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1143
- const void_ = _expect === 'void'
1144
- const va = readVar(name), vb = emit(val)
1145
- // Peel f64.convert_i32_s/u when va is i32 — typed-array integer reads wrap their
1146
- // i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
1147
- // raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
1148
- // hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
1149
- let vbi = vb
1150
- if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
1151
- Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
1152
- const inner = vb[1]
1153
- vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
1154
- }
1155
- if (i32op && va.type === 'i32' && vbi.type === 'i32')
1156
- return writeVar(name, i32op(va, vbi), void_)
1157
- return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
1158
- }
1159
-
1160
- /**
1161
- * Core emitter table. Maps AST ops to WASM IR generators.
1162
- * ctx.core.emit is seeded with a flat copy of this object on reset;
1163
- * modules add or override ops on ctx.core.emit directly.
1164
- * @type {Record<string, (...args: any[]) => Array>}
1165
- */
1166
- export const emitter = {
1167
- // === Spread operator ===
1168
- // Note: spread is handled specially in call contexts; this catches stray uses
1169
- '...': () => err('Spread (...) can only be used in function/method calls or array literals'),
1170
-
1171
- // === Statements ===
1172
-
1173
- ';': (...args) => {
1174
- const out = []
1175
- for (const a of args) {
1176
- out.push(...emitFlat(a))
1177
- }
1178
- return out
1179
- },
1180
- '{': (...args) => args.map(emit).filter(x => x != null),
1181
- ',': (...args) => {
1182
- const results = args.map(emit).filter(x => x != null)
1183
- if (results.length === 0) return null
1184
- if (results.length === 1) return results[0]
1185
- const last = results[results.length - 1]
1186
- // Flatten: multi-instruction arrays (from ';') need spreading, typed nodes need drop
1187
- const spread = r => Array.isArray(r) && Array.isArray(r[0]) ? r : [r]
1188
- const dropSpread = r => r.type ? [['drop', r]] : spread(r)
1189
- // If last expression is void (store, etc.), add explicit return value
1190
- if (!last.type) {
1191
- return typed(['block', ['result', 'f64'],
1192
- ...results.flatMap(dropSpread),
1193
- ['f64.const', 0]], 'f64')
1194
- }
1195
- return typed(['block', ['result', last.type],
1196
- ...results.slice(0, -1).flatMap(dropSpread), last], last.type)
1197
- },
1198
- 'let': emitDecl,
1199
- 'const': emitDecl,
1200
- 'export': () => null,
1201
- // 'block' can appear from jzify transforming labeled blocks or as WASM block IR
1202
- 'block': (...args) => {
1203
- // WASM block IR: first arg is ['result', type] → pass through, preserve type
1204
- if (Array.isArray(args[0]) && args[0][0] === 'result')
1205
- return typed(['block', ...args], args[0][1])
1206
- const inner = args.length === 1 ? args[0] : [';', ...args]
1207
- return emitFlat(['{}', inner])
1208
- },
1209
-
1210
- 'throw': expr => {
1211
- ctx.runtime.throws = ctx.runtime.userThrows = true
1212
- const thrown = temp()
1213
- return typed(['block',
1214
- ['local.set', `$${thrown}`, asF64(emit(expr))],
1215
- ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${thrown}`]]],
1216
- ['throw', '$__jz_err', ['local.get', `$${thrown}`]]], 'void')
1217
- },
1218
-
1219
- 'catch': (body, errName, handler) => {
1220
- if (!canThrow(body)) return emitFlat(body)
1221
-
1222
- ctx.runtime.throws = ctx.runtime.userThrows = true
1223
- const id = ctx.func.uniq++
1224
- ctx.func.locals.set(errName, 'f64')
1225
- const prev = ctx.func.inTry; ctx.func.inTry = true
1226
- let bodyIR; try { bodyIR = emitFlat(body) } finally { ctx.func.inTry = prev }
1227
- const handlerIR = emitFlat(handler)
1228
- return typed(['block', `$outer${id}`, ['result', 'f64'],
1229
- ['block', `$catch${id}`, ['result', 'f64'],
1230
- ['try_table', ['catch', '$__jz_err', `$catch${id}`],
1231
- ...bodyIR],
1232
- ['f64.const', 0],
1233
- ['br', `$outer${id}`]],
1234
- ['local.set', `$${errName}`],
1235
- ...handlerIR,
1236
- ['f64.const', 0]], 'f64')
1237
- },
1238
-
1239
- 'finally': (body, cleanup) => {
1240
- if (!canThrow(body)) {
1241
- const parentStack = ctx.func.finallyStack || []
1242
- const activeStack = parentStack.concat([cleanup])
1243
- const bodyIR = withFinallyStack(activeStack, () => emitFlat(body))
1244
- const cleanupIR = isTerminator(body) ? [] : withFinallyStack(parentStack, () => emitFlat(cleanup))
1245
- return [...bodyIR, ...cleanupIR]
1246
- }
1247
-
1248
- ctx.runtime.throws = ctx.runtime.userThrows = true
1249
- const id = ctx.func.uniq++
1250
- const errLocal = temp('err')
1251
- const parentStack = ctx.func.finallyStack || []
1252
- const activeStack = parentStack.concat([cleanup])
1253
-
1254
- const prevTry = ctx.func.inTry
1255
- ctx.func.inTry = true
1256
- const bodyIR = withFinallyStack(activeStack, () => {
1257
- try { return emitFlat(body) }
1258
- finally { ctx.func.inTry = prevTry }
1259
- })
1260
- const normalCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
1261
- const throwCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
1262
-
1263
- return ['block', `$fin_done${id}`,
1264
- ['block', `$fin_catch${id}`, ['result', 'f64'],
1265
- ['try_table', ['catch', '$__jz_err', `$fin_catch${id}`],
1266
- ...bodyIR],
1267
- ...normalCleanup,
1268
- ['br', `$fin_done${id}`]],
1269
- ['local.set', `$${errLocal}`],
1270
- ...throwCleanup,
1271
- ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${errLocal}`]]],
1272
- ['throw', '$__jz_err', ['local.get', `$${errLocal}`]]]
1273
- },
1274
-
1275
- 'return': expr => {
1276
- const finalizers = emitFinalizers()
1277
- const finalizerBlock = () => [['block', ...finalizers]]
1278
- if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
1279
- const vals = expr.slice(1).map(e => asF64(emit(e)))
1280
- if (finalizers.length === 0) return typed(['return', ...vals], 'void')
1281
- const names = vals.map(() => temp('ret'))
1282
- return [
1283
- ...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
1284
- ...finalizerBlock(),
1285
- typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
1286
- ]
1287
- }
1288
- if (expr == null) return [...finalizers, typed(['return', NULL_IR], 'void')]
1289
- const rt = ctx.func.current?.results[0] || 'f64'
1290
- const pk = ctx.func.current?.ptrKind
1291
- const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
1292
- const ty = pk != null ? 'i32' : rt
1293
- const tcoed = tcoTailRewrite(ir, ty)
1294
- if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
1295
- return typed(tcoed, 'void')
1296
- }
1297
- if (finalizers.length > 0) {
1298
- const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
1299
- return [
1300
- ['local.set', `$${name}`, tcoed],
1301
- ...finalizerBlock(),
1302
- typed(['return', ['local.get', `$${name}`]], 'void'),
1303
- ]
1304
- }
1305
- return typed(['return', tcoed], 'void')
1306
- },
1307
-
1308
- // === Assignment ===
1309
-
1310
- '=': (name, val) => {
1311
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1312
- const void_ = _expect === 'void'
1313
- // Array index assignment: arr[i] = x
1314
- if (Array.isArray(name) && name[0] === '[]') {
1315
- const [, arr, idx] = name
1316
- const keyType = keyValType(idx)
1317
- // A provably-numeric index name — an int-certain loop counter or a
1318
- // NUMBER-typed local — can never be a string key, so the runtime
1319
- // `__is_str_key` → `__dyn_set` dispatch is dead. Mirrors the index *read*
1320
- // path (`intIndexIR`), closing the read/write asymmetry on `arr[i] = …`
1321
- // inside refined-array loops (e.g. watr's recursive AST walkers).
1322
- const idxNumericName = typeof idx === 'string' &&
1323
- (repOf(idx)?.intCertain === true || repOf(idx)?.val === VAL.NUMBER)
1324
- const useRuntimeKeyDispatch = !idxNumericName &&
1325
- (keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING))
1326
- const keyExpr = asF64(emit(idx))
1327
- const valueExpr = asF64(emit(val))
1328
- const storeArrayValue = (arrExpr, idxNode, persist) => {
1329
- const arrTmp = `${T}asi${ctx.func.uniq++}`
1330
- const idxTmp = `${T}asj${ctx.func.uniq++}`
1331
- const valTmp = `${T}asv${ctx.func.uniq++}`
1332
- ctx.func.locals.set(arrTmp, 'f64')
1333
- ctx.func.locals.set(idxTmp, 'i32')
1334
- ctx.func.locals.set(valTmp, 'f64')
1335
- inc('__arr_set_idx_ptr')
1336
- const body = [
1337
- ['local.set', `$${arrTmp}`, arrExpr],
1338
- ['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
1339
- ['local.set', `$${valTmp}`, valueExpr],
1340
- ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
1341
- ]
1342
- if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
1343
- body.push(['local.get', `$${valTmp}`])
1344
- return typed(['block', ['result', 'f64'], ...body], 'f64')
1345
- }
1346
- const setDyn = () => {
1347
- if (ctx.transform.strict)
1348
- err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
1349
- inc('__dyn_set')
1350
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
1351
- }
1352
- const dispatchKey = (numericIR) => {
1353
- if (ctx.transform.strict)
1354
- err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
1355
- const keyTmp = temp()
1356
- return typed(['block', ['result', 'f64'],
1357
- ['local.set', `$${keyTmp}`, keyExpr],
1358
- ['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
1359
- ['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
1360
- ['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
1361
- }
1362
- // Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
1363
- const litKey = isLiteralStr(idx) ? idx[1]
1364
- : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
1365
- : null
1366
- // SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
1367
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
1368
- const fo = ctx.func.flatObjects.get(arr)
1369
- const fi = fo.names.indexOf(litKey)
1370
- if (fi >= 0) {
1371
- const t = temp()
1372
- return typed(['block', ['result', 'f64'],
1373
- ['local.set', `$${t}`, valueExpr],
1374
- ['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
1375
- ['local.get', `$${t}`]], 'f64')
1376
- }
1377
- }
1378
- if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
1379
- const slot = ctx.schema.find(arr, litKey)
1380
- if (slot >= 0) {
1381
- const t = temp()
1382
- return typed(['block', ['result', 'f64'],
1383
- ['local.set', `$${t}`, valueExpr],
1384
- ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), slot, ['local.get', `$${t}`]),
1385
- ['local.get', `$${t}`]], 'f64')
1386
- }
1387
- }
1388
- const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
1389
- if (arrIndex != null && typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
1390
- const persist = ptr => {
1391
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1392
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1393
- return ['local.set', `$${arr}`, ptr]
1394
- }
1395
- return storeArrayValue(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), persist)
1396
- }
1397
- if (keyType === VAL.STRING) return setDyn()
1398
- if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
1399
- lookupValType(arr) === 'typed') {
1400
- const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
1401
- if (r) return r
1402
- // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
1403
- // returns the stored value as f64, used directly as the expr result.
1404
- inc('__typed_set_idx')
1405
- return typed(['call', '$__typed_set_idx',
1406
- asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
1407
- }
1408
- if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
1409
- const inner = ctx.schema.emitInner(arr)
1410
- const arrVT = lookupValType(arr) || VAL.OBJECT
1411
- const storeNumeric = keyNode => storeArrayValue(inner, keyNode, ptr =>
1412
- ['f64.store', ptrOffsetIR(asF64(emit(arr)), arrVT), ptr])
1413
- if (useRuntimeKeyDispatch) {
1414
- inc('__dyn_set', '__is_str_key')
1415
- return dispatchKey(storeNumeric)
1416
- }
1417
- return typed(storeNumeric(keyExpr), 'f64')
1418
- }
1419
- const va = emit(arr), vi = asI32(emit(idx)), vv = valueExpr, t = temp()
1420
- if (typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
1421
- const persist = ptr => {
1422
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1423
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1424
- return ['local.set', `$${arr}`, ptr]
1425
- }
1426
- if (useRuntimeKeyDispatch) {
1427
- inc('__dyn_set', '__is_str_key')
1428
- return dispatchKey(keyNode => storeArrayValue(asF64(va), keyNode, persist))
1429
- }
1430
- return storeArrayValue(asF64(va), keyExpr, persist)
1431
- }
1432
- // arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
1433
- const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
1434
- const arrVT = knownArrVT || VAL.OBJECT
1435
- if (useRuntimeKeyDispatch) {
1436
- inc('__dyn_set', '__is_str_key')
1437
- // When arr type is unknown (could be TypedArray) and __typed_set_idx is
1438
- // available, dispatch the numeric branch through __ptr_type so TypedArray
1439
- // writes go by element type. Without this, ternary-typed arrays (e.g.
1440
- // `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
1441
- // f64.store boxed bytes regardless of element width.
1442
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1443
- if (knownArrVT == null && hasTypedSet) {
1444
- const objTmp = temp('asu')
1445
- const idxTmp = tempI32('asi')
1446
- inc('__ptr_type', '__typed_set_idx')
1447
- return dispatchKey(keyNode => {
1448
- const keyI32 = asI32(typed(keyNode, 'f64'))
1449
- return ['block', ['result', 'f64'],
1450
- ['local.set', `$${objTmp}`, asF64(va)],
1451
- ['local.set', `$${idxTmp}`, keyI32],
1452
- ['local.set', `$${t}`, vv],
1453
- ['if', ['result', 'f64'],
1454
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1455
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1456
- ['else', ['block', ['result', 'f64'],
1457
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1458
- ['local.get', `$${t}`]]]]]
1459
- })
1460
- }
1461
- return dispatchKey(keyNode => {
1462
- const keyI32 = asI32(typed(keyNode, 'f64'))
1463
- return ['block', ['result', 'f64'],
1464
- ['local.set', `$${t}`, vv],
1465
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', keyI32, ['i32.const', 3]]], ['local.get', `$${t}`]],
1466
- ['local.get', `$${t}`]]
1467
- })
1468
- }
1469
- if (typeof arr !== 'string') {
1470
- const objTmp = temp('asu')
1471
- const idxTmp = tempI32('asi')
1472
- const ptrTmp = temp('asp')
1473
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1474
- inc('__ptr_type', '__arr_set_idx_ptr')
1475
- if (hasTypedSet) inc('__typed_set_idx')
1476
- return typed(['block', ['result', 'f64'],
1477
- ['local.set', `$${objTmp}`, asF64(va)],
1478
- ['local.set', `$${idxTmp}`, vi],
1479
- ['local.set', `$${t}`, vv],
1480
- ['if', ['result', 'f64'],
1481
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1482
- ['then', ['block', ['result', 'f64'],
1483
- ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1484
- ['local.get', `$${t}`]]],
1485
- ['else', hasTypedSet ? ['if', ['result', 'f64'],
1486
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1487
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1488
- ['else', ['block', ['result', 'f64'],
1489
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1490
- ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1491
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1492
- ['local.get', `$${t}`]]]]], 'f64')
1493
- }
1494
- if (typeof arr === 'string' && knownArrVT == null) {
1495
- const objTmp = temp('asu')
1496
- const idxTmp = tempI32('asi')
1497
- const ptrTmp = temp('asp')
1498
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1499
- inc('__ptr_type', '__arr_set_idx_ptr')
1500
- if (hasTypedSet) inc('__typed_set_idx')
1501
- const persist = ptr => {
1502
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1503
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1504
- return ['local.set', `$${arr}`, ptr]
1505
- }
1506
- return typed(['block', ['result', 'f64'],
1507
- ['local.set', `$${objTmp}`, asF64(va)],
1508
- ['local.set', `$${idxTmp}`, vi],
1509
- ['local.set', `$${t}`, vv],
1510
- ['if', ['result', 'f64'],
1511
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1512
- ['then', ['block', ['result', 'f64'],
1513
- ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1514
- persist(['local.get', `$${ptrTmp}`]),
1515
- ['local.get', `$${t}`]]],
1516
- ['else', hasTypedSet ? ['if', ['result', 'f64'],
1517
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1518
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1519
- ['else', ['block', ['result', 'f64'],
1520
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1521
- ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1522
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1523
- ['local.get', `$${t}`]]]]], 'f64')
1524
- }
1525
- return typed(['block', ['result', 'f64'],
1526
- ['local.set', `$${t}`, vv],
1527
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
1528
- ['local.get', `$${t}`]], 'f64')
1529
- }
1530
- // Object property assignment: obj.prop = x
1531
- if (Array.isArray(name) && name[0] === '.') {
1532
- const [, obj, prop] = name
1533
- // SRoA flat object: `o.prop = x` → `local.set $o#i` (no heap store).
1534
- const flatW = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
1535
- if (flatW) {
1536
- const fi = flatW.names.indexOf(prop)
1537
- if (fi >= 0) {
1538
- const t = temp()
1539
- return typed(['block', ['result', 'f64'],
1540
- ['local.set', `$${t}`, asF64(emit(val))],
1541
- ['local.set', `$${obj}#${fi}`, ['local.get', `$${t}`]],
1542
- ['local.get', `$${t}`]], 'f64')
1543
- }
1544
- }
1545
- // Schema-based object → f64.store at fixed offset.
1546
- if (typeof obj === 'string' && ctx.schema.find) {
1547
- const idx = ctx.schema.find(obj, prop)
1548
- if (idx >= 0) {
1549
- const va = emit(obj), vv = asF64(emit(val)), t = temp()
1550
- const shadow = needsDynShadow(obj)
1551
- if (shadow) inc('__dyn_set')
1552
- const stmts = [
1553
- ['local.set', `$${t}`, vv],
1554
- ctx.abi.object.ops.store(ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), idx, ['local.get', `$${t}`]),
1555
- ]
1556
- if (shadow)
1557
- stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
1558
- stmts.push(['local.get', `$${t}`])
1559
- return typed(['block', ['result', 'f64'], ...stmts], 'f64')
1560
- }
1561
- }
1562
- if (typeof obj === 'string') {
1563
- const objType = keyValType(obj)
1564
- // OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
1565
- // properties go through __dyn_set, which writes to the per-OBJECT
1566
- // propsPtr at off-16 — same path as object-literal dyn shadow writes
1567
- // (module/object.js). __hash_set assumes HASH bucket layout and would
1568
- // corrupt OBJECT memory.
1569
- if (usesDynProps(objType) || objType === VAL.OBJECT) {
1570
- inc('__dyn_set')
1571
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1572
- }
1573
- if (ctx.func.names.has(obj) && !ctx.func.locals?.has(obj) && !ctx.func.current?.params?.some(p => p.name === obj)) {
1574
- inc('__dyn_set')
1575
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1576
- }
1577
- if (objType == null && ctx.transform.host !== 'wasi') {
1578
- ctx.features.external = true
1579
- }
1580
- inc('__hash_set')
1581
- const setCall = typed(['f64.reinterpret_i64', ['call', '$__hash_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1582
- if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
1583
- ['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
1584
- // Closure-captured (boxed) locals store the value at the cell address — local.tee
1585
- // would write to the i32 cell pointer, not the f64 value. Route through writeVar.
1586
- if (ctx.func.boxed?.has(obj)) return writeVar(obj, setCall, false)
1587
- return typed(['local.tee', `$${obj}`, setCall], 'f64')
1588
- }
1589
- if (ctx.transform.host !== 'wasi') ctx.features.external = true
1590
- inc('__dyn_set')
1591
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1592
- }
1593
- if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
1594
- if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
1595
- inc('__to_num')
1596
- return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
1597
- }
1598
- return writeVar(name, emit(val), void_)
1599
- },
1600
-
1601
- // Compound assignments: read-modify-write with type coercion
1602
- '+=': (name, val) => {
1603
- // Complex LHS (obj.prop, arr[i]) → desugar to side-effect-safe `name = name + val`
1604
- if (typeof name !== 'string') return emit(['=', name, ['+', name, val]])
1605
- // String concatenation: desugar to name = name + val (+ handler knows about strings).
1606
- // Also desugar when either side has unknown type — the `+` operator picks runtime
1607
- // string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
1608
- // silently corrupt string concatenations through unknown-typed values.
1609
- const vt = typeof name === 'string' ? keyValType(name) : null
1610
- const vtB = keyValType(val)
1611
- if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
1612
- if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
1613
- return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
1614
- },
1615
- ...Object.fromEntries([
1616
- ['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
1617
- ].map(([op, fn]) => [op, (name, val) => {
1618
- if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1619
- return compoundAssign(name, val,
1620
- (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1621
- fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1622
- )
1623
- }])),
1624
- '%=': (name, val) => {
1625
- if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
1626
- return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
1627
- },
1628
-
1629
- // Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
1630
- ...Object.fromEntries([
1631
- ['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
1632
- ['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
1633
- ].map(([op, fn]) => [op, (name, val) => {
1634
- if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1635
- if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
1636
- const void_ = _expect === 'void'
1637
- const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
1638
- return writeVar(name, result, void_)
1639
- }
1640
- return compoundAssign(name, val,
1641
- (a, b) => asF64(typed([`i32.${fn}`, toI32(a), toI32(b)], 'i32')),
1642
- (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1643
- )
1644
- }])),
1645
-
1646
- // Logical compound assignments: a ||= b → a = a || b, a &&= b → a = a && b
1647
- // Logical/nullish compound assignments: read → check → conditionally write
1648
- // For complex LHS (obj.prop, arr[i]): emit as check(read(lhs)) ? write(lhs, val) : read(lhs)
1649
- ...Object.fromEntries(['||=', '&&=', '??='].map(op => [op, (name, val) => {
1650
- // Complex LHS → desugar (side-effect-safe since obj/arr/idx are locals)
1651
- if (typeof name !== 'string') {
1652
- const baseOp = op.slice(0, -1) // '||', '&&', '??'
1653
- return emit([baseOp, name, ['=', name, val]])
1654
- }
1655
- if (isConst(name)) err(`Assignment to const '${name}'`)
1656
- const void_ = _expect === 'void'
1657
- const t = temp()
1658
- const va = readVar(name)
1659
- // Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
1660
- const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1661
- const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
1662
- // &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
1663
- const [thenExpr, elseExpr] = op === '||='
1664
- ? [['local.get', `$${t}`], asF64(emit(val))]
1665
- : [asF64(emit(val)), ['local.get', `$${t}`]]
1666
- const result = typed(['if', ['result', 'f64'], cond, ['then', thenExpr], ['else', elseExpr]], 'f64')
1667
- // Write back (handles boxed/global/local)
1668
- if (ctx.func.boxed?.has(name)) {
1669
- const bt = temp()
1670
- return typed(['block', ['result', 'f64'],
1671
- ['local.set', `$${bt}`, result],
1672
- ['f64.store', boxedAddr(name), ['local.get', `$${bt}`]],
1673
- ['local.get', `$${bt}`]], 'f64')
1674
- }
1675
- return writeVar(name, result, void_)
1676
- }])),
1677
-
1678
- // === Increment/Decrement ===
1679
- // Postfix resolved in prepare: i++ → (++i) - 1
1680
-
1681
- ...Object.fromEntries([['++', 'add'], ['--', 'sub']].map(([op, fn]) => [op, name => {
1682
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1683
- const void_ = _expect === 'void'
1684
- const v = readVar(name)
1685
- const one = v.type === 'i32' ? ['i32.const', 1] : ['f64.const', 1]
1686
- return writeVar(name, typed([`${v.type}.${fn}`, v, one], v.type), void_)
1687
- }])),
1688
-
1689
- // === Arithmetic (type-preserving) ===
1690
-
1691
- // Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
1692
- '+': (a, b) => {
1693
- if (_expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
1694
- // String concatenation: pure string operands skip generic ToString coercion.
1695
- const vtA = keyValType(a)
1696
- const vtB = keyValType(b)
1697
- if (vtA === VAL.STRING && vtB === VAL.STRING) {
1698
- // Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
1699
- // generic concat dispatch when rhs is a string-index. The byte flows
1700
- // straight from __char_at into memory, and the bump-extend path elides
1701
- // the alloc+copy when lhs is the heap-top STRING.
1702
- if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
1703
- if (keyValType(b[1]) === VAL.STRING) {
1704
- inc('__str_append_byte', '__char_at')
1705
- return typed(['call', '$__str_append_byte',
1706
- asI64(emit(a)),
1707
- ctx.abi.string.ops.charCodeAt(asF64(emit(b[1])), asI32(emit(b[2])), ctx),
1708
- ], 'f64')
1709
- }
1710
- }
1711
- return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
1712
- }
1713
- if (vtA === VAL.STRING || vtB === VAL.STRING) {
1714
- // An OBJECT operand coerces via ToPrimitive(string) at compile time —
1715
- // __str_concat's runtime __to_str cannot invoke a user-defined toString.
1716
- // A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
1717
- const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
1718
- : vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
1719
- const ea = strOperand(vtA, a)
1720
- const eb = strOperand(vtB, b)
1721
- return typed(ctx.abi.string.ops.concat(ea, eb, ctx), 'f64')
1722
- }
1723
- if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
1724
- return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
1725
- // Runtime string dispatch when at least one side could be a string. When one side has
1726
- // a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
1727
- // chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
1728
- // operand needs the runtime check.
1729
- if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
1730
- const tA = temp('add'), tB = temp('add')
1731
- inc('__str_concat', '__is_str_key')
1732
- const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
1733
- const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
1734
- const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
1735
- const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1736
- if (checkA && checkB) {
1737
- return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
1738
- }
1739
- // Exactly one side is checked. Pre-eval the known side first, then the if branches on the unknown.
1740
- const preEval = vtA == null ? ['local.set', `$${tB}`, asF64(emit(b))] : ['local.set', `$${tA}`, asF64(emit(a))]
1741
- return typed(['block', ['result', 'f64'],
1742
- preEval,
1743
- ['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
1744
- ], 'f64')
1745
- }
1746
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
1747
- if (_f) return _f
1748
- // Neither side is a string here (string paths handled above), but either may
1749
- // still be null/undefined/pointer — numeric `+` performs ToNumber like `-`/`*`.
1750
- if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1751
- if (isLit(va) && litVal(va) === 0) return toNumF64(b, vb)
1752
- // An `.unsigned` operand is a uint32 (range [0, 2^32)); JS `+` is a float
1753
- // op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
1754
- // Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
1755
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
1756
- return typed(['f64.add', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1757
- },
1758
- '-': (a, b) => {
1759
- if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
1760
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1761
- return b === undefined
1762
- ? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1763
- : fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
1764
- if (b === undefined) return emitNeg(a)
1765
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
1766
- if (_f) return _f
1767
- if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1768
- // Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
1769
- // so avoid the wrapping i32.sub fast-path. See `+` above.
1770
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
1771
- return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1772
- },
1773
- 'u+': a => {
1774
- if (valTypeOf(a) === VAL.BIGINT)
1775
- return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
1776
- const v = emit(a)
1777
- if (v.type === 'i32') return asF64(v)
1778
- if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
1779
- inc('__to_num')
1780
- return typed(['call', '$__to_num', asI64(v)], 'f64')
1781
- },
1782
- 'u-': a => emitNeg(a),
1783
- '*': (a, b) => {
1784
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1785
- return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
1786
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
1787
- if (_f) return _f
1788
- if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1789
- if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
1790
- if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
1791
- if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
1792
- // `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
1793
- // `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
1794
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
1795
- return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1796
- },
1797
- '/': (a, b) => {
1798
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1799
- return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
1800
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
1801
- if (_f) return _f
1802
- if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1803
- return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1804
- },
1805
- '%': (a, b) => {
1806
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1807
- return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
1808
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
1809
- if (_f) return _f
1810
- // ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
1811
- // The i32.rem_s fast path traps on a zero divisor, so divert a literal-zero divisor.
1812
- if (isLit(vb) && litVal(vb) === 0) return emitNum(NaN)
1813
- // `.unsigned` operand: `i32.rem_s` reads the uint32 as a negative signed value
1814
- // ((2^32-1)%7 → rem_s(-1,7) = -1, not 3). Widen to f64 — see `+` above.
1815
- if (isI32Num(va) && isI32Num(vb) && !va.unsigned && !vb.unsigned) return typed(['i32.rem_s', va, vb], 'i32')
1816
- return f64rem(toNumF64(a, va), toNumF64(b, vb))
1817
- },
1818
- // === Comparisons (always i32 result) ===
1819
-
1820
- '==': (a, b) => {
1821
- const charCmp = emitSingleCharIndexCmp(a, b)
1822
- if (charCmp) return charCmp
1823
- const subCmp = emitSubstringEqCmp(a, b)
1824
- if (subCmp) return subCmp
1825
- // JS loose nullish equality: x == null / x == undefined.
1826
- // If the non-literal side has a known non-null VAL type, fold to 0.
1827
- if (isNullishLit(a)) {
1828
- if (valTypeOf(b)) return emitNum(0)
1829
- return isNullish(asF64(emit(b)))
1830
- }
1831
- if (isNullishLit(b)) {
1832
- if (valTypeOf(a)) return emitNum(0)
1833
- return isNullish(asF64(emit(a)))
1834
- }
1835
- // typeof x == 'string' → compile-time type check (prepare rewrites string to type code)
1836
- const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
1837
- const va = emit(a), vb = emit(b)
1838
- if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
1839
- // Either side known-pure NUMBER (literal or typed) → f64.eq is correct regardless
1840
- // of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
1841
- // reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
1842
- // Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
1843
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1844
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1845
- if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
1846
- if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
1847
- if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1848
- // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1849
- // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
1850
- // STRING needs __eq (heap strings can be equal by content but different pointers).
1851
- // BIGINT needs __eq (heap-allocated, content compare).
1852
- if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1853
- return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1854
- }
1855
- inc('__eq')
1856
- return typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
1857
- },
1858
- '!=': (a, b) => {
1859
- const charCmp = emitSingleCharIndexCmp(a, b, true)
1860
- if (charCmp) return charCmp
1861
- const subCmp = emitSubstringEqCmp(a, b, true)
1862
- if (subCmp) return subCmp
1863
- if (isNullishLit(a)) {
1864
- if (valTypeOf(b)) return emitNum(1)
1865
- return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
1866
- }
1867
- if (isNullishLit(b)) {
1868
- if (valTypeOf(a)) return emitNum(1)
1869
- return typed(['i32.eqz', isNullish(asF64(emit(a)))], 'i32')
1870
- }
1871
- const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
1872
- const va = emit(a), vb = emit(b)
1873
- if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1874
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1875
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1876
- if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
1877
- if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
1878
- if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1879
- if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1880
- return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1881
- }
1882
- inc('__eq')
1883
- return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
1884
- },
1885
- '===': (a, b) => emitStrictEq(a, b, false),
1886
- '!==': (a, b) => emitStrictEq(a, b, true),
1887
- '<': cmpOp('lt_s', 'lt', (a, b) => a < b),
1888
- '>': cmpOp('gt_s', 'gt', (a, b) => a > b),
1889
- '<=': cmpOp('le_s', 'le', (a, b) => a <= b),
1890
- '>=': cmpOp('ge_s', 'ge', (a, b) => a >= b),
1891
-
1892
- // === Logical ===
1893
-
1894
- '!': a => {
1895
- const v = emit(a)
1896
- if (v.type === 'i32') return typed(['i32.eqz', v], 'i32')
1897
- // Unboxed pointer offsets: falsy iff zero offset.
1898
- if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
1899
- // Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
1900
- // Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
1901
- // VAL.BOOL rides the 0/1 numeric carrier (not a pointer), so normalize it to
1902
- // NUMBER and let it fall to the truthy path — `!false` must be `true`.
1903
- const vt = numericVal(resolveValType(a, valTypeOf, lookupValType))
1904
- if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
1905
- return isNullish(asF64(v))
1906
- }
1907
- inc('__is_truthy')
1908
- return typed(['i32.eqz', ['call', '$__is_truthy', asI64(v)]], 'i32')
1909
- },
1910
-
1911
- '?:': (a, b, c) => {
1912
- // Constant condition → emit only the live branch
1913
- const ca = emit(a)
1914
- if (isLit(ca)) { const v = litVal(ca); return (v !== 0 && v === v) ? emit(b) : emit(c) }
1915
- const cond = toBoolFromEmitted(ca)
1916
- // Flow-sensitive refinement: each arm sees narrowing consistent with `a` being truthy / falsy.
1917
- const thenRefs = extractRefinements(a, new Map(), true)
1918
- const elseRefs = extractRefinements(a, new Map(), false)
1919
- const vb = withRefinements(thenRefs, b, () => emit(b))
1920
- const vc = withRefinements(elseRefs, c, () => emit(c))
1921
- // L: Use WASM select for pure ternaries — branchless, smaller bytecode
1922
- if (vb.type === 'i32' && vc.type === 'i32') {
1923
- // Propagate matching ptrKind/ptrAux so a downstream asF64 takes the NaN-rebox
1924
- // path instead of `f64.convert_i32_s`. Mismatched kinds drop both — caller's
1925
- // asF64 will treat the i32 as numeric, which is correct for non-pointer i32s.
1926
- // ptrKind matches but ptrAux differs (e.g. polymorphic OBJECT with two
1927
- // distinct schemaIds, or TYPED with two element types) — fall through to
1928
- // the f64 path. There each arm reboxes independently, preserving its own
1929
- // aux in the NaN-box. The single-i32 path can only carry one aux on the
1930
- // result, so `boxPtrIR` would default to 0 and lose the runtime schema /
1931
- // elemType bits needed by downstream lookups (e.g. __dyn_get's OBJECT-
1932
- // schema fallback uses receiver aux to resolve `.prop`).
1933
- const auxMismatch = vb.ptrKind != null && vb.ptrKind === vc.ptrKind
1934
- && (vb.ptrAux ?? null) !== (vc.ptrAux ?? null)
1935
- if (!auxMismatch) {
1936
- const tagPtr = (n) => {
1937
- if (vb.ptrKind != null && vb.ptrKind === vc.ptrKind) {
1938
- n.ptrKind = vb.ptrKind
1939
- if (vb.ptrAux != null && vb.ptrAux === vc.ptrAux) n.ptrAux = vb.ptrAux
1940
- }
1941
- return n
1942
- }
1943
- if (isPureIR(vb) && isPureIR(vc))
1944
- return tagPtr(typed(['select', vb, vc, cond], 'i32'))
1945
- return tagPtr(typed(['if', ['result', 'i32'], cond, ['then', vb], ['else', vc]], 'i32'))
1946
- }
1947
- }
1948
- const fb = asF64(vb), fc = asF64(vc)
1949
- const vtb = resolveValType(b, valTypeOf, lookupValType)
1950
- const vtc = resolveValType(c, valTypeOf, lookupValType)
1951
- const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
1952
- const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
1953
- || vb.closureFuncIdx != null || vc.closureFuncIdx != null
1954
- || isNaNBoxLit(fb) || isNaNBoxLit(fc)
1955
- if (refPayload) {
1956
- const ib = ['i64.reinterpret_f64', fb]
1957
- const ic = ['i64.reinterpret_f64', fc]
1958
- const bits = isPureIR(fb) && isPureIR(fc)
1959
- ? ['select', ib, ic, cond]
1960
- : ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
1961
- return typed(['f64.reinterpret_i64', bits], 'f64')
1962
- }
1963
- if (!refPayload && isPureIR(fb) && isPureIR(fc))
1964
- return typed(['select', fb, fc, cond], 'f64')
1965
- return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
1966
- },
1967
-
1968
- '&&': (a, b) => {
1969
- const va = emit(a)
1970
- // Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
1971
- if (isLit(va)) {
1972
- const v = litVal(va)
1973
- if (v !== 0 && v === v) {
1974
- const refs = extractRefinements(a, new Map(), true)
1975
- return withRefinements(refs, b, () => emit(b))
1976
- }
1977
- return va
1978
- }
1979
- // a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
1980
- // (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
1981
- const rightRefs = extractRefinements(a, new Map(), true)
1982
- const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
1983
- // i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
1984
- // skip f64 round-trip and __is_truthy call entirely.
1985
- if (va.type === 'i32') {
1986
- const vb = emitRight()
1987
- const t = tempI32()
1988
- if (vb.type === 'i32') {
1989
- return typed(['if', ['result', 'i32'],
1990
- ['local.tee', `$${t}`, va],
1991
- ['then', vb],
1992
- ['else', ['local.get', `$${t}`]]], 'i32')
1993
- }
1994
- return typed(['if', ['result', 'f64'],
1995
- ['local.tee', `$${t}`, va],
1996
- ['then', asF64(vb)],
1997
- ['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
1998
- }
1999
- const t = temp()
2000
- const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
2001
- return typed(['if', ['result', 'f64'],
2002
- toBoolFromEmitted(teed),
2003
- ['then', asF64(emitRight())],
2004
- ['else', ['local.get', `$${t}`]]], 'f64')
2005
- },
2006
-
2007
- '||': (a, b) => {
2008
- const va = emit(a)
2009
- // Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
2010
- if (isLit(va)) {
2011
- const v = litVal(va)
2012
- if (v !== 0 && v === v) return va
2013
- const refs = extractRefinements(a, new Map(), false)
2014
- return withRefinements(refs, b, () => emit(b))
2015
- }
2016
- // a is falsy in the right-arm — `x == null || ...` proves x is null/undefined in b;
2017
- // De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
2018
- const rightRefs = extractRefinements(a, new Map(), false)
2019
- const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
2020
- if (va.type === 'i32') {
2021
- const vb = emitRight()
2022
- const t = tempI32()
2023
- if (vb.type === 'i32') {
2024
- return typed(['if', ['result', 'i32'],
2025
- ['local.tee', `$${t}`, va],
2026
- ['then', ['local.get', `$${t}`]],
2027
- ['else', vb]], 'i32')
2028
- }
2029
- return typed(['if', ['result', 'f64'],
2030
- ['local.tee', `$${t}`, va],
2031
- ['then', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')],
2032
- ['else', asF64(vb)]], 'f64')
2033
- }
2034
- const t = temp()
2035
- const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
2036
- return typed(['if', ['result', 'f64'],
2037
- toBoolFromEmitted(teed),
2038
- ['then', ['local.get', `$${t}`]],
2039
- ['else', asF64(emitRight())]], 'f64')
2040
- },
2041
-
2042
- // a ?? b: returns b only if a is nullish
2043
- '??': (a, b) => {
2044
- const va = emit(a)
2045
- const t = temp()
2046
- return typed(['if', ['result', 'f64'],
2047
- // Check: is a NOT nullish?
2048
- ['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
2049
- ['then', ['local.get', `$${t}`]],
2050
- ['else', asF64(emit(b))]], 'f64')
2051
- },
2052
-
2053
- 'void': a => {
2054
- const v = emit(a)
2055
- const dropAndUndef = (instr) => typed(['block', ['result', 'f64'], instr, 'drop', undefExpr()], 'f64')
2056
- if (v == null) return undefExpr()
2057
- const op = Array.isArray(v) ? v[0] : null
2058
- const wasmVoid = op === 'local.set' || (typeof op === 'string' && op.endsWith('.store'))
2059
- || op === 'memory.copy' || op === 'global.set'
2060
- if (wasmVoid)
2061
- return typed(['block', ['result', 'f64'], v, undefExpr()], 'f64')
2062
- if (v.type && v.type !== 'void')
2063
- return dropAndUndef(v)
2064
- return typed(['block', ['result', 'f64'], ...flat(v), undefExpr()], 'f64')
2065
- },
2066
-
2067
- '(': a => emit(a),
2068
-
2069
- // === Bitwise (i32 for numbers, i64 for BigInt) ===
2070
-
2071
- // Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
2072
- // i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
2073
- // the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
2074
- // unknown type) routes through __to_num so "2026" | 0 === 2026.
2075
- // `~~x` is the idiomatic int32 truncation: the two xor-with-(-1) cancel, leaving
2076
- // a single toI32 (whose NaN/Infinity guard runs once, unchanged). Fold it here so
2077
- // DSP/bytebeat `~~` doesn't emit a dead double-xor watr won't remove.
2078
- '~': a => {
2079
- if (Array.isArray(a) && a[0] === '~') {
2080
- const inner = a[1], iv = emit(inner)
2081
- return isLit(iv) ? emitNum(~~litVal(iv)) : typed(toI32(isI32Num(iv) ? iv : toNumF64(inner, iv)), 'i32')
2082
- }
2083
- const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(isI32Num(v) ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32')
2084
- },
2085
- ...Object.fromEntries([
2086
- ['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
2087
- ].map(([op, fn]) => [op, (a, b) => {
2088
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
2089
- return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
2090
- const va = emit(a), vb = emit(b)
2091
- if (isLit(va) && isLit(vb)) {
2092
- const la = litVal(va), lb = litVal(vb)
2093
- if (op === '&') return emitNum(la & lb); if (op === '|') return emitNum(la | lb)
2094
- if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
2095
- if (op === '>>') return emitNum(la >> lb)
2096
- }
2097
- const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
2098
- const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
2099
- return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
2100
- }])),
2101
- '>>>': (a, b) => {
2102
- const va = emit(a), vb = emit(b)
2103
- if (isLit(va) && isLit(vb)) {
2104
- const r = litVal(va) >>> litVal(vb) // JS uint32 result ∈ [0, 2^32)
2105
- // ≥ 2^31 doesn't fit signed i32: materialize the wrapped bits as an i32 const
2106
- // tagged `.unsigned` so `asF64` lifts via `convert_i32_u`. Emitting `f64.const r`
2107
- // here (the old foldConst path) would `trunc_sat_f64_s`-saturate to INT32_MAX
2108
- // when the enclosing function narrows to an i32 result. Values < 2^31 fold to a
2109
- // plain i32 const (signed == unsigned, stays foldable downstream).
2110
- if (r >= 0x80000000) { const node = typed(['i32.const', r | 0], 'i32'); node.unsigned = true; return node }
2111
- return emitNum(r)
2112
- }
2113
- // F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
2114
- // [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
2115
- // signed for negative-high-bit s values, flipping sign and breaking the
2116
- // canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
2117
- const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
2118
- const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
2119
- const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
2120
- node.unsigned = true
2121
- return node
2122
- },
2123
-
2124
- // === Control flow ===
2125
-
2126
- 'if': (cond, then, els) => {
2127
- // Dead branch elimination: constant condition → emit only the live branch
2128
- const ce = emit(cond)
2129
- if (isLit(ce)) {
2130
- const v = litVal(ce), truthy = v !== 0 && v === v
2131
- if (truthy) return emitFlat(then)
2132
- if (els != null) return emitFlat(els)
2133
- return null
2134
- }
2135
- const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
2136
- // Flow-sensitive type refinement: narrow types within each branch based on the guard.
2137
- const thenRefs = extractRefinements(cond, new Map(), true)
2138
- const elseRefs = extractRefinements(cond, new Map(), false)
2139
- const thenBody = withRefinements(thenRefs, then, () => emitFlat(then))
2140
- if (els != null) {
2141
- const elseBody = withRefinements(elseRefs, els, () => emitFlat(els))
2142
- return ['if', c, ['then', ...thenBody], ['else', ...elseBody]]
2143
- }
2144
- return ['if', c, ['then', ...thenBody]]
2145
- },
2146
-
2147
- 'for': (init, cond, step, body) => {
2148
- if (body === undefined) return err('for-in/for-of not supported')
2149
- if (!ctx.transform.optimize || ctx.transform.optimize.smallConstForUnroll !== false) {
2150
- const unrolled = unrollSmallConstFor(init, cond, step, body)
2151
- if (unrolled) return unrolled
2152
- }
2153
- const id = ctx.func.uniq++
2154
- const brk = `$brk${id}`, loop = `$loop${id}`
2155
- // The cont wrapper is only needed if the body has a `continue` AND there is a step
2156
- // expression — `continue` must jump to before the step. Without a step, `continue`
2157
- // can target the loop label directly, saving a redundant `block`.
2158
- const needsCont = step && hasOwnContinue(body)
2159
- const cont = needsCont ? `$cont${id}` : loop
2160
- ctx.func.stack.push({ brk, loop: cont })
2161
- const result = []
2162
- if (init != null) result.push(...emitFlat(init))
2163
- const loopBody = []
2164
- if (cond) loopBody.push(['br_if', brk, ['i32.eqz', toBool(cond)]])
2165
- if (needsCont) loopBody.push(['block', cont, ...emitFlat(body)])
2166
- else loopBody.push(...emitFlat(body))
2167
- if (step) loopBody.push(...emitFlat(step))
2168
- loopBody.push(['br', loop])
2169
- result.push(['block', brk, ['loop', loop, ...loopBody]])
2170
- ctx.func.stack.pop()
2171
- return result.length === 1 ? result[0] : result
2172
- },
2173
-
2174
- 'switch': (discriminant, ...cases) => {
2175
- const disc = `${T}disc${ctx.func.uniq++}`
2176
- ctx.func.locals.set(disc, 'f64')
2177
-
2178
- const result = [['local.set', `$${disc}`, asF64(emit(discriminant))]]
2179
-
2180
- for (const c of cases) {
2181
- if (c[0] === 'case') {
2182
- const [, test, body] = c
2183
- const skip = `$skip${ctx.func.uniq++}`
2184
- // Block: skip if discriminant != test, otherwise execute body
2185
- result.push(['block', skip,
2186
- ['br_if', skip, typed(['f64.ne', typed(['local.get', `$${disc}`], 'f64'), asF64(emit(test))], 'i32')],
2187
- ...emitFlat(body)])
2188
- } else if (c[0] === 'default') {
2189
- result.push(...emitFlat(c[1]))
2190
- }
2191
- }
2192
-
2193
- return result
2194
- },
2195
-
2196
- 'while': (cond, body) => emitter['for'](null, cond, null, body),
2197
- 'label': (name, body) => {
2198
- const brk = `$label${ctx.func.uniq++}`
2199
- ctx.func.stack.push({ label: name, brk })
2200
- const result = ['block', brk, ...emitFlat(body)]
2201
- ctx.func.stack.pop()
2202
- return result
2203
- },
2204
- 'break': (label) => {
2205
- const target = label == null
2206
- ? loopTop().brk
2207
- : ctx.func.stack.findLast(frame => frame.label === label)?.brk
2208
- if (!target) err(`break label '${label}' is not in scope`)
2209
- return [...emitFinalizers(), ['br', target]]
2210
- },
2211
- 'continue': (label) => {
2212
- if (label != null) err(`continue label '${label}' is not supported`)
2213
- return [...emitFinalizers(), ['br', loopTop().loop]]
2214
- },
2215
-
2216
- // === Call ===
2217
-
2218
- // Arrow as value → closure
2219
- '=>': (rawParams, body) => {
2220
- if (!ctx.closure.make) err('Closures require fn module (auto-included)')
2221
-
2222
- const raw = extractParams(rawParams)
2223
- const params = [], defaults = {}
2224
- let restParam = null, bodyPrefix = []
2225
- for (const r of raw) {
2226
- const c = classifyParam(r)
2227
- if (c.kind === 'rest') { restParam = c.name; params.push(c.name) }
2228
- else if (c.kind === 'plain') params.push(c.name)
2229
- else if (c.kind === 'default') { params.push(c.name); defaults[c.name] = c.defValue }
2230
- else {
2231
- const tmp = `${T}p${ctx.func.uniq++}`
2232
- params.push(tmp)
2233
- if (c.kind === 'destruct-default') defaults[tmp] = c.defValue
2234
- bodyPrefix.push(['let', ['=', c.pattern, tmp]])
2235
- }
2236
- }
2237
-
2238
- // Prepend destructuring to body (if any destructured params)
2239
- if (bodyPrefix.length) {
2240
- if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
2241
- body = ['{}', [';', ...bodyPrefix, ...body[1].slice(1)]]
2242
- else if (Array.isArray(body) && body[0] === '{}')
2243
- body = ['{}', [';', ...bodyPrefix, body[1]]]
2244
- else body = ['{}', [';', ...bodyPrefix, ['return', body]]]
2245
- }
2246
-
2247
- // Find free variables in body that aren't params → captures
2248
- const paramSet = new Set(params)
2249
- const captures = []
2250
- findFreeVars(body, paramSet, captures)
2251
- for (const def of Object.values(defaults)) findFreeVars(def, paramSet, captures)
2252
-
2253
- // Pass closure info including rest param and defaults
2254
- const closureInfo = { params, body, captures, restParam }
2255
- if (Object.keys(defaults).length) closureInfo.defaults = defaults
2256
- return ctx.closure.make(closureInfo)
2257
- },
2258
-
2259
- '()': (callee, callArgs) => {
2260
- let argList = Array.isArray(callArgs)
2261
- ? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
2262
- : callArgs ? [callArgs] : []
2263
-
2264
- // Helper: expand spread arguments into flat list of normal arguments + spread markers
2265
- // Returns { normal: [...], spreads: [(pos, expr), ...] }
2266
- const parseArgs = (args) => {
2267
- const normal = []
2268
- const spreads = []
2269
- for (let i = 0; i < args.length; i++) {
2270
- const arg = args[i]
2271
- if (Array.isArray(arg) && arg[0] === '...') {
2272
- spreads.push({ pos: normal.length, expr: arg[1] })
2273
- } else {
2274
- normal.push(arg)
2275
- }
2276
- }
2277
- return { normal, spreads, hasSpread: spreads.length > 0 }
2278
- }
2279
-
2280
- const parsed = parseArgs(argList)
2281
-
2282
- // Closure devirtualization: a callee that is a module global proven (by
2283
- // plan.js) to hold one statically-known function for the whole post-init
2284
- // program rewrites to that function — the known-top-level-function branch
2285
- // below then emits a direct `call`, dropping the indirect/trampoline path.
2286
- if (typeof callee === 'string' && ctx.func.globalDevirt?.has(callee))
2287
- callee = ctx.func.globalDevirt.get(callee)
2288
-
2289
- // Optional method call: obj?.method(args) — null if obj is nullish, else
2290
- // obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
2291
- // distinct from the regular method call's ['.', obj, method] callee. Receiver
2292
- // hoists into a temp so the nullish check and the method dispatch below see
2293
- // the same evaluation; recursion with a synthetic '.'-callee reuses the type-
2294
- // aware method-dispatch in this same handler rather than duplicating it.
2295
- if (Array.isArray(callee) && callee[0] === '?.') {
2296
- const [, obj, method] = callee
2297
- const t = `${T}om${ctx.func.uniq++}`
2298
- ctx.func.locals.set(t, 'f64')
2299
- const va = asF64(emit(obj))
2300
- const methodCall = emitter['()'](['.', t, method], callArgs)
2301
- return typed(['block', ['result', 'f64'],
2302
- ['local.set', `$${t}`, va],
2303
- ['if', ['result', 'f64'],
2304
- ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
2305
- ['then', asF64(methodCall)],
2306
- ['else', nullExpr()]]], 'f64')
2307
- }
2308
-
2309
- // Method call: obj.method(args) → type-aware dispatch
2310
- if (Array.isArray(callee) && callee[0] === '.') {
2311
- const [, obj, method] = callee
2312
-
2313
- // charCodeAt with a statically in-bounds index — emit the i32
2314
- // (OOB-impossible) contract directly; the generic path keeps the
2315
- // f64/NaN JS-spec result. See analyze.js inBoundsCharCodeAt.
2316
- if (method === 'charCodeAt' && !parsed.hasSpread && parsed.normal.length === 1
2317
- && ctx.abi.string?.ops?.charCodeAt && inBoundsCharCodeAt(ctx).has(callee)) {
2318
- const recv = emit(obj)
2319
- // jsstring carrier: receiver is an externref boundary param. Route to
2320
- // `wasm:js-string.charCodeAt` directly — the in-bounds proof rules out
2321
- // the OOB trap the builtin would otherwise raise.
2322
- if (recv?.type === 'externref') {
2323
- ctx.core.jsstring.add('charCodeAt')
2324
- return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
2325
- }
2326
- return typed(ctx.abi.string.ops.charCodeAt(
2327
- asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
2328
- }
2329
-
2330
- // Function property call: fn.prop(args) → direct call to fn$prop.
2331
- // Skipped when the property was reassigned (wrapper composition) — then
2332
- // it is a mutable slot and must be read dynamically before the call.
2333
- if (typeof obj === 'string' && ctx.func.names.has(obj) && !ctx.func.multiProp.has(`${obj}.${method}`)) {
2334
- const fname = `${obj}$${method}`
2335
- if (ctx.func.names.has(fname)) {
2336
- const func = ctx.func.map.get(fname)
2337
- const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
2338
- while (emittedArgs.length < func.sig.params.length)
2339
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2340
- const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
2341
- if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2342
- if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2343
- if (func.sig.unsignedResult) callIR.unsigned = true
2344
- return callIR
2345
- }
2346
- }
2347
-
2348
- let vt = keyValType(obj)
2349
- // A reassigned slice/concat receiver may carry a stale `vt` — a reassignment
2350
- // inside a nested closure escapes analyzeValTypes' poisoning (its walk stops
2351
- // at `=>`). Drop to runtime dispatch, but only for guessy types: STRING/ARRAY
2352
- // dispatch correctly either way, and BUFFER/TYPED are construction proofs
2353
- // (`new ArrayBuffer`/`new XxxArray`) — the runtime String/Array fallback has
2354
- // no branch for them, so nulling `vt` would miscompile `ab.slice()` into an
2355
- // f64-array copy. jzify also splits every `var x = init` into `let x; x = init`,
2356
- // marking single-assignment vars "reassigned"; keeping definite BUFFER/TYPED
2357
- // is what keeps `var`-declared buffers correct.
2358
- if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
2359
- && (method === 'slice' || method === 'concat')
2360
- && vt !== VAL.STRING && vt !== VAL.ARRAY
2361
- && vt !== VAL.BUFFER && vt !== VAL.TYPED) vt = null
2362
-
2363
- // Helper to call method with arguments (handles spread expansion)
2364
- const callMethod = (objArg, methodEmitter) => {
2365
- if (!parsed.hasSpread) {
2366
- return methodEmitter(objArg, ...parsed.normal)
2367
- }
2368
-
2369
- // Bulk push fast path: `obj.push(...src)` — single spread, no normal args, named obj.
2370
- // The generic single-spread loop below calls methodEmitter per iteration, which expands
2371
- // to a full .push (grow check + ptr_offset + store + set_len) every step. Amortising the
2372
- // grow + set_len across the whole spread eliminates ~3 stdlib calls per byte in watr's
2373
- // hot `out.push(...HANDLER[op](...))` path (~24M bytes/iter on raycast).
2374
- if (method === 'push' && parsed.normal.length === 0 &&
2375
- parsed.spreads.length === 1 && typeof objArg === 'string') {
2376
- const spreadExpr = parsed.spreads[0].expr
2377
- inc('__len'); inc('__arr_grow'); inc('__set_len'); inc('__ptr_offset')
2378
- const o = `${T}po${ctx.func.uniq++}`,
2379
- sa = `${T}psa${ctx.func.uniq++}`,
2380
- sl = `${T}psl${ctx.func.uniq++}`,
2381
- ol = `${T}pol${ctx.func.uniq++}`,
2382
- si = `${T}psi${ctx.func.uniq++}`,
2383
- base = `${T}pb${ctx.func.uniq++}`
2384
- ctx.func.locals.set(o, 'f64'); ctx.func.locals.set(sa, 'f64')
2385
- ctx.func.locals.set(sl, 'i32'); ctx.func.locals.set(ol, 'i32')
2386
- ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
2387
-
2388
- const objIsArr = lookupValType(objArg) === VAL.ARRAY
2389
- // A materialized multi-value is not a statically-typed pointer — let
2390
- // emitSpreadCopy resolve its kind once at runtime.
2391
- const srcVT = multiCount(spreadExpr) ? undefined : valTypeOf(spreadExpr)
2392
- const n = multiCount(spreadExpr)
2393
- const ir = []
2394
- ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
2395
- ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
2396
- ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
2397
- // Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
2398
- if (objIsArr) {
2399
- ir.push(['local.set', `$${ol}`,
2400
- ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
2401
- } else {
2402
- ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
2403
- }
2404
- // Single grow for the full spread (vs per-element grow check in the generic loop).
2405
- ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
2406
- ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
2407
- // base captured AFTER grow (grow may relocate the array).
2408
- ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
2409
- // Bulk-copy the spread: an ARRAY source is a contiguous f64 block → memory.copy.
2410
- ir.push(['local.set', `$${si}`, ['local.get', `$${ol}`]])
2411
- ir.push(...emitSpreadCopy(base, si, sa, sl, srcVT))
2412
- // Single set_len for the full spread.
2413
- ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
2414
- ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
2415
- // Update source variable: grow may have moved the pointer.
2416
- if (ctx.func.boxed?.has(objArg)) {
2417
- ir.push(['f64.store', ['local.get', `$${ctx.func.boxed.get(objArg)}`], ['local.get', `$${o}`]])
2418
- } else if (ctx.scope.globals.has(objArg) && !ctx.func.locals?.has(objArg)) {
2419
- ir.push(['global.set', `$${objArg}`, ['local.get', `$${o}`]])
2420
- } else {
2421
- ir.push(['local.set', `$${objArg}`, ['local.get', `$${o}`]])
2422
- }
2423
- ir.push(['f64.convert_i32_s', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
2424
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
2425
- }
2426
-
2427
- // Single spread at end: call method with normal args, then loop spread elements
2428
- if (parsed.spreads.length === 1 && parsed.spreads[0].pos === parsed.normal.length) {
2429
- const spreadExpr = parsed.spreads[0].expr
2430
- 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++}`
2431
- ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
2432
- ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
2433
- // Emit-time rep seeding for a fresh spread-staging local (no prior reader).
2434
- // Without this, the loop body's `[]` read on `arr` falls back to polymorphic
2435
- // dispatch — VAL.* on the rep elides STRING gate for ARRAY/TYPED spreads.
2436
- const spreadVT = valTypeOf(spreadExpr)
2437
- if (spreadVT) updateRep(arr, { val: spreadVT })
2438
-
2439
- // In-place spread methods modify target; accumulating methods (concat) return new values
2440
- const inPlace = SPREAD_MUTATORS.has(method)
2441
- // unshift prepends each arg to the front — iterating forward reverses the
2442
- // intended order, so walk the spread from end to start.
2443
- const reverseIter = method === 'unshift'
2444
- const ir = []
2445
- ir.push(['local.set', `$${acc}`, asF64(emit(objArg))])
2446
- if (parsed.normal.length > 0) {
2447
- const r = asF64(methodEmitter(objArg, ...parsed.normal))
2448
- ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
2449
- }
2450
-
2451
- inc('__len')
2452
- const n = multiCount(spreadExpr)
2453
- ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
2454
- ir.push(['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]])
2455
- ir.push(['local.set', `$${idx}`,
2456
- reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
2457
- const loopId = ctx.func.uniq++
2458
- const loopBody = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
2459
- ir.push(['block', `$break${loopId}`,
2460
- ['loop', `$continue${loopId}`,
2461
- ['br_if', `$break${loopId}`,
2462
- reverseIter
2463
- ? ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]
2464
- : ['i32.ge_u', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
2465
- inPlace ? ['drop', loopBody] : ['local.set', `$${acc}`, loopBody],
2466
- ['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['i32.const', reverseIter ? -1 : 1]]],
2467
- ['br', `$continue${loopId}`]]])
2468
-
2469
- ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
2470
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
2471
- }
2472
-
2473
- // General spread case: iterate args in original order, batch contiguous normal
2474
- // args into a single call, emit a per-element loop for each spread.
2475
- //
2476
- // inPlace methods (push/unshift/add/set): call methodEmitter(objArg, ...) each
2477
- // time so the source variable's local gets updated (else heap grow/realloc
2478
- // wouldn't be visible to subsequent uses of the variable). Final value is objArg.
2479
- //
2480
- // non-inPlace (concat, etc.): chain via temp acc since return value is the new
2481
- // collection.
2482
- const inPlaceG = SPREAD_MUTATORS.has(method)
2483
- const combinedG = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2484
- inc('__len')
2485
-
2486
- if (inPlaceG) {
2487
- const irG = []
2488
- let batch = []
2489
- const flushBatch = () => {
2490
- if (!batch.length) return
2491
- irG.push(['drop', asF64(methodEmitter(objArg, ...batch))])
2492
- batch = []
2493
- }
2494
- for (const item of combinedG) {
2495
- if (Array.isArray(item) && item[0] === '__spread') {
2496
- flushBatch()
2497
- const spreadExpr = item[1]
2498
- const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
2499
- ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
2500
- // Emit-time rep seeding for fresh spread-staging local (see arr-spread comment above).
2501
- const spreadVT = valTypeOf(spreadExpr)
2502
- if (spreadVT) updateRep(arrL, { val: spreadVT })
2503
- const n = multiCount(spreadExpr)
2504
- irG.push(
2505
- ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
2506
- ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
2507
- ['local.set', `$${idxL}`, ['i32.const', 0]])
2508
- const loopId = ctx.func.uniq++
2509
- const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
2510
- irG.push(['block', `$break${loopId}`,
2511
- ['loop', `$continue${loopId}`,
2512
- ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
2513
- ['drop', loopBody],
2514
- ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
2515
- ['br', `$continue${loopId}`]]])
2516
- } else {
2517
- batch.push(item)
2518
- }
2519
- }
2520
- flushBatch()
2521
- irG.push(asF64(emit(objArg)))
2522
- return typed(['block', ['result', 'f64'], ...irG], 'f64')
2523
- }
2524
-
2525
- const accG = `${T}acc${ctx.func.uniq++}`
2526
- ctx.func.locals.set(accG, 'f64')
2527
- const irG = [['local.set', `$${accG}`, asF64(emit(objArg))]]
2528
- let batch = []
2529
- const flushBatch = () => {
2530
- if (!batch.length) return
2531
- irG.push(['local.set', `$${accG}`, asF64(methodEmitter(accG, ...batch))])
2532
- batch = []
2533
- }
2534
- for (const item of combinedG) {
2535
- if (Array.isArray(item) && item[0] === '__spread') {
2536
- flushBatch()
2537
- const spreadExpr = item[1]
2538
- const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
2539
- ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
2540
- const spreadVT = valTypeOf(spreadExpr)
2541
- if (spreadVT) updateRep(arrL, { val: spreadVT })
2542
- const n = multiCount(spreadExpr)
2543
- irG.push(
2544
- ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
2545
- ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
2546
- ['local.set', `$${idxL}`, ['i32.const', 0]])
2547
- const loopId = ctx.func.uniq++
2548
- const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
2549
- irG.push(['block', `$break${loopId}`,
2550
- ['loop', `$continue${loopId}`,
2551
- ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
2552
- ['local.set', `$${accG}`, loopBody],
2553
- ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
2554
- ['br', `$continue${loopId}`]]])
2555
- } else {
2556
- batch.push(item)
2557
- }
2558
- }
2559
- flushBatch()
2560
- irG.push(['local.get', `$${accG}`])
2561
- return typed(['block', ['result', 'f64'], ...irG], 'f64')
2562
- }
2563
-
2564
- // Boxed object: delegate method to inner value (slot 0)
2565
- if (typeof obj === 'string' && ctx.schema.isBoxed?.(obj)) {
2566
- const innerVt = repOf(obj)?.val
2567
- const emitter = ctx.core.emit[`.${innerVt}:${method}`] || ctx.core.emit[`.${method}`]
2568
- if (emitter) {
2569
- const innerName = `${obj}${T}inner`
2570
- if (!ctx.func.locals.has(innerName)) ctx.func.locals.set(innerName, 'f64')
2571
- const boxBase = tempI32('bb')
2572
- // Load current inner value from boxed object's slot 0 (may have been updated by prior mutations)
2573
- // Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
2574
- const loadInner = [
2575
- ['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
2576
- ['local.set', `$${innerName}`, ctx.abi.object.ops.load(['local.get', `$${boxBase}`], 0)]]
2577
- const result = callMethod(innerName, emitter)
2578
- // Mutating methods may reallocate; writeback inner value to boxed slot
2579
- if (BOXED_MUTATORS.has(method)) {
2580
- const wb = ctx.abi.object.ops.store(['local.get', `$${boxBase}`], 0, ['local.get', `$${innerName}`])
2581
- return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
2582
- }
2583
- // Non-mutating: just load inner and call
2584
- return typed(['block', ['result', 'f64'], ...loadInner, asF64(result)], 'f64')
2585
- }
2586
- }
2587
-
2588
- // valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
2589
- // property shadows. An assigned `obj.valueOf`/`obj.toString` must win over
2590
- // the builtin emitter for any receiver that can carry a dynamic-prop
2591
- // sidecar — a sidecar-bearing static type (array/typed/object) OR a
2592
- // statically-unknown receiver (e.g. an array-element read `arr[0]`, whose
2593
- // type is only known at runtime). Probe the sidecar and call it when it
2594
- // holds a closure, else fall back to the builtin (generic when untyped:
2595
- // `.valueOf` returns the receiver, `.toString` runs type-aware __to_str).
2596
- // Parallels the member-READ check in module/core.js emitPropAccess (which
2597
- // stays scoped to known sidecar types). (watr's `str()` attaches
2598
- // `bytes.valueOf = () => s`, recovered via `.valueOf()`.)
2599
- if ((method === 'valueOf' || method === 'toString') && ctx.closure.call
2600
- && !parsed.hasSpread && parsed.normal.length === 0
2601
- && (vt === VAL.ARRAY || vt === VAL.TYPED || vt === VAL.OBJECT || !vt)) {
2602
- const builtin = (vt && ctx.core.emit[`.${vt}:${method}`]) || ctx.core.emit[`.${method}`]
2603
- if (builtin) {
2604
- const objTmp = temp('vobj'), propTmp = temp('vprop')
2605
- inc('__dyn_get_expr', '__ptr_type')
2606
- return typed(['block', ['result', 'f64'],
2607
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2608
- ['local.set', `$${propTmp}`, ['f64.reinterpret_i64',
2609
- ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]]],
2610
- ['if', ['result', 'f64'],
2611
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2612
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [])],
2613
- ['else', asF64(callMethod(objTmp, builtin))]]], 'f64')
2614
- }
2615
- }
2616
-
2617
- // Known type → static dispatch
2618
- if (vt && ctx.core.emit[`.${vt}:${method}`]) {
2619
- return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
2620
- }
2621
-
2622
- // Unknown / guessed-array type, both string + generic exist → runtime dispatch by ptr type.
2623
- // analyze.js defaults untyped `.slice()` results to VAL.ARRAY, which is a guess, not a proof;
2624
- // runtime dispatch resolves whether the operand is actually a string or an array.
2625
- // Concretely-typed non-string values (BUFFER, TYPED, MAP, …) fall through to the generic
2626
- // emitter which already knows how to handle them.
2627
- const strKey = `.string:${method}`, genKey = `.${method}`
2628
- if ((!vt || vt === VAL.ARRAY) && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
2629
- const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
2630
- ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
2631
- const strEmitter = ctx.core.emit[strKey]
2632
- const genEmitter = ctx.core.emit[genKey]
2633
- return typed(['block', ['result', 'f64'],
2634
- ['local.set', `$${t}`, asF64(emit(obj))],
2635
- ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
2636
- ['if', ['result', 'f64'],
2637
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
2638
- ['then', callMethod(t, strEmitter)],
2639
- ['else', callMethod(t, genEmitter)]]], 'f64')
2640
- }
2641
-
2642
- // Schema property function call: x.prop(args) where prop is a closure in boxed schema
2643
- if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
2644
- const idx = ctx.schema.find(obj, method)
2645
- if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
2646
- const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
2647
- if (parsed.hasSpread) {
2648
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2649
- return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
2650
- }
2651
- return ctx.closure.call(propRead, parsed.normal)
2652
- }
2653
- }
2654
-
2655
- // Schema property function call: x.prop(args) where prop is a closure in boxed schema
2656
- if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
2657
- const idx = ctx.schema.find(obj, method)
2658
- if (idx >= 0) {
2659
- const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
2660
- return ctx.closure.call(propRead, parsed.normal)
2661
- }
2662
- }
2663
-
2664
- // Generic only — but a collection emitter (`.get`/`.set`/`.has`/`.add`/
2665
- // `.delete`) assumes a Map/Set receiver: a proven collection already
2666
- // dispatched via `.${vt}:${method}` above, so reaching here means the
2667
- // receiver is not a proven collection. A zero-arg call then cannot be the
2668
- // collection op (each needs ≥1 key/value arg) — it is a user/closure
2669
- // method (e.g. `new C().get()`). Skip the collection emitter so it falls
2670
- // through to closure/dynamic dispatch instead of crashing on `emit(key)`.
2671
- const collectionMisfit = COLLECTION_METHODS.has(method) &&
2672
- !parsed.hasSpread && parsed.normal.length === 0
2673
- if (ctx.core.emit[genKey] && !collectionMisfit) {
2674
- return callMethod(obj, ctx.core.emit[genKey])
2675
- }
2676
-
2677
- // Dynamic property function call on non-external values.
2678
- if (ctx.closure.call) {
2679
- if (ctx.transform.strict)
2680
- 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 }.`)
2681
- const objTmp = `${T}mobj${ctx.func.uniq++}`
2682
- ctx.func.locals.set(objTmp, 'f64')
2683
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2684
- const arrayIR = buildArrayWithSpreads(combined)
2685
- const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
2686
- const propTmp = `${T}mprop${ctx.func.uniq++}`
2687
- ctx.func.locals.set(propTmp, 'f64')
2688
- if (usesDynProps(vt)) {
2689
- inc('__dyn_get_expr', '__ptr_type')
2690
- return typed(['block', ['result', 'f64'],
2691
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2692
- ['local.set', `$${propTmp}`, propRead],
2693
- ['if', ['result', 'f64'],
2694
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2695
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2696
- ['else', undefExpr()]]], 'f64')
2697
- }
2698
- // WASI: no PTR.EXTERNAL values; closure-only dispatch is correct.
2699
- if (ctx.transform.host === 'wasi') {
2700
- inc('__dyn_get_expr', '__ptr_type')
2701
- return typed(['block', ['result', 'f64'],
2702
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2703
- ['local.set', `$${propTmp}`, propRead],
2704
- ['if', ['result', 'f64'],
2705
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2706
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2707
- ['else', undefExpr()]]], 'f64')
2708
- }
2709
- inc('__dyn_get_expr', '__ext_call', '__ptr_type')
2710
- ctx.features.external = true
2711
- return typed(['block', ['result', 'f64'],
2712
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2713
- ['local.set', `$${propTmp}`, propRead],
2714
- ['if', ['result', 'f64'],
2715
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2716
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2717
- ['else', ['if', ['result', 'f64'],
2718
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.EXTERNAL]],
2719
- ['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
2720
- ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
2721
- ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2722
- ['i64.reinterpret_f64', arrayIR]]]],
2723
- ['else', undefExpr()]]]]], 'f64')
2724
- }
2725
-
2726
- // Unknown callee - assume external method
2727
- if (ctx.transform.strict)
2728
- 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 }.`)
2729
- // Under wasi there is no host `__ext_call` — the call lowers to a
2730
- // no-op returning `undefined`. This is by-design so polymorphic code
2731
- // can target js and wasi from one source; users who want fail-fast
2732
- // pass `strict: true` (handled above).
2733
- if (ctx.transform.host === 'wasi') return undefExpr()
2734
- inc('__ext_call')
2735
- ctx.features.external = true
2736
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2737
- const arrayIR = buildArrayWithSpreads(combined)
2738
- return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
2739
- ['i64.reinterpret_f64', asF64(emit(obj))],
2740
- ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2741
- ['i64.reinterpret_f64', arrayIR]]], 'f64');
2742
- }
2743
-
2744
- if (typeof callee === 'string' && ctx.core.emit[callee] && !isBoundName(callee) && !isUserFunc(callee)) {
2745
- // Pass spread args through to emitter (e.g. Math.max(...arr))
2746
- if (parsed.hasSpread) {
2747
- const allArgs = []
2748
- let ni = 0
2749
- for (const s of parsed.spreads) {
2750
- while (ni < s.pos) allArgs.push(parsed.normal[ni++])
2751
- allArgs.push(['...', s.expr])
2752
- }
2753
- while (ni < parsed.normal.length) allArgs.push(parsed.normal[ni++])
2754
- return ctx.core.emit[callee](...allArgs)
2755
- }
2756
- return ctx.core.emit[callee](...parsed.normal)
2757
- }
2758
-
2759
- // Direct call if callee is a known top-level function
2760
- if (typeof callee === 'string' && ctx.func.names.has(callee) && !isBoundName(callee)) {
2761
- const func = ctx.func.map.get(callee)
2762
-
2763
- // Rest param case: collect all args (including expanded spreads) into array
2764
- if (func?.rest) {
2765
- const fixedParamCount = func.sig.params.length - 1
2766
- const fixedArgs = parsed.normal.slice(0, fixedParamCount)
2767
- // Pad missing fixed args with `undefined` so default-param init triggers per spec.
2768
- const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
2769
- while (emittedFixed.length < fixedParamCount)
2770
- emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2771
-
2772
- // Reconstruct with spreads, then take rest args
2773
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2774
- const restArgsFinal = combined.slice(fixedParamCount)
2775
-
2776
- // Build array: emit code for normal args + code to expand spreads
2777
- const arrayIR = buildArrayWithSpreads(restArgsFinal)
2778
- const callIR = typed(['call', `$${callee}`,
2779
- ...emittedFixed,
2780
- arrayIR], func.sig.results[0])
2781
- if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2782
- if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2783
- if (func.sig.unsignedResult) callIR.unsigned = true
2784
- return callIR
2785
- }
2786
-
2787
- // Regular function call without rest params
2788
- if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
2789
- // Pad missing args with `undefined` so default-param init triggers per spec
2790
- // (only undefined, not null, should trigger defaults). Drop extras to match
2791
- // JS calling convention — emitting them anyway produces an invalid call
2792
- // when the callee is a fixed-arity import (e.g. `_interp`-registered host
2793
- // stubs) since wasm validates arg count. Use ?? rather than || so a
2794
- // legitimate 0-arity callee isn't bypassed.
2795
- const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
2796
- const expected = func?.sig.params.length ?? args.length
2797
- while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2798
- if (args.length > expected) args.length = expected
2799
- // Multi-value return: materialize as heap array (caller expects single pointer)
2800
- if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
2801
- const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
2802
- if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2803
- if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2804
- // Unsigned-uint32 result (every tail was `>>>`): consumer's asF64 must use
2805
- // `f64.convert_i32_u` instead of `_s`, preserving the [0, 2^32) range.
2806
- if (func?.sig.unsignedResult) callIR.unsigned = true
2807
- return callIR
2808
- }
2809
-
2810
- // A3: const-bound, non-escaping closure → direct call to body (skip call_indirect).
2811
- // emitDecl registered name → bodyName when it saw the closure.make IR. Body signature
2812
- // is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64. We pass the closure
2813
- // NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
2814
- if (typeof callee === 'string' && !parsed.hasSpread
2815
- && ctx.func.directClosures?.has(callee)) {
2816
- const bodyName = ctx.func.directClosures.get(callee)
2817
- const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
2818
- const n = parsed.normal.length
2819
- if (n <= W) {
2820
- const slots = parsed.normal.map(a => asF64(emit(a)))
2821
- while (slots.length < W) slots.push(undefExpr())
2822
- return typed(['call', `$${bodyName}`,
2823
- asF64(emit(callee)),
2824
- typed(['i32.const', n], 'i32'),
2825
- ...slots], 'f64')
2826
- }
2827
- }
2828
-
2829
- // Closure call: callee is a variable holding a NaN-boxed closure pointer
2830
- // Uniform convention: fn.call packs all args into an array
2831
- if (ctx.closure.call) {
2832
- if (parsed.hasSpread) {
2833
- // Spread: build the args array directly (handles __spread markers)
2834
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2835
- const arrayIR = buildArrayWithSpreads(combined)
2836
- // Pass pre-built array as single already-emitted arg
2837
- return ctx.closure.call(emit(callee), [arrayIR], true)
2838
- }
2839
- return ctx.closure.call(emit(callee), parsed.normal)
2840
- }
2841
-
2842
- // Unknown callee — assume direct call. Match arg count to the declared
2843
- // signature when the callee is a registered env import (e.g. `_interp`-
2844
- // wired host stubs). JS calling convention drops extras and pads missing;
2845
- // wasm requires exact arity, so emitting raw argList against a fixed-arity
2846
- // import would invalidate the module.
2847
- let calleeArity = null
2848
- if (typeof callee === 'string') {
2849
- const imp = ctx.module.imports?.find(i =>
2850
- Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
2851
- if (imp) {
2852
- let n = 0
2853
- for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
2854
- calleeArity = n
2855
- }
2856
- }
2857
- const emittedArgs = argList.map(a => asF64(emit(a)))
2858
- if (calleeArity != null) {
2859
- while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
2860
- if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
2861
- }
2862
- return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
2863
- },
2864
- }
2865
-
2866
- // === Emit dispatch ===
2867
-
2868
- // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
2869
- // Per ECMAScript, an optional access short-circuits the entire continuation, not just
2870
- // its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
2871
- // nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
2872
- //
2873
- // At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
2874
- // the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
2875
- // with that optional replaced by a regular access. The single guard short-circuits the
2876
- // whole continuation. Nested optionals further inside the chain are left intact and
2877
- // handle their own short-circuiting on recursion.
2878
- function liftOptionalChain(node) {
2879
- const path = []
2880
- let cur = node
2881
- while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
2882
- cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
2883
- path.push(cur)
2884
- cur = cur[1]
2885
- }
2886
- // Find the deepest optional with continuation outside it. optIdx === 0 means the
2887
- // chain root itself is optional with no continuation — handled by the regular
2888
- // `?.` / `?.[]` / `?.()` emitters.
2889
- let optIdx = -1
2890
- for (let i = path.length - 1; i >= 1; i--) {
2891
- if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
2892
- optIdx = i
2893
- break
2894
- }
2895
- }
2896
- if (optIdx <= 0) return null
2897
- const opt = path[optIdx]
2898
- const headExpr = opt[1]
2899
- const t = temp('oc')
2900
- let rebuilt
2901
- if (opt[0] === '?.') rebuilt = ['.', t, opt[2]]
2902
- else if (opt[0] === '?.[]') rebuilt = ['[]', t, opt[2]]
2903
- else rebuilt = ['()', t, ...opt.slice(2)]
2904
- for (let i = optIdx - 1; i >= 0; i--) {
2905
- const outer = path[i]
2906
- rebuilt = [outer[0], rebuilt, ...outer.slice(2)]
2907
- }
2908
- return typed(['block', ['result', 'f64'],
2909
- ['local.set', `$${t}`, asF64(emit(headExpr))],
2910
- ['if', ['result', 'f64'],
2911
- ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
2912
- ['then', asF64(emit(rebuilt))],
2913
- ['else', undefExpr()]]], 'f64')
2914
- }
2915
-
2916
- /**
2917
- * Emit single AST node to typed WASM IR.
2918
- * Every returned node has .type = 'i32' | 'f64'.
2919
- * @param {import('./prepare.js').ASTNode} node
2920
- * @returns {Array} typed WASM S-expression
2921
- */
2922
- export function emit(node, expect) {
2923
- _expect = expect || null
2924
- if (Array.isArray(node)) {
2925
- ctx.error.node = node
2926
- if (node.loc != null) ctx.error.loc = node.loc
2927
- }
2928
- if (node == null) return null
2929
- // Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
2930
- // AST), but their working representation is the plain number 0/1 — identical
2931
- // codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
2932
- if (node === true) return emitNum(1)
2933
- if (node === false) return emitNum(0)
2934
- if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
2935
- return nullExpr()
2936
- if (typeof node === 'bigint') {
2937
- // Truncate to 64 bits — `BigInt.asUintN(64, …)` semantics, same as the
2938
- // explicit mask `node & 0xFFFFFFFFFFFFFFFFn`. Decimal form (vs. the prior
2939
- // unsigned-hex dance) is enough now that watr's optimize.js getConst
2940
- // handles signed strings correctly (4.6.8 W5 fix).
2941
- return typed(['f64.reinterpret_i64', ['i64.const', BigInt.asUintN(64, node).toString()]], 'f64')
2942
- }
2943
- if (typeof node === 'number') return emitNum(node)
2944
- if (typeof node === 'string') {
2945
- // Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
2946
- if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node) || repOf(node)?.intConst != null)
2947
- return readVar(node)
2948
- // Top-level function used as value → wrap as closure pointer for call_indirect
2949
- if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
2950
- // Trampoline signature: uniform closure ABI (env f64, argc i32, a0..a{MAX-1} f64) → f64.
2951
- // Forwards the first N inline slots to $func where N = func's fixed param count.
2952
- const func = ctx.func.map.get(node)
2953
- const sigParams = func?.sig.params || []
2954
- if (sigParams.length > MAX_CLOSURE_ARITY) err(`Function ${node} used as closure value has ${sigParams.length} params, exceeds MAX_CLOSURE_ARITY=${MAX_CLOSURE_ARITY}`)
2955
- const trampolineName = `${T}tramp_${node}`
2956
- if (!ctx.core.stdlib[trampolineName]) {
2957
- const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
2958
- const paramDecls = ['(param $__env f64)', '(param $__argc i32)']
2959
- for (let i = 0; i < W; i++) paramDecls.push(`(param $__a${i} f64)`)
2960
- // Forward fixed slots; if func expects i32, convert via trunc_sat
2961
- const fwd = sigParams.map((p, i) =>
2962
- p.type === 'i32'
2963
- ? `(i32.trunc_sat_f64_s (local.get $__a${i}))`
2964
- : `(local.get $__a${i})`).join(' ')
2965
- if ((func?.sig.results.length || 1) > 1) {
2966
- const n = func.sig.results.length
2967
- const arr = `${T}retarr`
2968
- const temps = Array.from({ length: n }, (_, i) => `${T}ret${i}`)
2969
- const tempLocals = temps.map(name => `(local $${name} f64)`).join(' ')
2970
- const stores = temps.map((name, i) =>
2971
- `(f64.store (i32.add (local.get $${arr}) (i32.const ${i * 8})) (local.get $${name}))`
2972
- ).join(' ')
2973
- const capture = temps.slice().reverse().map(name => `(local.set $${name})`).join(' ')
2974
- 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})))`
2975
- inc(trampolineName, '__alloc', '__mkptr')
2976
- } else {
2977
- // Convert i32/i64 results back to f64 — uniform closure ABI returns f64.
2978
- const resType = func?.sig.results[0]
2979
- const callExpr = `(call $${node} ${fwd})`
2980
- const wrapped = resType === 'i32'
2981
- ? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
2982
- : resType === 'i64'
2983
- ? `(f64.reinterpret_i64 ${callExpr})`
2984
- : callExpr
2985
- ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${wrapped})`
2986
- inc(trampolineName)
2987
- }
2988
- }
2989
- let idx = ctx.closure.table.indexOf(trampolineName)
2990
- if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
2991
- const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
2992
- ir.closureFuncIdx = idx
2993
- return ir
2994
- }
2995
- // Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
2996
- if (node.includes('.') && ctx.core.emit[node]) {
2997
- const handler = ctx.core.emit[node]
2998
- return handler.length > 0 ? builtinFunctionValue(node) : handler()
2999
- }
3000
- // Auto-import known host globals (WebAssembly, globalThis, etc.)
3001
- const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
3002
- if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
3003
- if (ctx.transform.host === 'wasi') err(`host:'wasi': reference to host global \`${node}\` requires an env import. Remove the reference or use host:'js'.`)
3004
- ctx.features.external = true
3005
- ctx.scope.globals.set(node, null)
3006
- // Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed
3007
- // external-ref payload across the wasm↔JS global boundary (same hazard
3008
- // as env.print/setTimeout — see module/console.js header). asF64()
3009
- // reinterprets to f64 at every read site.
3010
- ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, 'i64']])
3011
- ctx.scope.globalTypes.set(node, 'i64')
3012
- return typed(['global.get', `$${node}`], 'i64')
3013
- }
3014
- const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
3015
- return typed(['local.get', `$${node}`], t)
3016
- }
3017
- if (!Array.isArray(node)) return typed(['f64.const', 0], 'f64')
3018
-
3019
- const [op, ...args] = node
3020
- // WASM IR passthrough: internally-generated IR nodes (from statement flattening) pass through
3021
- if (typeof op === 'string' && !ctx.core.emit[op] && (op.includes('.') || WASM_OPS.has(op))) return node
3022
-
3023
- // Literal node [, value] — handle null/undefined values
3024
- if (op == null && args.length === 1) {
3025
- const v = args[0]
3026
- return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
3027
- }
3028
-
3029
- // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
3030
- // Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
3031
- // with the optional already replaced by a non-optional access on a guarded temp.
3032
- if (op === '.' || op === '[]' || op === '()') {
3033
- const lifted = liftOptionalChain(node)
3034
- if (lifted) return lifted
3035
- }
3036
-
3037
- const handler = ctx.core.emit[op]
3038
- if (!handler) err(`Unknown op: ${op}`)
3039
- return handler(...args)
3040
- }