jz 0.5.0 → 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 +281 -142
  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 +461 -185
  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 +591 -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} +600 -205
  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 -2997
  77. package/src/jzify.js +0 -1553
  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,2997 +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
- return ['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
793
- ['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
794
- ['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
795
- ['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
796
- ['br', `$loop${loopId}`]]]
797
- }
798
- const advance = ['local.set', `$${posLocal}`,
799
- ['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
800
- if (staticVT === VAL.ARRAY) return [arrCopy(), advance]
801
- if (staticVT === VAL.STRING || staticVT === VAL.TYPED) return [scalarLoop(), advance]
802
- inc('__ptr_type')
803
- return [['if',
804
- ['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.ARRAY]],
805
- ['then', arrCopy()],
806
- ['else', scalarLoop()]], advance]
807
- }
808
-
809
- /**
810
- * Build an array from items, handling ['__spread', expr] markers.
811
- * Split into sections (normal arrays and spreads), then copy all into result.
812
- */
813
- export function buildArrayWithSpreads(items) {
814
- const spreads = []
815
- for (let i = 0; i < items.length; i++) {
816
- if (Array.isArray(items[i]) && items[i][0] === '__spread') {
817
- spreads.push({ pos: i, expr: items[i][1] })
818
- }
819
- }
820
-
821
- if (spreads.length === 0) {
822
- return emit(['[', ...items])
823
- }
824
-
825
- const sections = []
826
- let currentArray = []
827
-
828
- for (let i = 0; i < items.length; i++) {
829
- if (Array.isArray(items[i]) && items[i][0] === '__spread') {
830
- if (currentArray.length > 0) {
831
- sections.push({ type: 'array', items: currentArray })
832
- currentArray = []
833
- }
834
- sections.push({ type: 'spread', expr: items[i][1] })
835
- } else {
836
- currentArray.push(items[i])
837
- }
838
- }
839
- if (currentArray.length > 0) {
840
- sections.push({ type: 'array', items: currentArray })
841
- }
842
-
843
- // A single all-normal section is a plain literal — defer to the `[` emitter.
844
- // A single *spread* section is NOT shortcut to `emit(sec.expr)`: that would
845
- // alias the source, but `[...x]` must yield a fresh array. It falls through
846
- // to the alloc + emitSpreadCopy path below, which copies.
847
- if (sections.length === 1 && sections[0].type === 'array') {
848
- return emit(['[', ...sections[0].items])
849
- }
850
-
851
- const len = tempI32('len')
852
- const pos = tempI32('pos')
853
- const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
854
- const result = out.local
855
-
856
- const ir = []
857
- inc('__len')
858
-
859
- // Pass 1 — evaluate every section IN SOURCE ORDER into temps. JS spread keeps
860
- // strict left-to-right order: a later spread whose source mutates an earlier
861
- // element's input must still observe the pre-mutation value. Array items
862
- // become per-item f64 temps; spreads become a ptr temp + a cached __len.
863
- for (const sec of sections) {
864
- if (sec.type === 'array') {
865
- sec.itemLocals = []
866
- for (let i = 0; i < sec.items.length; i++) {
867
- const it = `${T}ai${ctx.func.uniq++}`
868
- ctx.func.locals.set(it, 'f64')
869
- sec.itemLocals.push(it)
870
- ir.push(['local.set', `$${it}`, asF64(emit(sec.items[i]))])
871
- }
872
- } else {
873
- sec.local = `${T}sp${ctx.func.uniq++}`
874
- ctx.func.locals.set(sec.local, 'f64')
875
- sec.lenLocal = `${T}spl${ctx.func.uniq++}`
876
- ctx.func.locals.set(sec.lenLocal, 'i32')
877
- // A materialized multi-value is not a statically-typed pointer — let
878
- // emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
879
- sec.val = multiCount(sec.expr) ? undefined : valTypeOf(sec.expr)
880
- const n = multiCount(sec.expr)
881
- ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
882
- // Cache __len once per spread; reused below for total-len sum and the copy.
883
- ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
884
- }
885
- }
886
-
887
- // Pass 2 — total length (array sections statically sized, spreads cached above).
888
- ir.push(['local.set', `$${len}`, ['i32.const', 0]])
889
- for (const sec of sections) {
890
- if (sec.type === 'array') {
891
- ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
892
- } else {
893
- ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['local.get', `$${sec.lenLocal}`]]])
894
- }
895
- }
896
-
897
- // Pass 3 — allocate exact, then store the pre-evaluated temps.
898
- ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
899
- for (const sec of sections) {
900
- if (sec.type === 'array') {
901
- for (const it of sec.itemLocals) {
902
- ir.push(
903
- ['f64.store',
904
- ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
905
- ['local.get', `$${it}`]],
906
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
907
- )
908
- }
909
- } else {
910
- ir.push(...emitSpreadCopy(result, pos, sec.local, sec.lenLocal, sec.val))
911
- }
912
- }
913
-
914
- ir.push(out.ptr)
915
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
916
- }
917
-
918
- /** Check if node is a block body (statement list, not object literal/expression) */
919
- const isBlockBody = n => Array.isArray(n) && n[0] === '{}' && n.length === 2 && Array.isArray(n[1]) && STMT_OPS.has(n[1]?.[0])
920
-
921
- /** Emit node in void context: emit + drop any value. Block bodies route through emitBody. */
922
- export function emitFlat(node) {
923
- if (isBlockBody(node)) return emitBody(node)
924
- const ir = emit(node, 'void')
925
- const items = flat(ir)
926
- if (ir?.type && ir.type !== 'void') items.push('drop')
927
- return items
928
- }
929
-
930
- /** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitFlat per statement.
931
- * Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
932
- * rest of the enclosing block. Refinements added here are rolled back on block exit. */
933
- export function emitBody(node) {
934
- const inner = node[1]
935
- const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
936
- const out = []
937
- const accumulated = []
938
- for (let i = 0; i < stmts.length; i++) {
939
- const s = stmts[i]
940
- if (s == null || typeof s === 'number') continue
941
- out.push(...emitFlat(s))
942
- // After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
943
- // Skip names that are reassigned later — refinement would be unsound past the assignment.
944
- if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
945
- const refs = extractRefinements(s[1], new Map(), false)
946
- for (const [name, fact] of refs) {
947
- let reassigned = false
948
- for (let j = i + 1; j < stmts.length; j++)
949
- if (isReassigned(stmts[j], name)) { reassigned = true; break }
950
- if (reassigned) continue
951
- const cur = ctx.func.refinements.get(name)
952
- accumulated.push([name, cur])
953
- // Merge so sibling early-returns layering on the same name compose
954
- // (e.g. `if (typeof x === 'string') return; if (Array.isArray(x)) return;`
955
- // leaves both `notString: true` and would-be array exclusion stacked).
956
- ctx.func.refinements.set(name, cur ? { ...cur, ...fact } : fact)
957
- }
958
- }
959
- }
960
- // Restore prior refinements on block exit.
961
- for (let i = accumulated.length - 1; i >= 0; i--) {
962
- const [name, prev] = accumulated[i]
963
- if (prev === undefined) ctx.func.refinements.delete(name); else ctx.func.refinements.set(name, prev)
964
- }
965
- return out
966
- }
967
-
968
- // A VAL.BOOL value rides the cheap 0/1 numeric carrier, and `ToNumber(bool)` is
969
- // exactly that carrier — so for relational / loose-equality coercion a boolean
970
- // behaves identically to a number. Normalize it before the type-directed compare
971
- // dispatch (the BOOL fact still drives typeof / String / boundary boxing; only
972
- // these arithmetic-shaped operators read it as numeric).
973
- const numericVal = vt => vt === VAL.BOOL ? VAL.NUMBER : vt
974
-
975
- // Primitive value-type classes for strict-equality type-mismatch folding. Two
976
- // operands of different known classes — when at least one is a primitive — can
977
- // never be `===` (number/boolean/string/bigint don't cross-coerce under `===`).
978
- // Two *reference* kinds (array vs object, …) fall through to the shared ref-eq
979
- // path instead, which already resolves distinct pointers to `false`.
980
- const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
981
-
982
- /**
983
- * Strict `===`/`!==`. Unlike loose `==`, no coercion: a statically-known type
984
- * mismatch folds to a constant (`true === 1` → false, `"1" === 1` → false). When
985
- * the types match — or one side is statically unknown — the result is bit-for-bit
986
- * identical to loose `==` on same-type operands, so we delegate to it.
987
- *
988
- * Two carrier-level limitations remain (documented gaps, not regressions):
989
- * • booleans and numbers share the 0/1 carrier, so `1 === trueDynamic` can only
990
- * be told apart when the boolean's type is statically known;
991
- * • jz unifies `null` and `undefined` into one NaN-boxed sentinel, so
992
- * `null === undefined` is `true` (same as `==`).
993
- */
994
- function emitStrictEq(a, b, negate) {
995
- // `typeof x === 'type'` (prepare rewrote the literal to a numeric code) — typeof
996
- // always yields a string, so strict and loose agree; reuse the loose lowering.
997
- const tc = emitTypeofCmp(a, b, negate ? 'ne' : 'eq'); if (tc) return tc
998
- // Known, differing primitive classes can never be strictly equal.
999
- const rawA = resolveValType(a, valTypeOf, lookupValType)
1000
- const rawB = resolveValType(b, valTypeOf, lookupValType)
1001
- if (rawA && rawB && rawA !== rawB && (STRICT_PRIM.has(rawA) || STRICT_PRIM.has(rawB)))
1002
- return emitNum(negate ? 1 : 0)
1003
- // Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
1004
- return emitter[negate ? '!=' : '=='](a, b)
1005
- }
1006
-
1007
- /** Comparison op factory with constant folding. */
1008
- const cmpOp = (i32op, f64op, fn) => (a, b) => {
1009
- const va = emit(a), vb = emit(b)
1010
- // Skip the const-fold for `.unsigned` operands: `litVal` is the signed bit pattern
1011
- // (-1, not 4294967295), so folding the order would be wrong. Fall through to the
1012
- // f64 widen path below, which converts each operand by its own signedness.
1013
- if (isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
1014
- // String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
1015
- // (always false), so without this the spec-correct `"a" < "b"` returns 0.
1016
- // Route both-STRING operands through __str_cmp's three-way result, then apply
1017
- // the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
1018
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1019
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1020
- if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
1021
- const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
1022
- return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
1023
- }
1024
- if (vta === VAL.STRING && vtb === VAL.STRING) {
1025
- return typed([`i32.${i32op}`, ctx.abi.string.ops.cmp(asF64(va), asF64(vb), ctx), ['i32.const', 0]], 'i32')
1026
- }
1027
- // Exactly one operand is a known string; the other has no static type, so it
1028
- // may hold a string pointer at runtime (e.g. `c >= '0'` where `c` came from
1029
- // `s[i]` on an untyped receiver). JS relational compare is lexicographic only
1030
- // when *both* sides are strings, else it ToNumbers both. The f64 path below
1031
- // would compare the unknown side's NaN-boxed string bits as a float (NaN ⇒
1032
- // always false), so dispatch at runtime on the unknown side: string → __str_cmp
1033
- // three-way; else ToNumber both. Mirrors `+`'s __is_str_key string dispatch.
1034
- // Gated on a *known-string* counterpart, so numeric loops (`i < n`) never pay
1035
- // the check — comparing against a string literal signals string intent.
1036
- if (((vta === VAL.STRING && vtb == null) || (vtb === VAL.STRING && vta == null)) && ctx.abi.string?.ops?.cmp) {
1037
- const unkIsA = vta == null
1038
- const ta = temp('cmp'), tb = temp('cmp')
1039
- inc('__is_str_key')
1040
- const getA = typed(['local.get', `$${ta}`], 'f64'), getB = typed(['local.get', `$${tb}`], 'f64')
1041
- const check = ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${unkIsA ? ta : tb}`]]]
1042
- const strCmp = [`i32.${i32op}`, ctx.abi.string.ops.cmp(getA, getB, ctx), ['i32.const', 0]]
1043
- const numCmp = [`f64.${f64op}`, toNumF64(a, getA), toNumF64(b, getB)]
1044
- return typed(['block', ['result', 'i32'],
1045
- ['local.set', `$${ta}`, asF64(va)],
1046
- ['local.set', `$${tb}`, asF64(vb)],
1047
- ['if', ['result', 'i32'], check, ['then', strCmp], ['else', numCmp]]], 'i32')
1048
- }
1049
- if (vta === VAL.DATE || vtb === VAL.DATE) {
1050
- const dateNum = (node, v, vt) => {
1051
- if (vt !== VAL.DATE) return toNumF64(node, v)
1052
- const ptr = v.ptrKind === VAL.DATE
1053
- ? v
1054
- : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
1055
- return typed(['f64.load', ptr], 'f64')
1056
- }
1057
- return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
1058
- }
1059
- if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
1060
- return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
1061
- if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
1062
- return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
1063
- // An `.unsigned` i32 operand ([0, 2^32)) can't share a signed i32 compare with a
1064
- // possibly-signed one: mixed sign inverts the order (3 < 0xFFFFFFFF unsigned, but
1065
- // 3 > -1 signed). Widen to f64, where asF64 converts each operand by its own
1066
- // signedness (convert_i32_u for unsigned, _s otherwise) to its true numeric value.
1067
- if (!va.unsigned && !vb.unsigned) {
1068
- const ai = intConstValue(a), bi = intConstValue(b)
1069
- if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
1070
- if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
1071
- if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${i32op}`, va, vb], 'i32')
1072
- }
1073
- return typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
1074
- }
1075
-
1076
- function needsRelationalToNumber(expr, vt) {
1077
- if (vt === VAL.STRING) return true
1078
- if (vt != null) return false
1079
- return mayReadBoxedValue(expr)
1080
- }
1081
-
1082
- function needsLooseEqualityToNumber(expr, vt) {
1083
- if (vt === VAL.STRING) return true
1084
- if (vt != null) return false
1085
- return mayReadBoxedValue(expr)
1086
- }
1087
-
1088
- function looseNumberEq(numIR, otherNode, otherIR, negate = false) {
1089
- const t = temp('eq')
1090
- const other = typed(['local.get', `$${t}`], 'f64')
1091
- const cmp = ['f64.eq', asF64(numIR), toNumF64(otherNode, other)]
1092
- return typed(['block', ['result', 'i32'],
1093
- ['local.set', `$${t}`, asF64(otherIR)],
1094
- ['if', ['result', 'i32'], isNullish(other),
1095
- ['then', ['i32.const', negate ? 1 : 0]],
1096
- ['else', negate ? ['i32.eqz', cmp] : cmp]]], 'i32')
1097
- }
1098
-
1099
- function mayReadBoxedValue(expr) {
1100
- return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
1101
- }
1102
-
1103
- function intConstValue(expr) {
1104
- if (typeof expr === 'number' && Number.isInteger(expr)) return expr
1105
- if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
1106
- if (typeof expr === 'string') {
1107
- const v = repOf(expr)?.intConst
1108
- if (v != null) return v
1109
- }
1110
- return null
1111
- }
1112
-
1113
- function bigintUnsignedBound(expr) {
1114
- const n = bigintConstValue(expr)
1115
- return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
1116
- }
1117
-
1118
- function bigintConstValue(expr) {
1119
- if (typeof expr === 'bigint') return expr
1120
- if (!Array.isArray(expr)) return null
1121
- if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
1122
- if (expr[0] === 'u-') {
1123
- const n = bigintConstValue(expr[1])
1124
- return n == null ? null : -n
1125
- }
1126
- return null
1127
- }
1128
-
1129
- function arrayIndexKey(key) {
1130
- const n = Number(key)
1131
- const u = n >>> 0
1132
- return String(u) === key && u !== 0xffffffff ? u : null
1133
- }
1134
-
1135
- /** Compound assignment: read → op → write back (via readVar/writeVar). */
1136
- function compoundAssign(name, val, f64op, i32op) {
1137
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1138
- const void_ = _expect === 'void'
1139
- const va = readVar(name), vb = emit(val)
1140
- // Peel f64.convert_i32_s/u when va is i32 — typed-array integer reads wrap their
1141
- // i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
1142
- // raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
1143
- // hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
1144
- let vbi = vb
1145
- if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
1146
- Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
1147
- const inner = vb[1]
1148
- vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
1149
- }
1150
- if (i32op && va.type === 'i32' && vbi.type === 'i32')
1151
- return writeVar(name, i32op(va, vbi), void_)
1152
- return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
1153
- }
1154
-
1155
- /**
1156
- * Core emitter table. Maps AST ops to WASM IR generators.
1157
- * ctx.core.emit is seeded with a flat copy of this object on reset;
1158
- * modules add or override ops on ctx.core.emit directly.
1159
- * @type {Record<string, (...args: any[]) => Array>}
1160
- */
1161
- export const emitter = {
1162
- // === Spread operator ===
1163
- // Note: spread is handled specially in call contexts; this catches stray uses
1164
- '...': () => err('Spread (...) can only be used in function/method calls or array literals'),
1165
-
1166
- // === Statements ===
1167
-
1168
- ';': (...args) => {
1169
- const out = []
1170
- for (const a of args) {
1171
- out.push(...emitFlat(a))
1172
- }
1173
- return out
1174
- },
1175
- '{': (...args) => args.map(emit).filter(x => x != null),
1176
- ',': (...args) => {
1177
- const results = args.map(emit).filter(x => x != null)
1178
- if (results.length === 0) return null
1179
- if (results.length === 1) return results[0]
1180
- const last = results[results.length - 1]
1181
- // Flatten: multi-instruction arrays (from ';') need spreading, typed nodes need drop
1182
- const spread = r => Array.isArray(r) && Array.isArray(r[0]) ? r : [r]
1183
- const dropSpread = r => r.type ? [['drop', r]] : spread(r)
1184
- // If last expression is void (store, etc.), add explicit return value
1185
- if (!last.type) {
1186
- return typed(['block', ['result', 'f64'],
1187
- ...results.flatMap(dropSpread),
1188
- ['f64.const', 0]], 'f64')
1189
- }
1190
- return typed(['block', ['result', last.type],
1191
- ...results.slice(0, -1).flatMap(dropSpread), last], last.type)
1192
- },
1193
- 'let': emitDecl,
1194
- 'const': emitDecl,
1195
- 'export': () => null,
1196
- // 'block' can appear from jzify transforming labeled blocks or as WASM block IR
1197
- 'block': (...args) => {
1198
- // WASM block IR: first arg is ['result', type] → pass through, preserve type
1199
- if (Array.isArray(args[0]) && args[0][0] === 'result')
1200
- return typed(['block', ...args], args[0][1])
1201
- const inner = args.length === 1 ? args[0] : [';', ...args]
1202
- return emitFlat(['{}', inner])
1203
- },
1204
-
1205
- 'throw': expr => {
1206
- ctx.runtime.throws = ctx.runtime.userThrows = true
1207
- const thrown = temp()
1208
- return typed(['block',
1209
- ['local.set', `$${thrown}`, asF64(emit(expr))],
1210
- ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${thrown}`]]],
1211
- ['throw', '$__jz_err', ['local.get', `$${thrown}`]]], 'void')
1212
- },
1213
-
1214
- 'catch': (body, errName, handler) => {
1215
- if (!canThrow(body)) return emitFlat(body)
1216
-
1217
- ctx.runtime.throws = ctx.runtime.userThrows = true
1218
- const id = ctx.func.uniq++
1219
- ctx.func.locals.set(errName, 'f64')
1220
- const prev = ctx.func.inTry; ctx.func.inTry = true
1221
- let bodyIR; try { bodyIR = emitFlat(body) } finally { ctx.func.inTry = prev }
1222
- const handlerIR = emitFlat(handler)
1223
- return typed(['block', `$outer${id}`, ['result', 'f64'],
1224
- ['block', `$catch${id}`, ['result', 'f64'],
1225
- ['try_table', ['catch', '$__jz_err', `$catch${id}`],
1226
- ...bodyIR],
1227
- ['f64.const', 0],
1228
- ['br', `$outer${id}`]],
1229
- ['local.set', `$${errName}`],
1230
- ...handlerIR,
1231
- ['f64.const', 0]], 'f64')
1232
- },
1233
-
1234
- 'finally': (body, cleanup) => {
1235
- if (!canThrow(body)) {
1236
- const parentStack = ctx.func.finallyStack || []
1237
- const activeStack = parentStack.concat([cleanup])
1238
- const bodyIR = withFinallyStack(activeStack, () => emitFlat(body))
1239
- const cleanupIR = isTerminator(body) ? [] : withFinallyStack(parentStack, () => emitFlat(cleanup))
1240
- return [...bodyIR, ...cleanupIR]
1241
- }
1242
-
1243
- ctx.runtime.throws = ctx.runtime.userThrows = true
1244
- const id = ctx.func.uniq++
1245
- const errLocal = temp('err')
1246
- const parentStack = ctx.func.finallyStack || []
1247
- const activeStack = parentStack.concat([cleanup])
1248
-
1249
- const prevTry = ctx.func.inTry
1250
- ctx.func.inTry = true
1251
- const bodyIR = withFinallyStack(activeStack, () => {
1252
- try { return emitFlat(body) }
1253
- finally { ctx.func.inTry = prevTry }
1254
- })
1255
- const normalCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
1256
- const throwCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
1257
-
1258
- return ['block', `$fin_done${id}`,
1259
- ['block', `$fin_catch${id}`, ['result', 'f64'],
1260
- ['try_table', ['catch', '$__jz_err', `$fin_catch${id}`],
1261
- ...bodyIR],
1262
- ...normalCleanup,
1263
- ['br', `$fin_done${id}`]],
1264
- ['local.set', `$${errLocal}`],
1265
- ...throwCleanup,
1266
- ['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${errLocal}`]]],
1267
- ['throw', '$__jz_err', ['local.get', `$${errLocal}`]]]
1268
- },
1269
-
1270
- 'return': expr => {
1271
- const finalizers = emitFinalizers()
1272
- const finalizerBlock = () => [['block', ...finalizers]]
1273
- if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
1274
- const vals = expr.slice(1).map(e => asF64(emit(e)))
1275
- if (finalizers.length === 0) return typed(['return', ...vals], 'void')
1276
- const names = vals.map(() => temp('ret'))
1277
- return [
1278
- ...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
1279
- ...finalizerBlock(),
1280
- typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
1281
- ]
1282
- }
1283
- if (expr == null) return [...finalizers, typed(['return', NULL_IR], 'void')]
1284
- const rt = ctx.func.current?.results[0] || 'f64'
1285
- const pk = ctx.func.current?.ptrKind
1286
- const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
1287
- const ty = pk != null ? 'i32' : rt
1288
- const tcoed = tcoTailRewrite(ir, ty)
1289
- if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
1290
- return typed(tcoed, 'void')
1291
- }
1292
- if (finalizers.length > 0) {
1293
- const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
1294
- return [
1295
- ['local.set', `$${name}`, tcoed],
1296
- ...finalizerBlock(),
1297
- typed(['return', ['local.get', `$${name}`]], 'void'),
1298
- ]
1299
- }
1300
- return typed(['return', tcoed], 'void')
1301
- },
1302
-
1303
- // === Assignment ===
1304
-
1305
- '=': (name, val) => {
1306
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1307
- const void_ = _expect === 'void'
1308
- // Array index assignment: arr[i] = x
1309
- if (Array.isArray(name) && name[0] === '[]') {
1310
- const [, arr, idx] = name
1311
- const keyType = keyValType(idx)
1312
- // A provably-numeric index name — an int-certain loop counter or a
1313
- // NUMBER-typed local — can never be a string key, so the runtime
1314
- // `__is_str_key` → `__dyn_set` dispatch is dead. Mirrors the index *read*
1315
- // path (`intIndexIR`), closing the read/write asymmetry on `arr[i] = …`
1316
- // inside refined-array loops (e.g. watr's recursive AST walkers).
1317
- const idxNumericName = typeof idx === 'string' &&
1318
- (repOf(idx)?.intCertain === true || repOf(idx)?.val === VAL.NUMBER)
1319
- const useRuntimeKeyDispatch = !idxNumericName &&
1320
- (keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING))
1321
- const keyExpr = asF64(emit(idx))
1322
- const valueExpr = asF64(emit(val))
1323
- const storeArrayValue = (arrExpr, idxNode, persist) => {
1324
- const arrTmp = `${T}asi${ctx.func.uniq++}`
1325
- const idxTmp = `${T}asj${ctx.func.uniq++}`
1326
- const valTmp = `${T}asv${ctx.func.uniq++}`
1327
- ctx.func.locals.set(arrTmp, 'f64')
1328
- ctx.func.locals.set(idxTmp, 'i32')
1329
- ctx.func.locals.set(valTmp, 'f64')
1330
- inc('__arr_set_idx_ptr')
1331
- const body = [
1332
- ['local.set', `$${arrTmp}`, arrExpr],
1333
- ['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
1334
- ['local.set', `$${valTmp}`, valueExpr],
1335
- ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
1336
- ]
1337
- if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
1338
- body.push(['local.get', `$${valTmp}`])
1339
- return typed(['block', ['result', 'f64'], ...body], 'f64')
1340
- }
1341
- const setDyn = () => {
1342
- if (ctx.transform.strict)
1343
- 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 }.`)
1344
- inc('__dyn_set')
1345
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
1346
- }
1347
- const dispatchKey = (numericIR) => {
1348
- if (ctx.transform.strict)
1349
- 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 }.`)
1350
- const keyTmp = temp()
1351
- return typed(['block', ['result', 'f64'],
1352
- ['local.set', `$${keyTmp}`, keyExpr],
1353
- ['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
1354
- ['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
1355
- ['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
1356
- }
1357
- // Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
1358
- const litKey = isLiteralStr(idx) ? idx[1]
1359
- : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
1360
- : null
1361
- // SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
1362
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
1363
- const fo = ctx.func.flatObjects.get(arr)
1364
- const fi = fo.names.indexOf(litKey)
1365
- if (fi >= 0) {
1366
- const t = temp()
1367
- return typed(['block', ['result', 'f64'],
1368
- ['local.set', `$${t}`, valueExpr],
1369
- ['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
1370
- ['local.get', `$${t}`]], 'f64')
1371
- }
1372
- }
1373
- if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
1374
- const slot = ctx.schema.find(arr, litKey)
1375
- if (slot >= 0) {
1376
- const t = temp()
1377
- return typed(['block', ['result', 'f64'],
1378
- ['local.set', `$${t}`, valueExpr],
1379
- ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), slot, ['local.get', `$${t}`]),
1380
- ['local.get', `$${t}`]], 'f64')
1381
- }
1382
- }
1383
- const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
1384
- if (arrIndex != null && typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
1385
- const persist = ptr => {
1386
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1387
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1388
- return ['local.set', `$${arr}`, ptr]
1389
- }
1390
- return storeArrayValue(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), persist)
1391
- }
1392
- if (keyType === VAL.STRING) return setDyn()
1393
- if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
1394
- lookupValType(arr) === 'typed') {
1395
- const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
1396
- if (r) return r
1397
- // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
1398
- // returns the stored value as f64, used directly as the expr result.
1399
- inc('__typed_set_idx')
1400
- return typed(['call', '$__typed_set_idx',
1401
- asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
1402
- }
1403
- if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
1404
- const inner = ctx.schema.emitInner(arr)
1405
- const arrVT = lookupValType(arr) || VAL.OBJECT
1406
- const storeNumeric = keyNode => storeArrayValue(inner, keyNode, ptr =>
1407
- ['f64.store', ptrOffsetIR(asF64(emit(arr)), arrVT), ptr])
1408
- if (useRuntimeKeyDispatch) {
1409
- inc('__dyn_set', '__is_str_key')
1410
- return dispatchKey(storeNumeric)
1411
- }
1412
- return typed(storeNumeric(keyExpr), 'f64')
1413
- }
1414
- const va = emit(arr), vi = asI32(emit(idx)), vv = valueExpr, t = temp()
1415
- if (typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
1416
- const persist = ptr => {
1417
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1418
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1419
- return ['local.set', `$${arr}`, ptr]
1420
- }
1421
- if (useRuntimeKeyDispatch) {
1422
- inc('__dyn_set', '__is_str_key')
1423
- return dispatchKey(keyNode => storeArrayValue(asF64(va), keyNode, persist))
1424
- }
1425
- return storeArrayValue(asF64(va), keyExpr, persist)
1426
- }
1427
- // arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
1428
- const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
1429
- const arrVT = knownArrVT || VAL.OBJECT
1430
- if (useRuntimeKeyDispatch) {
1431
- inc('__dyn_set', '__is_str_key')
1432
- // When arr type is unknown (could be TypedArray) and __typed_set_idx is
1433
- // available, dispatch the numeric branch through __ptr_type so TypedArray
1434
- // writes go by element type. Without this, ternary-typed arrays (e.g.
1435
- // `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
1436
- // f64.store boxed bytes regardless of element width.
1437
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1438
- if (knownArrVT == null && hasTypedSet) {
1439
- const objTmp = temp('asu')
1440
- const idxTmp = tempI32('asi')
1441
- inc('__ptr_type', '__typed_set_idx')
1442
- return dispatchKey(keyNode => {
1443
- const keyI32 = asI32(typed(keyNode, 'f64'))
1444
- return ['block', ['result', 'f64'],
1445
- ['local.set', `$${objTmp}`, asF64(va)],
1446
- ['local.set', `$${idxTmp}`, keyI32],
1447
- ['local.set', `$${t}`, vv],
1448
- ['if', ['result', 'f64'],
1449
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1450
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1451
- ['else', ['block', ['result', 'f64'],
1452
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1453
- ['local.get', `$${t}`]]]]]
1454
- })
1455
- }
1456
- return dispatchKey(keyNode => {
1457
- const keyI32 = asI32(typed(keyNode, 'f64'))
1458
- return ['block', ['result', 'f64'],
1459
- ['local.set', `$${t}`, vv],
1460
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', keyI32, ['i32.const', 3]]], ['local.get', `$${t}`]],
1461
- ['local.get', `$${t}`]]
1462
- })
1463
- }
1464
- if (typeof arr !== 'string') {
1465
- const objTmp = temp('asu')
1466
- const idxTmp = tempI32('asi')
1467
- const ptrTmp = temp('asp')
1468
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1469
- inc('__ptr_type', '__arr_set_idx_ptr')
1470
- if (hasTypedSet) inc('__typed_set_idx')
1471
- return typed(['block', ['result', 'f64'],
1472
- ['local.set', `$${objTmp}`, asF64(va)],
1473
- ['local.set', `$${idxTmp}`, vi],
1474
- ['local.set', `$${t}`, vv],
1475
- ['if', ['result', 'f64'],
1476
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1477
- ['then', ['block', ['result', 'f64'],
1478
- ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1479
- ['local.get', `$${t}`]]],
1480
- ['else', hasTypedSet ? ['if', ['result', 'f64'],
1481
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1482
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1483
- ['else', ['block', ['result', 'f64'],
1484
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1485
- ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1486
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1487
- ['local.get', `$${t}`]]]]], 'f64')
1488
- }
1489
- if (typeof arr === 'string' && knownArrVT == null) {
1490
- const objTmp = temp('asu')
1491
- const idxTmp = tempI32('asi')
1492
- const ptrTmp = temp('asp')
1493
- const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1494
- inc('__ptr_type', '__arr_set_idx_ptr')
1495
- if (hasTypedSet) inc('__typed_set_idx')
1496
- const persist = ptr => {
1497
- if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1498
- if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1499
- return ['local.set', `$${arr}`, ptr]
1500
- }
1501
- return typed(['block', ['result', 'f64'],
1502
- ['local.set', `$${objTmp}`, asF64(va)],
1503
- ['local.set', `$${idxTmp}`, vi],
1504
- ['local.set', `$${t}`, vv],
1505
- ['if', ['result', 'f64'],
1506
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1507
- ['then', ['block', ['result', 'f64'],
1508
- ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1509
- persist(['local.get', `$${ptrTmp}`]),
1510
- ['local.get', `$${t}`]]],
1511
- ['else', hasTypedSet ? ['if', ['result', 'f64'],
1512
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1513
- ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1514
- ['else', ['block', ['result', 'f64'],
1515
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1516
- ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1517
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1518
- ['local.get', `$${t}`]]]]], 'f64')
1519
- }
1520
- return typed(['block', ['result', 'f64'],
1521
- ['local.set', `$${t}`, vv],
1522
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
1523
- ['local.get', `$${t}`]], 'f64')
1524
- }
1525
- // Object property assignment: obj.prop = x
1526
- if (Array.isArray(name) && name[0] === '.') {
1527
- const [, obj, prop] = name
1528
- // SRoA flat object: `o.prop = x` → `local.set $o#i` (no heap store).
1529
- const flatW = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
1530
- if (flatW) {
1531
- const fi = flatW.names.indexOf(prop)
1532
- if (fi >= 0) {
1533
- const t = temp()
1534
- return typed(['block', ['result', 'f64'],
1535
- ['local.set', `$${t}`, asF64(emit(val))],
1536
- ['local.set', `$${obj}#${fi}`, ['local.get', `$${t}`]],
1537
- ['local.get', `$${t}`]], 'f64')
1538
- }
1539
- }
1540
- // Schema-based object → f64.store at fixed offset.
1541
- if (typeof obj === 'string' && ctx.schema.find) {
1542
- const idx = ctx.schema.find(obj, prop)
1543
- if (idx >= 0) {
1544
- const va = emit(obj), vv = asF64(emit(val)), t = temp()
1545
- const shadow = needsDynShadow(obj)
1546
- if (shadow) inc('__dyn_set')
1547
- const stmts = [
1548
- ['local.set', `$${t}`, vv],
1549
- ctx.abi.object.ops.store(ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), idx, ['local.get', `$${t}`]),
1550
- ]
1551
- if (shadow)
1552
- stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
1553
- stmts.push(['local.get', `$${t}`])
1554
- return typed(['block', ['result', 'f64'], ...stmts], 'f64')
1555
- }
1556
- }
1557
- if (typeof obj === 'string') {
1558
- const objType = keyValType(obj)
1559
- // OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
1560
- // properties go through __dyn_set, which writes to the per-OBJECT
1561
- // propsPtr at off-16 — same path as object-literal dyn shadow writes
1562
- // (module/object.js). __hash_set assumes HASH bucket layout and would
1563
- // corrupt OBJECT memory.
1564
- if (usesDynProps(objType) || objType === VAL.OBJECT) {
1565
- inc('__dyn_set')
1566
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1567
- }
1568
- if (ctx.func.names.has(obj) && !ctx.func.locals?.has(obj) && !ctx.func.current?.params?.some(p => p.name === obj)) {
1569
- inc('__dyn_set')
1570
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1571
- }
1572
- if (objType == null && ctx.transform.host !== 'wasi') {
1573
- ctx.features.external = true
1574
- }
1575
- inc('__hash_set')
1576
- const setCall = typed(['f64.reinterpret_i64', ['call', '$__hash_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1577
- if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
1578
- ['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
1579
- // Closure-captured (boxed) locals store the value at the cell address — local.tee
1580
- // would write to the i32 cell pointer, not the f64 value. Route through writeVar.
1581
- if (ctx.func.boxed?.has(obj)) return writeVar(obj, setCall, false)
1582
- return typed(['local.tee', `$${obj}`, setCall], 'f64')
1583
- }
1584
- if (ctx.transform.host !== 'wasi') ctx.features.external = true
1585
- inc('__dyn_set')
1586
- return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1587
- }
1588
- if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
1589
- if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
1590
- inc('__to_num')
1591
- return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
1592
- }
1593
- return writeVar(name, emit(val), void_)
1594
- },
1595
-
1596
- // Compound assignments: read-modify-write with type coercion
1597
- '+=': (name, val) => {
1598
- // Complex LHS (obj.prop, arr[i]) → desugar to side-effect-safe `name = name + val`
1599
- if (typeof name !== 'string') return emit(['=', name, ['+', name, val]])
1600
- // String concatenation: desugar to name = name + val (+ handler knows about strings).
1601
- // Also desugar when either side has unknown type — the `+` operator picks runtime
1602
- // string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
1603
- // silently corrupt string concatenations through unknown-typed values.
1604
- const vt = typeof name === 'string' ? keyValType(name) : null
1605
- const vtB = keyValType(val)
1606
- if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
1607
- if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
1608
- return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
1609
- },
1610
- ...Object.fromEntries([
1611
- ['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
1612
- ].map(([op, fn]) => [op, (name, val) => {
1613
- if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1614
- return compoundAssign(name, val,
1615
- (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1616
- fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1617
- )
1618
- }])),
1619
- '%=': (name, val) => {
1620
- if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
1621
- return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
1622
- },
1623
-
1624
- // Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
1625
- ...Object.fromEntries([
1626
- ['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
1627
- ['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
1628
- ].map(([op, fn]) => [op, (name, val) => {
1629
- if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1630
- if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
1631
- const void_ = _expect === 'void'
1632
- const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
1633
- return writeVar(name, result, void_)
1634
- }
1635
- return compoundAssign(name, val,
1636
- (a, b) => asF64(typed([`i32.${fn}`, toI32(a), toI32(b)], 'i32')),
1637
- (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1638
- )
1639
- }])),
1640
-
1641
- // Logical compound assignments: a ||= b → a = a || b, a &&= b → a = a && b
1642
- // Logical/nullish compound assignments: read → check → conditionally write
1643
- // For complex LHS (obj.prop, arr[i]): emit as check(read(lhs)) ? write(lhs, val) : read(lhs)
1644
- ...Object.fromEntries(['||=', '&&=', '??='].map(op => [op, (name, val) => {
1645
- // Complex LHS → desugar (side-effect-safe since obj/arr/idx are locals)
1646
- if (typeof name !== 'string') {
1647
- const baseOp = op.slice(0, -1) // '||', '&&', '??'
1648
- return emit([baseOp, name, ['=', name, val]])
1649
- }
1650
- if (isConst(name)) err(`Assignment to const '${name}'`)
1651
- const void_ = _expect === 'void'
1652
- const t = temp()
1653
- const va = readVar(name)
1654
- // Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
1655
- const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1656
- const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
1657
- // &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
1658
- const [thenExpr, elseExpr] = op === '||='
1659
- ? [['local.get', `$${t}`], asF64(emit(val))]
1660
- : [asF64(emit(val)), ['local.get', `$${t}`]]
1661
- const result = typed(['if', ['result', 'f64'], cond, ['then', thenExpr], ['else', elseExpr]], 'f64')
1662
- // Write back (handles boxed/global/local)
1663
- if (ctx.func.boxed?.has(name)) {
1664
- const bt = temp()
1665
- return typed(['block', ['result', 'f64'],
1666
- ['local.set', `$${bt}`, result],
1667
- ['f64.store', boxedAddr(name), ['local.get', `$${bt}`]],
1668
- ['local.get', `$${bt}`]], 'f64')
1669
- }
1670
- return writeVar(name, result, void_)
1671
- }])),
1672
-
1673
- // === Increment/Decrement ===
1674
- // Postfix resolved in prepare: i++ → (++i) - 1
1675
-
1676
- ...Object.fromEntries([['++', 'add'], ['--', 'sub']].map(([op, fn]) => [op, name => {
1677
- if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1678
- const void_ = _expect === 'void'
1679
- const v = readVar(name)
1680
- const one = v.type === 'i32' ? ['i32.const', 1] : ['f64.const', 1]
1681
- return writeVar(name, typed([`${v.type}.${fn}`, v, one], v.type), void_)
1682
- }])),
1683
-
1684
- // === Arithmetic (type-preserving) ===
1685
-
1686
- // Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
1687
- '+': (a, b) => {
1688
- if (_expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
1689
- // String concatenation: pure string operands skip generic ToString coercion.
1690
- const vtA = keyValType(a)
1691
- const vtB = keyValType(b)
1692
- if (vtA === VAL.STRING && vtB === VAL.STRING) {
1693
- // Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
1694
- // generic concat dispatch when rhs is a string-index. The byte flows
1695
- // straight from __char_at into memory, and the bump-extend path elides
1696
- // the alloc+copy when lhs is the heap-top STRING.
1697
- if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
1698
- if (keyValType(b[1]) === VAL.STRING) {
1699
- inc('__str_append_byte', '__char_at')
1700
- return typed(['call', '$__str_append_byte',
1701
- asI64(emit(a)),
1702
- ctx.abi.string.ops.charCodeAt(asF64(emit(b[1])), asI32(emit(b[2])), ctx),
1703
- ], 'f64')
1704
- }
1705
- }
1706
- return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
1707
- }
1708
- if (vtA === VAL.STRING || vtB === VAL.STRING) {
1709
- // An OBJECT operand coerces via ToPrimitive(string) at compile time —
1710
- // __str_concat's runtime __to_str cannot invoke a user-defined toString.
1711
- // A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
1712
- const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
1713
- : vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
1714
- const ea = strOperand(vtA, a)
1715
- const eb = strOperand(vtB, b)
1716
- return typed(ctx.abi.string.ops.concat(ea, eb, ctx), 'f64')
1717
- }
1718
- if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
1719
- return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
1720
- // Runtime string dispatch when at least one side could be a string. When one side has
1721
- // a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
1722
- // chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
1723
- // operand needs the runtime check.
1724
- if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
1725
- const tA = temp('add'), tB = temp('add')
1726
- inc('__str_concat', '__is_str_key')
1727
- const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
1728
- const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
1729
- const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
1730
- const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1731
- if (checkA && checkB) {
1732
- return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
1733
- }
1734
- // Exactly one side is checked. Pre-eval the known side first, then the if branches on the unknown.
1735
- const preEval = vtA == null ? ['local.set', `$${tB}`, asF64(emit(b))] : ['local.set', `$${tA}`, asF64(emit(a))]
1736
- return typed(['block', ['result', 'f64'],
1737
- preEval,
1738
- ['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
1739
- ], 'f64')
1740
- }
1741
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
1742
- if (_f) return _f
1743
- // Neither side is a string here (string paths handled above), but either may
1744
- // still be null/undefined/pointer — numeric `+` performs ToNumber like `-`/`*`.
1745
- if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1746
- if (isLit(va) && litVal(va) === 0) return toNumF64(b, vb)
1747
- // An `.unsigned` operand is a uint32 (range [0, 2^32)); JS `+` is a float
1748
- // op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
1749
- // Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
1750
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
1751
- return typed(['f64.add', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1752
- },
1753
- '-': (a, b) => {
1754
- if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
1755
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1756
- return b === undefined
1757
- ? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1758
- : fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
1759
- if (b === undefined) return emitNeg(a)
1760
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
1761
- if (_f) return _f
1762
- if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1763
- // Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
1764
- // so avoid the wrapping i32.sub fast-path. See `+` above.
1765
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
1766
- return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1767
- },
1768
- 'u+': a => {
1769
- if (valTypeOf(a) === VAL.BIGINT)
1770
- return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
1771
- const v = emit(a)
1772
- if (v.type === 'i32') return asF64(v)
1773
- if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
1774
- inc('__to_num')
1775
- return typed(['call', '$__to_num', asI64(v)], 'f64')
1776
- },
1777
- 'u-': a => emitNeg(a),
1778
- '*': (a, b) => {
1779
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1780
- return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
1781
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
1782
- if (_f) return _f
1783
- if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1784
- if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
1785
- if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
1786
- if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
1787
- // `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
1788
- // `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
1789
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
1790
- return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1791
- },
1792
- '/': (a, b) => {
1793
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1794
- return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
1795
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
1796
- if (_f) return _f
1797
- if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1798
- return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1799
- },
1800
- '%': (a, b) => {
1801
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1802
- return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
1803
- const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
1804
- if (_f) return _f
1805
- // ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
1806
- // The i32.rem_s fast path traps on a zero divisor, so divert a literal-zero divisor.
1807
- if (isLit(vb) && litVal(vb) === 0) return emitNum(NaN)
1808
- // `.unsigned` operand: `i32.rem_s` reads the uint32 as a negative signed value
1809
- // ((2^32-1)%7 → rem_s(-1,7) = -1, not 3). Widen to f64 — see `+` above.
1810
- if (isI32Num(va) && isI32Num(vb) && !va.unsigned && !vb.unsigned) return typed(['i32.rem_s', va, vb], 'i32')
1811
- return f64rem(toNumF64(a, va), toNumF64(b, vb))
1812
- },
1813
- // === Comparisons (always i32 result) ===
1814
-
1815
- '==': (a, b) => {
1816
- const charCmp = emitSingleCharIndexCmp(a, b)
1817
- if (charCmp) return charCmp
1818
- const subCmp = emitSubstringEqCmp(a, b)
1819
- if (subCmp) return subCmp
1820
- // JS loose nullish equality: x == null / x == undefined.
1821
- // If the non-literal side has a known non-null VAL type, fold to 0.
1822
- if (isNullishLit(a)) {
1823
- if (valTypeOf(b)) return emitNum(0)
1824
- return isNullish(asF64(emit(b)))
1825
- }
1826
- if (isNullishLit(b)) {
1827
- if (valTypeOf(a)) return emitNum(0)
1828
- return isNullish(asF64(emit(a)))
1829
- }
1830
- // typeof x == 'string' → compile-time type check (prepare rewrites string to type code)
1831
- const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
1832
- const va = emit(a), vb = emit(b)
1833
- if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
1834
- // Either side known-pure NUMBER (literal or typed) → f64.eq is correct regardless
1835
- // of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
1836
- // reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
1837
- // Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
1838
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1839
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1840
- if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
1841
- if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
1842
- if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1843
- // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1844
- // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
1845
- // STRING needs __eq (heap strings can be equal by content but different pointers).
1846
- // BIGINT needs __eq (heap-allocated, content compare).
1847
- if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1848
- return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1849
- }
1850
- inc('__eq')
1851
- return typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
1852
- },
1853
- '!=': (a, b) => {
1854
- const charCmp = emitSingleCharIndexCmp(a, b, true)
1855
- if (charCmp) return charCmp
1856
- const subCmp = emitSubstringEqCmp(a, b, true)
1857
- if (subCmp) return subCmp
1858
- if (isNullishLit(a)) {
1859
- if (valTypeOf(b)) return emitNum(1)
1860
- return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
1861
- }
1862
- if (isNullishLit(b)) {
1863
- if (valTypeOf(a)) return emitNum(1)
1864
- return typed(['i32.eqz', isNullish(asF64(emit(a)))], 'i32')
1865
- }
1866
- const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
1867
- const va = emit(a), vb = emit(b)
1868
- if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1869
- const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
1870
- const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
1871
- if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
1872
- if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
1873
- if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1874
- if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1875
- return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1876
- }
1877
- inc('__eq')
1878
- return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
1879
- },
1880
- '===': (a, b) => emitStrictEq(a, b, false),
1881
- '!==': (a, b) => emitStrictEq(a, b, true),
1882
- '<': cmpOp('lt_s', 'lt', (a, b) => a < b),
1883
- '>': cmpOp('gt_s', 'gt', (a, b) => a > b),
1884
- '<=': cmpOp('le_s', 'le', (a, b) => a <= b),
1885
- '>=': cmpOp('ge_s', 'ge', (a, b) => a >= b),
1886
-
1887
- // === Logical ===
1888
-
1889
- '!': a => {
1890
- const v = emit(a)
1891
- if (v.type === 'i32') return typed(['i32.eqz', v], 'i32')
1892
- // Unboxed pointer offsets: falsy iff zero offset.
1893
- if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
1894
- // Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
1895
- // Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
1896
- // VAL.BOOL rides the 0/1 numeric carrier (not a pointer), so normalize it to
1897
- // NUMBER and let it fall to the truthy path — `!false` must be `true`.
1898
- const vt = numericVal(resolveValType(a, valTypeOf, lookupValType))
1899
- if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
1900
- return isNullish(asF64(v))
1901
- }
1902
- inc('__is_truthy')
1903
- return typed(['i32.eqz', ['call', '$__is_truthy', asI64(v)]], 'i32')
1904
- },
1905
-
1906
- '?:': (a, b, c) => {
1907
- // Constant condition → emit only the live branch
1908
- const ca = emit(a)
1909
- if (isLit(ca)) { const v = litVal(ca); return (v !== 0 && v === v) ? emit(b) : emit(c) }
1910
- const cond = toBoolFromEmitted(ca)
1911
- // Flow-sensitive refinement: each arm sees narrowing consistent with `a` being truthy / falsy.
1912
- const thenRefs = extractRefinements(a, new Map(), true)
1913
- const elseRefs = extractRefinements(a, new Map(), false)
1914
- const vb = withRefinements(thenRefs, b, () => emit(b))
1915
- const vc = withRefinements(elseRefs, c, () => emit(c))
1916
- // L: Use WASM select for pure ternaries — branchless, smaller bytecode
1917
- if (vb.type === 'i32' && vc.type === 'i32') {
1918
- // Propagate matching ptrKind/ptrAux so a downstream asF64 takes the NaN-rebox
1919
- // path instead of `f64.convert_i32_s`. Mismatched kinds drop both — caller's
1920
- // asF64 will treat the i32 as numeric, which is correct for non-pointer i32s.
1921
- // ptrKind matches but ptrAux differs (e.g. polymorphic OBJECT with two
1922
- // distinct schemaIds, or TYPED with two element types) — fall through to
1923
- // the f64 path. There each arm reboxes independently, preserving its own
1924
- // aux in the NaN-box. The single-i32 path can only carry one aux on the
1925
- // result, so `boxPtrIR` would default to 0 and lose the runtime schema /
1926
- // elemType bits needed by downstream lookups (e.g. __dyn_get's OBJECT-
1927
- // schema fallback uses receiver aux to resolve `.prop`).
1928
- const auxMismatch = vb.ptrKind != null && vb.ptrKind === vc.ptrKind
1929
- && (vb.ptrAux ?? null) !== (vc.ptrAux ?? null)
1930
- if (!auxMismatch) {
1931
- const tagPtr = (n) => {
1932
- if (vb.ptrKind != null && vb.ptrKind === vc.ptrKind) {
1933
- n.ptrKind = vb.ptrKind
1934
- if (vb.ptrAux != null && vb.ptrAux === vc.ptrAux) n.ptrAux = vb.ptrAux
1935
- }
1936
- return n
1937
- }
1938
- if (isPureIR(vb) && isPureIR(vc))
1939
- return tagPtr(typed(['select', vb, vc, cond], 'i32'))
1940
- return tagPtr(typed(['if', ['result', 'i32'], cond, ['then', vb], ['else', vc]], 'i32'))
1941
- }
1942
- }
1943
- const fb = asF64(vb), fc = asF64(vc)
1944
- const vtb = resolveValType(b, valTypeOf, lookupValType)
1945
- const vtc = resolveValType(c, valTypeOf, lookupValType)
1946
- const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
1947
- const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
1948
- || vb.closureFuncIdx != null || vc.closureFuncIdx != null
1949
- || isNaNBoxLit(fb) || isNaNBoxLit(fc)
1950
- if (refPayload) {
1951
- const ib = ['i64.reinterpret_f64', fb]
1952
- const ic = ['i64.reinterpret_f64', fc]
1953
- const bits = isPureIR(fb) && isPureIR(fc)
1954
- ? ['select', ib, ic, cond]
1955
- : ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
1956
- return typed(['f64.reinterpret_i64', bits], 'f64')
1957
- }
1958
- if (!refPayload && isPureIR(fb) && isPureIR(fc))
1959
- return typed(['select', fb, fc, cond], 'f64')
1960
- return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
1961
- },
1962
-
1963
- '&&': (a, b) => {
1964
- const va = emit(a)
1965
- // Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
1966
- if (isLit(va)) {
1967
- const v = litVal(va)
1968
- if (v !== 0 && v === v) {
1969
- const refs = extractRefinements(a, new Map(), true)
1970
- return withRefinements(refs, b, () => emit(b))
1971
- }
1972
- return va
1973
- }
1974
- // a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
1975
- // (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
1976
- const rightRefs = extractRefinements(a, new Map(), true)
1977
- const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
1978
- // i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
1979
- // skip f64 round-trip and __is_truthy call entirely.
1980
- if (va.type === 'i32') {
1981
- const vb = emitRight()
1982
- const t = tempI32()
1983
- if (vb.type === 'i32') {
1984
- return typed(['if', ['result', 'i32'],
1985
- ['local.tee', `$${t}`, va],
1986
- ['then', vb],
1987
- ['else', ['local.get', `$${t}`]]], 'i32')
1988
- }
1989
- return typed(['if', ['result', 'f64'],
1990
- ['local.tee', `$${t}`, va],
1991
- ['then', asF64(vb)],
1992
- ['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
1993
- }
1994
- const t = temp()
1995
- const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1996
- return typed(['if', ['result', 'f64'],
1997
- toBoolFromEmitted(teed),
1998
- ['then', asF64(emitRight())],
1999
- ['else', ['local.get', `$${t}`]]], 'f64')
2000
- },
2001
-
2002
- '||': (a, b) => {
2003
- const va = emit(a)
2004
- // Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
2005
- if (isLit(va)) {
2006
- const v = litVal(va)
2007
- if (v !== 0 && v === v) return va
2008
- const refs = extractRefinements(a, new Map(), false)
2009
- return withRefinements(refs, b, () => emit(b))
2010
- }
2011
- // a is falsy in the right-arm — `x == null || ...` proves x is null/undefined in b;
2012
- // De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
2013
- const rightRefs = extractRefinements(a, new Map(), false)
2014
- const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
2015
- if (va.type === 'i32') {
2016
- const vb = emitRight()
2017
- const t = tempI32()
2018
- if (vb.type === 'i32') {
2019
- return typed(['if', ['result', 'i32'],
2020
- ['local.tee', `$${t}`, va],
2021
- ['then', ['local.get', `$${t}`]],
2022
- ['else', vb]], 'i32')
2023
- }
2024
- return typed(['if', ['result', 'f64'],
2025
- ['local.tee', `$${t}`, va],
2026
- ['then', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')],
2027
- ['else', asF64(vb)]], 'f64')
2028
- }
2029
- const t = temp()
2030
- const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
2031
- return typed(['if', ['result', 'f64'],
2032
- toBoolFromEmitted(teed),
2033
- ['then', ['local.get', `$${t}`]],
2034
- ['else', asF64(emitRight())]], 'f64')
2035
- },
2036
-
2037
- // a ?? b: returns b only if a is nullish
2038
- '??': (a, b) => {
2039
- const va = emit(a)
2040
- const t = temp()
2041
- return typed(['if', ['result', 'f64'],
2042
- // Check: is a NOT nullish?
2043
- ['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
2044
- ['then', ['local.get', `$${t}`]],
2045
- ['else', asF64(emit(b))]], 'f64')
2046
- },
2047
-
2048
- 'void': a => {
2049
- const v = emit(a)
2050
- const dropAndUndef = (instr) => typed(['block', ['result', 'f64'], instr, 'drop', undefExpr()], 'f64')
2051
- if (v == null) return undefExpr()
2052
- const op = Array.isArray(v) ? v[0] : null
2053
- const wasmVoid = op === 'local.set' || (typeof op === 'string' && op.endsWith('.store'))
2054
- || op === 'memory.copy' || op === 'global.set'
2055
- if (wasmVoid)
2056
- return typed(['block', ['result', 'f64'], v, undefExpr()], 'f64')
2057
- if (v.type && v.type !== 'void')
2058
- return dropAndUndef(v)
2059
- return typed(['block', ['result', 'f64'], ...flat(v), undefExpr()], 'f64')
2060
- },
2061
-
2062
- '(': a => emit(a),
2063
-
2064
- // === Bitwise (i32 for numbers, i64 for BigInt) ===
2065
-
2066
- // Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
2067
- // i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
2068
- // the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
2069
- // unknown type) routes through __to_num so "2026" | 0 === 2026.
2070
- '~': a => { 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') },
2071
- ...Object.fromEntries([
2072
- ['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
2073
- ].map(([op, fn]) => [op, (a, b) => {
2074
- if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
2075
- return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
2076
- const va = emit(a), vb = emit(b)
2077
- if (isLit(va) && isLit(vb)) {
2078
- const la = litVal(va), lb = litVal(vb)
2079
- if (op === '&') return emitNum(la & lb); if (op === '|') return emitNum(la | lb)
2080
- if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
2081
- if (op === '>>') return emitNum(la >> lb)
2082
- }
2083
- const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
2084
- const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
2085
- return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
2086
- }])),
2087
- '>>>': (a, b) => {
2088
- const va = emit(a), vb = emit(b)
2089
- if (isLit(va) && isLit(vb)) {
2090
- const r = litVal(va) >>> litVal(vb) // JS uint32 result ∈ [0, 2^32)
2091
- // ≥ 2^31 doesn't fit signed i32: materialize the wrapped bits as an i32 const
2092
- // tagged `.unsigned` so `asF64` lifts via `convert_i32_u`. Emitting `f64.const r`
2093
- // here (the old foldConst path) would `trunc_sat_f64_s`-saturate to INT32_MAX
2094
- // when the enclosing function narrows to an i32 result. Values < 2^31 fold to a
2095
- // plain i32 const (signed == unsigned, stays foldable downstream).
2096
- if (r >= 0x80000000) { const node = typed(['i32.const', r | 0], 'i32'); node.unsigned = true; return node }
2097
- return emitNum(r)
2098
- }
2099
- // F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
2100
- // [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
2101
- // signed for negative-high-bit s values, flipping sign and breaking the
2102
- // canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
2103
- const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
2104
- const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
2105
- const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
2106
- node.unsigned = true
2107
- return node
2108
- },
2109
-
2110
- // === Control flow ===
2111
-
2112
- 'if': (cond, then, els) => {
2113
- // Dead branch elimination: constant condition → emit only the live branch
2114
- const ce = emit(cond)
2115
- if (isLit(ce)) {
2116
- const v = litVal(ce), truthy = v !== 0 && v === v
2117
- if (truthy) return emitFlat(then)
2118
- if (els != null) return emitFlat(els)
2119
- return null
2120
- }
2121
- const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
2122
- // Flow-sensitive type refinement: narrow types within each branch based on the guard.
2123
- const thenRefs = extractRefinements(cond, new Map(), true)
2124
- const elseRefs = extractRefinements(cond, new Map(), false)
2125
- const thenBody = withRefinements(thenRefs, then, () => emitFlat(then))
2126
- if (els != null) {
2127
- const elseBody = withRefinements(elseRefs, els, () => emitFlat(els))
2128
- return ['if', c, ['then', ...thenBody], ['else', ...elseBody]]
2129
- }
2130
- return ['if', c, ['then', ...thenBody]]
2131
- },
2132
-
2133
- 'for': (init, cond, step, body) => {
2134
- if (body === undefined) return err('for-in/for-of not supported')
2135
- if (!ctx.transform.optimize || ctx.transform.optimize.smallConstForUnroll !== false) {
2136
- const unrolled = unrollSmallConstFor(init, cond, step, body)
2137
- if (unrolled) return unrolled
2138
- }
2139
- const id = ctx.func.uniq++
2140
- const brk = `$brk${id}`, loop = `$loop${id}`
2141
- // The cont wrapper is only needed if the body has a `continue` AND there is a step
2142
- // expression — `continue` must jump to before the step. Without a step, `continue`
2143
- // can target the loop label directly, saving a redundant `block`.
2144
- const needsCont = step && hasOwnContinue(body)
2145
- const cont = needsCont ? `$cont${id}` : loop
2146
- ctx.func.stack.push({ brk, loop: cont })
2147
- const result = []
2148
- if (init != null) result.push(...emitFlat(init))
2149
- const loopBody = []
2150
- if (cond) loopBody.push(['br_if', brk, ['i32.eqz', toBool(cond)]])
2151
- if (needsCont) loopBody.push(['block', cont, ...emitFlat(body)])
2152
- else loopBody.push(...emitFlat(body))
2153
- if (step) loopBody.push(...emitFlat(step))
2154
- loopBody.push(['br', loop])
2155
- result.push(['block', brk, ['loop', loop, ...loopBody]])
2156
- ctx.func.stack.pop()
2157
- return result.length === 1 ? result[0] : result
2158
- },
2159
-
2160
- 'switch': (discriminant, ...cases) => {
2161
- const disc = `${T}disc${ctx.func.uniq++}`
2162
- ctx.func.locals.set(disc, 'f64')
2163
-
2164
- const result = [['local.set', `$${disc}`, asF64(emit(discriminant))]]
2165
-
2166
- for (const c of cases) {
2167
- if (c[0] === 'case') {
2168
- const [, test, body] = c
2169
- const skip = `$skip${ctx.func.uniq++}`
2170
- // Block: skip if discriminant != test, otherwise execute body
2171
- result.push(['block', skip,
2172
- ['br_if', skip, typed(['f64.ne', typed(['local.get', `$${disc}`], 'f64'), asF64(emit(test))], 'i32')],
2173
- ...emitFlat(body)])
2174
- } else if (c[0] === 'default') {
2175
- result.push(...emitFlat(c[1]))
2176
- }
2177
- }
2178
-
2179
- return result
2180
- },
2181
-
2182
- 'while': (cond, body) => emitter['for'](null, cond, null, body),
2183
- 'label': (name, body) => {
2184
- const brk = `$label${ctx.func.uniq++}`
2185
- ctx.func.stack.push({ label: name, brk })
2186
- const result = ['block', brk, ...emitFlat(body)]
2187
- ctx.func.stack.pop()
2188
- return result
2189
- },
2190
- 'break': (label) => {
2191
- const target = label == null
2192
- ? loopTop().brk
2193
- : ctx.func.stack.findLast(frame => frame.label === label)?.brk
2194
- if (!target) err(`break label '${label}' is not in scope`)
2195
- return [...emitFinalizers(), ['br', target]]
2196
- },
2197
- 'continue': (label) => {
2198
- if (label != null) err(`continue label '${label}' is not supported`)
2199
- return [...emitFinalizers(), ['br', loopTop().loop]]
2200
- },
2201
-
2202
- // === Call ===
2203
-
2204
- // Arrow as value → closure
2205
- '=>': (rawParams, body) => {
2206
- if (!ctx.closure.make) err('Closures require fn module (auto-included)')
2207
-
2208
- const raw = extractParams(rawParams)
2209
- const params = [], defaults = {}
2210
- let restParam = null, bodyPrefix = []
2211
- for (const r of raw) {
2212
- const c = classifyParam(r)
2213
- if (c.kind === 'rest') { restParam = c.name; params.push(c.name) }
2214
- else if (c.kind === 'plain') params.push(c.name)
2215
- else if (c.kind === 'default') { params.push(c.name); defaults[c.name] = c.defValue }
2216
- else {
2217
- const tmp = `${T}p${ctx.func.uniq++}`
2218
- params.push(tmp)
2219
- if (c.kind === 'destruct-default') defaults[tmp] = c.defValue
2220
- bodyPrefix.push(['let', ['=', c.pattern, tmp]])
2221
- }
2222
- }
2223
-
2224
- // Prepend destructuring to body (if any destructured params)
2225
- if (bodyPrefix.length) {
2226
- if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
2227
- body = ['{}', [';', ...bodyPrefix, ...body[1].slice(1)]]
2228
- else if (Array.isArray(body) && body[0] === '{}')
2229
- body = ['{}', [';', ...bodyPrefix, body[1]]]
2230
- else body = ['{}', [';', ...bodyPrefix, ['return', body]]]
2231
- }
2232
-
2233
- // Find free variables in body that aren't params → captures
2234
- const paramSet = new Set(params)
2235
- const captures = []
2236
- findFreeVars(body, paramSet, captures)
2237
- for (const def of Object.values(defaults)) findFreeVars(def, paramSet, captures)
2238
-
2239
- // Pass closure info including rest param and defaults
2240
- const closureInfo = { params, body, captures, restParam }
2241
- if (Object.keys(defaults).length) closureInfo.defaults = defaults
2242
- return ctx.closure.make(closureInfo)
2243
- },
2244
-
2245
- '()': (callee, callArgs) => {
2246
- let argList = Array.isArray(callArgs)
2247
- ? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
2248
- : callArgs ? [callArgs] : []
2249
-
2250
- // Helper: expand spread arguments into flat list of normal arguments + spread markers
2251
- // Returns { normal: [...], spreads: [(pos, expr), ...] }
2252
- const parseArgs = (args) => {
2253
- const normal = []
2254
- const spreads = []
2255
- for (let i = 0; i < args.length; i++) {
2256
- const arg = args[i]
2257
- if (Array.isArray(arg) && arg[0] === '...') {
2258
- spreads.push({ pos: normal.length, expr: arg[1] })
2259
- } else {
2260
- normal.push(arg)
2261
- }
2262
- }
2263
- return { normal, spreads, hasSpread: spreads.length > 0 }
2264
- }
2265
-
2266
- const parsed = parseArgs(argList)
2267
-
2268
- // Closure devirtualization: a callee that is a module global proven (by
2269
- // plan.js) to hold one statically-known function for the whole post-init
2270
- // program rewrites to that function — the known-top-level-function branch
2271
- // below then emits a direct `call`, dropping the indirect/trampoline path.
2272
- if (typeof callee === 'string' && ctx.func.globalDevirt?.has(callee))
2273
- callee = ctx.func.globalDevirt.get(callee)
2274
-
2275
- // Optional method call: obj?.method(args) — null if obj is nullish, else
2276
- // obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
2277
- // distinct from the regular method call's ['.', obj, method] callee. Receiver
2278
- // hoists into a temp so the nullish check and the method dispatch below see
2279
- // the same evaluation; recursion with a synthetic '.'-callee reuses the type-
2280
- // aware method-dispatch in this same handler rather than duplicating it.
2281
- if (Array.isArray(callee) && callee[0] === '?.') {
2282
- const [, obj, method] = callee
2283
- const t = `${T}om${ctx.func.uniq++}`
2284
- ctx.func.locals.set(t, 'f64')
2285
- const va = asF64(emit(obj))
2286
- const methodCall = emitter['()'](['.', t, method], callArgs)
2287
- return typed(['block', ['result', 'f64'],
2288
- ['local.set', `$${t}`, va],
2289
- ['if', ['result', 'f64'],
2290
- ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
2291
- ['then', asF64(methodCall)],
2292
- ['else', nullExpr()]]], 'f64')
2293
- }
2294
-
2295
- // Method call: obj.method(args) → type-aware dispatch
2296
- if (Array.isArray(callee) && callee[0] === '.') {
2297
- const [, obj, method] = callee
2298
-
2299
- // charCodeAt with a statically in-bounds index — emit the i32
2300
- // (OOB-impossible) contract directly; the generic path keeps the
2301
- // f64/NaN JS-spec result. See analyze.js inBoundsCharCodeAt.
2302
- if (method === 'charCodeAt' && !parsed.hasSpread && parsed.normal.length === 1
2303
- && ctx.abi.string?.ops?.charCodeAt && inBoundsCharCodeAt(ctx).has(callee)) {
2304
- const recv = emit(obj)
2305
- // jsstring carrier: receiver is an externref boundary param. Route to
2306
- // `wasm:js-string.charCodeAt` directly — the in-bounds proof rules out
2307
- // the OOB trap the builtin would otherwise raise.
2308
- if (recv?.type === 'externref') {
2309
- ctx.core.jsstring.add('charCodeAt')
2310
- return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
2311
- }
2312
- return typed(ctx.abi.string.ops.charCodeAt(
2313
- asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
2314
- }
2315
-
2316
- // Function property call: fn.prop(args) → direct call to fn$prop.
2317
- // Skipped when the property was reassigned (wrapper composition) — then
2318
- // it is a mutable slot and must be read dynamically before the call.
2319
- if (typeof obj === 'string' && ctx.func.names.has(obj) && !ctx.func.multiProp.has(`${obj}.${method}`)) {
2320
- const fname = `${obj}$${method}`
2321
- if (ctx.func.names.has(fname)) {
2322
- const func = ctx.func.map.get(fname)
2323
- const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
2324
- while (emittedArgs.length < func.sig.params.length)
2325
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2326
- const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
2327
- if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2328
- if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2329
- if (func.sig.unsignedResult) callIR.unsigned = true
2330
- return callIR
2331
- }
2332
- }
2333
-
2334
- let vt = keyValType(obj)
2335
- // A reassigned slice/concat receiver may carry a stale `vt` — a reassignment
2336
- // inside a nested closure escapes analyzeValTypes' poisoning (its walk stops
2337
- // at `=>`). Drop to runtime dispatch, but only for guessy types: STRING/ARRAY
2338
- // dispatch correctly either way, and BUFFER/TYPED are construction proofs
2339
- // (`new ArrayBuffer`/`new XxxArray`) — the runtime String/Array fallback has
2340
- // no branch for them, so nulling `vt` would miscompile `ab.slice()` into an
2341
- // f64-array copy. jzify also splits every `var x = init` into `let x; x = init`,
2342
- // marking single-assignment vars "reassigned"; keeping definite BUFFER/TYPED
2343
- // is what keeps `var`-declared buffers correct.
2344
- if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
2345
- && (method === 'slice' || method === 'concat')
2346
- && vt !== VAL.STRING && vt !== VAL.ARRAY
2347
- && vt !== VAL.BUFFER && vt !== VAL.TYPED) vt = null
2348
-
2349
- // Helper to call method with arguments (handles spread expansion)
2350
- const callMethod = (objArg, methodEmitter) => {
2351
- if (!parsed.hasSpread) {
2352
- return methodEmitter(objArg, ...parsed.normal)
2353
- }
2354
-
2355
- // Bulk push fast path: `obj.push(...src)` — single spread, no normal args, named obj.
2356
- // The generic single-spread loop below calls methodEmitter per iteration, which expands
2357
- // to a full .push (grow check + ptr_offset + store + set_len) every step. Amortising the
2358
- // grow + set_len across the whole spread eliminates ~3 stdlib calls per byte in watr's
2359
- // hot `out.push(...HANDLER[op](...))` path (~24M bytes/iter on raycast).
2360
- if (method === 'push' && parsed.normal.length === 0 &&
2361
- parsed.spreads.length === 1 && typeof objArg === 'string') {
2362
- const spreadExpr = parsed.spreads[0].expr
2363
- inc('__len'); inc('__arr_grow'); inc('__set_len'); inc('__ptr_offset')
2364
- const o = `${T}po${ctx.func.uniq++}`,
2365
- sa = `${T}psa${ctx.func.uniq++}`,
2366
- sl = `${T}psl${ctx.func.uniq++}`,
2367
- ol = `${T}pol${ctx.func.uniq++}`,
2368
- si = `${T}psi${ctx.func.uniq++}`,
2369
- base = `${T}pb${ctx.func.uniq++}`
2370
- ctx.func.locals.set(o, 'f64'); ctx.func.locals.set(sa, 'f64')
2371
- ctx.func.locals.set(sl, 'i32'); ctx.func.locals.set(ol, 'i32')
2372
- ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
2373
-
2374
- const objIsArr = lookupValType(objArg) === VAL.ARRAY
2375
- // A materialized multi-value is not a statically-typed pointer — let
2376
- // emitSpreadCopy resolve its kind once at runtime.
2377
- const srcVT = multiCount(spreadExpr) ? undefined : valTypeOf(spreadExpr)
2378
- const n = multiCount(spreadExpr)
2379
- const ir = []
2380
- ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
2381
- ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
2382
- ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
2383
- // Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
2384
- if (objIsArr) {
2385
- ir.push(['local.set', `$${ol}`,
2386
- ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
2387
- } else {
2388
- ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
2389
- }
2390
- // Single grow for the full spread (vs per-element grow check in the generic loop).
2391
- ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
2392
- ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
2393
- // base captured AFTER grow (grow may relocate the array).
2394
- ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
2395
- // Bulk-copy the spread: an ARRAY source is a contiguous f64 block → memory.copy.
2396
- ir.push(['local.set', `$${si}`, ['local.get', `$${ol}`]])
2397
- ir.push(...emitSpreadCopy(base, si, sa, sl, srcVT))
2398
- // Single set_len for the full spread.
2399
- ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
2400
- ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
2401
- // Update source variable: grow may have moved the pointer.
2402
- if (ctx.func.boxed?.has(objArg)) {
2403
- ir.push(['f64.store', ['local.get', `$${ctx.func.boxed.get(objArg)}`], ['local.get', `$${o}`]])
2404
- } else if (ctx.scope.globals.has(objArg) && !ctx.func.locals?.has(objArg)) {
2405
- ir.push(['global.set', `$${objArg}`, ['local.get', `$${o}`]])
2406
- } else {
2407
- ir.push(['local.set', `$${objArg}`, ['local.get', `$${o}`]])
2408
- }
2409
- ir.push(['f64.convert_i32_s', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
2410
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
2411
- }
2412
-
2413
- // Single spread at end: call method with normal args, then loop spread elements
2414
- if (parsed.spreads.length === 1 && parsed.spreads[0].pos === parsed.normal.length) {
2415
- const spreadExpr = parsed.spreads[0].expr
2416
- 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++}`
2417
- ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
2418
- ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
2419
- // Emit-time rep seeding for a fresh spread-staging local (no prior reader).
2420
- // Without this, the loop body's `[]` read on `arr` falls back to polymorphic
2421
- // dispatch — VAL.* on the rep elides STRING gate for ARRAY/TYPED spreads.
2422
- const spreadVT = valTypeOf(spreadExpr)
2423
- if (spreadVT) updateRep(arr, { val: spreadVT })
2424
-
2425
- // In-place spread methods modify target; accumulating methods (concat) return new values
2426
- const inPlace = SPREAD_MUTATORS.has(method)
2427
- // unshift prepends each arg to the front — iterating forward reverses the
2428
- // intended order, so walk the spread from end to start.
2429
- const reverseIter = method === 'unshift'
2430
- const ir = []
2431
- ir.push(['local.set', `$${acc}`, asF64(emit(objArg))])
2432
- if (parsed.normal.length > 0) {
2433
- const r = asF64(methodEmitter(objArg, ...parsed.normal))
2434
- ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
2435
- }
2436
-
2437
- inc('__len')
2438
- const n = multiCount(spreadExpr)
2439
- ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
2440
- ir.push(['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]])
2441
- ir.push(['local.set', `$${idx}`,
2442
- reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
2443
- const loopId = ctx.func.uniq++
2444
- const loopBody = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
2445
- ir.push(['block', `$break${loopId}`,
2446
- ['loop', `$continue${loopId}`,
2447
- ['br_if', `$break${loopId}`,
2448
- reverseIter
2449
- ? ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]
2450
- : ['i32.ge_u', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
2451
- inPlace ? ['drop', loopBody] : ['local.set', `$${acc}`, loopBody],
2452
- ['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['i32.const', reverseIter ? -1 : 1]]],
2453
- ['br', `$continue${loopId}`]]])
2454
-
2455
- ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
2456
- return typed(['block', ['result', 'f64'], ...ir], 'f64')
2457
- }
2458
-
2459
- // General spread case: iterate args in original order, batch contiguous normal
2460
- // args into a single call, emit a per-element loop for each spread.
2461
- //
2462
- // inPlace methods (push/unshift/add/set): call methodEmitter(objArg, ...) each
2463
- // time so the source variable's local gets updated (else heap grow/realloc
2464
- // wouldn't be visible to subsequent uses of the variable). Final value is objArg.
2465
- //
2466
- // non-inPlace (concat, etc.): chain via temp acc since return value is the new
2467
- // collection.
2468
- const inPlaceG = SPREAD_MUTATORS.has(method)
2469
- const combinedG = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2470
- inc('__len')
2471
-
2472
- if (inPlaceG) {
2473
- const irG = []
2474
- let batch = []
2475
- const flushBatch = () => {
2476
- if (!batch.length) return
2477
- irG.push(['drop', asF64(methodEmitter(objArg, ...batch))])
2478
- batch = []
2479
- }
2480
- for (const item of combinedG) {
2481
- if (Array.isArray(item) && item[0] === '__spread') {
2482
- flushBatch()
2483
- const spreadExpr = item[1]
2484
- const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
2485
- ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
2486
- // Emit-time rep seeding for fresh spread-staging local (see arr-spread comment above).
2487
- const spreadVT = valTypeOf(spreadExpr)
2488
- if (spreadVT) updateRep(arrL, { val: spreadVT })
2489
- const n = multiCount(spreadExpr)
2490
- irG.push(
2491
- ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
2492
- ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
2493
- ['local.set', `$${idxL}`, ['i32.const', 0]])
2494
- const loopId = ctx.func.uniq++
2495
- const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
2496
- irG.push(['block', `$break${loopId}`,
2497
- ['loop', `$continue${loopId}`,
2498
- ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
2499
- ['drop', loopBody],
2500
- ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
2501
- ['br', `$continue${loopId}`]]])
2502
- } else {
2503
- batch.push(item)
2504
- }
2505
- }
2506
- flushBatch()
2507
- irG.push(asF64(emit(objArg)))
2508
- return typed(['block', ['result', 'f64'], ...irG], 'f64')
2509
- }
2510
-
2511
- const accG = `${T}acc${ctx.func.uniq++}`
2512
- ctx.func.locals.set(accG, 'f64')
2513
- const irG = [['local.set', `$${accG}`, asF64(emit(objArg))]]
2514
- let batch = []
2515
- const flushBatch = () => {
2516
- if (!batch.length) return
2517
- irG.push(['local.set', `$${accG}`, asF64(methodEmitter(accG, ...batch))])
2518
- batch = []
2519
- }
2520
- for (const item of combinedG) {
2521
- if (Array.isArray(item) && item[0] === '__spread') {
2522
- flushBatch()
2523
- const spreadExpr = item[1]
2524
- const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
2525
- ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
2526
- const spreadVT = valTypeOf(spreadExpr)
2527
- if (spreadVT) updateRep(arrL, { val: spreadVT })
2528
- const n = multiCount(spreadExpr)
2529
- irG.push(
2530
- ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
2531
- ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
2532
- ['local.set', `$${idxL}`, ['i32.const', 0]])
2533
- const loopId = ctx.func.uniq++
2534
- const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
2535
- irG.push(['block', `$break${loopId}`,
2536
- ['loop', `$continue${loopId}`,
2537
- ['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
2538
- ['local.set', `$${accG}`, loopBody],
2539
- ['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
2540
- ['br', `$continue${loopId}`]]])
2541
- } else {
2542
- batch.push(item)
2543
- }
2544
- }
2545
- flushBatch()
2546
- irG.push(['local.get', `$${accG}`])
2547
- return typed(['block', ['result', 'f64'], ...irG], 'f64')
2548
- }
2549
-
2550
- // Boxed object: delegate method to inner value (slot 0)
2551
- if (typeof obj === 'string' && ctx.schema.isBoxed?.(obj)) {
2552
- const innerVt = repOf(obj)?.val
2553
- const emitter = ctx.core.emit[`.${innerVt}:${method}`] || ctx.core.emit[`.${method}`]
2554
- if (emitter) {
2555
- const innerName = `${obj}${T}inner`
2556
- if (!ctx.func.locals.has(innerName)) ctx.func.locals.set(innerName, 'f64')
2557
- const boxBase = tempI32('bb')
2558
- // Load current inner value from boxed object's slot 0 (may have been updated by prior mutations)
2559
- // Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
2560
- const loadInner = [
2561
- ['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
2562
- ['local.set', `$${innerName}`, ctx.abi.object.ops.load(['local.get', `$${boxBase}`], 0)]]
2563
- const result = callMethod(innerName, emitter)
2564
- // Mutating methods may reallocate; writeback inner value to boxed slot
2565
- if (BOXED_MUTATORS.has(method)) {
2566
- const wb = ctx.abi.object.ops.store(['local.get', `$${boxBase}`], 0, ['local.get', `$${innerName}`])
2567
- return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
2568
- }
2569
- // Non-mutating: just load inner and call
2570
- return typed(['block', ['result', 'f64'], ...loadInner, asF64(result)], 'f64')
2571
- }
2572
- }
2573
-
2574
- // Known type → static dispatch
2575
- if (vt && ctx.core.emit[`.${vt}:${method}`]) {
2576
- return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
2577
- }
2578
-
2579
- // Unknown / guessed-array type, both string + generic exist → runtime dispatch by ptr type.
2580
- // analyze.js defaults untyped `.slice()` results to VAL.ARRAY, which is a guess, not a proof;
2581
- // runtime dispatch resolves whether the operand is actually a string or an array.
2582
- // Concretely-typed non-string values (BUFFER, TYPED, MAP, …) fall through to the generic
2583
- // emitter which already knows how to handle them.
2584
- const strKey = `.string:${method}`, genKey = `.${method}`
2585
- if ((!vt || vt === VAL.ARRAY) && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
2586
- const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
2587
- ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
2588
- const strEmitter = ctx.core.emit[strKey]
2589
- const genEmitter = ctx.core.emit[genKey]
2590
- return typed(['block', ['result', 'f64'],
2591
- ['local.set', `$${t}`, asF64(emit(obj))],
2592
- ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
2593
- ['if', ['result', 'f64'],
2594
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
2595
- ['then', callMethod(t, strEmitter)],
2596
- ['else', callMethod(t, genEmitter)]]], 'f64')
2597
- }
2598
-
2599
- // Schema property function call: x.prop(args) where prop is a closure in boxed schema
2600
- if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
2601
- const idx = ctx.schema.find(obj, method)
2602
- if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
2603
- const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
2604
- if (parsed.hasSpread) {
2605
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2606
- return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
2607
- }
2608
- return ctx.closure.call(propRead, parsed.normal)
2609
- }
2610
- }
2611
-
2612
- // Schema property function call: x.prop(args) where prop is a closure in boxed schema
2613
- if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
2614
- const idx = ctx.schema.find(obj, method)
2615
- if (idx >= 0) {
2616
- const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
2617
- return ctx.closure.call(propRead, parsed.normal)
2618
- }
2619
- }
2620
-
2621
- // Generic only — but a collection emitter (`.get`/`.set`/`.has`/`.add`/
2622
- // `.delete`) assumes a Map/Set receiver: a proven collection already
2623
- // dispatched via `.${vt}:${method}` above, so reaching here means the
2624
- // receiver is not a proven collection. A zero-arg call then cannot be the
2625
- // collection op (each needs ≥1 key/value arg) — it is a user/closure
2626
- // method (e.g. `new C().get()`). Skip the collection emitter so it falls
2627
- // through to closure/dynamic dispatch instead of crashing on `emit(key)`.
2628
- const collectionMisfit = COLLECTION_METHODS.has(method) &&
2629
- !parsed.hasSpread && parsed.normal.length === 0
2630
- if (ctx.core.emit[genKey] && !collectionMisfit) {
2631
- return callMethod(obj, ctx.core.emit[genKey])
2632
- }
2633
-
2634
- // Dynamic property function call on non-external values.
2635
- if (ctx.closure.call) {
2636
- if (ctx.transform.strict)
2637
- 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 }.`)
2638
- const objTmp = `${T}mobj${ctx.func.uniq++}`
2639
- ctx.func.locals.set(objTmp, 'f64')
2640
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2641
- const arrayIR = buildArrayWithSpreads(combined)
2642
- const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
2643
- const propTmp = `${T}mprop${ctx.func.uniq++}`
2644
- ctx.func.locals.set(propTmp, 'f64')
2645
- if (usesDynProps(vt)) {
2646
- inc('__dyn_get_expr', '__ptr_type')
2647
- return typed(['block', ['result', 'f64'],
2648
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2649
- ['local.set', `$${propTmp}`, propRead],
2650
- ['if', ['result', 'f64'],
2651
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2652
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2653
- ['else', undefExpr()]]], 'f64')
2654
- }
2655
- // WASI: no PTR.EXTERNAL values; closure-only dispatch is correct.
2656
- if (ctx.transform.host === 'wasi') {
2657
- inc('__dyn_get_expr', '__ptr_type')
2658
- return typed(['block', ['result', 'f64'],
2659
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2660
- ['local.set', `$${propTmp}`, propRead],
2661
- ['if', ['result', 'f64'],
2662
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2663
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2664
- ['else', undefExpr()]]], 'f64')
2665
- }
2666
- inc('__dyn_get_expr', '__ext_call', '__ptr_type')
2667
- ctx.features.external = true
2668
- return typed(['block', ['result', 'f64'],
2669
- ['local.set', `$${objTmp}`, asF64(emit(obj))],
2670
- ['local.set', `$${propTmp}`, propRead],
2671
- ['if', ['result', 'f64'],
2672
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2673
- ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2674
- ['else', ['if', ['result', 'f64'],
2675
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.EXTERNAL]],
2676
- ['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
2677
- ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
2678
- ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2679
- ['i64.reinterpret_f64', arrayIR]]]],
2680
- ['else', undefExpr()]]]]], 'f64')
2681
- }
2682
-
2683
- // Unknown callee - assume external method
2684
- if (ctx.transform.strict)
2685
- 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 }.`)
2686
- // Under wasi there is no host `__ext_call` — the call lowers to a
2687
- // no-op returning `undefined`. This is by-design so polymorphic code
2688
- // can target js and wasi from one source; users who want fail-fast
2689
- // pass `strict: true` (handled above).
2690
- if (ctx.transform.host === 'wasi') return undefExpr()
2691
- inc('__ext_call')
2692
- ctx.features.external = true
2693
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2694
- const arrayIR = buildArrayWithSpreads(combined)
2695
- return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
2696
- ['i64.reinterpret_f64', asF64(emit(obj))],
2697
- ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2698
- ['i64.reinterpret_f64', arrayIR]]], 'f64');
2699
- }
2700
-
2701
- if (typeof callee === 'string' && ctx.core.emit[callee] && !isBoundName(callee) && !isUserFunc(callee)) {
2702
- // Pass spread args through to emitter (e.g. Math.max(...arr))
2703
- if (parsed.hasSpread) {
2704
- const allArgs = []
2705
- let ni = 0
2706
- for (const s of parsed.spreads) {
2707
- while (ni < s.pos) allArgs.push(parsed.normal[ni++])
2708
- allArgs.push(['...', s.expr])
2709
- }
2710
- while (ni < parsed.normal.length) allArgs.push(parsed.normal[ni++])
2711
- return ctx.core.emit[callee](...allArgs)
2712
- }
2713
- return ctx.core.emit[callee](...parsed.normal)
2714
- }
2715
-
2716
- // Direct call if callee is a known top-level function
2717
- if (typeof callee === 'string' && ctx.func.names.has(callee) && !isBoundName(callee)) {
2718
- const func = ctx.func.map.get(callee)
2719
-
2720
- // Rest param case: collect all args (including expanded spreads) into array
2721
- if (func?.rest) {
2722
- const fixedParamCount = func.sig.params.length - 1
2723
- const fixedArgs = parsed.normal.slice(0, fixedParamCount)
2724
- // Pad missing fixed args with `undefined` so default-param init triggers per spec.
2725
- const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
2726
- while (emittedFixed.length < fixedParamCount)
2727
- emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2728
-
2729
- // Reconstruct with spreads, then take rest args
2730
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2731
- const restArgsFinal = combined.slice(fixedParamCount)
2732
-
2733
- // Build array: emit code for normal args + code to expand spreads
2734
- const arrayIR = buildArrayWithSpreads(restArgsFinal)
2735
- const callIR = typed(['call', `$${callee}`,
2736
- ...emittedFixed,
2737
- arrayIR], func.sig.results[0])
2738
- if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2739
- if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2740
- if (func.sig.unsignedResult) callIR.unsigned = true
2741
- return callIR
2742
- }
2743
-
2744
- // Regular function call without rest params
2745
- if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
2746
- // Pad missing args with `undefined` so default-param init triggers per spec
2747
- // (only undefined, not null, should trigger defaults). Drop extras to match
2748
- // JS calling convention — emitting them anyway produces an invalid call
2749
- // when the callee is a fixed-arity import (e.g. `_interp`-registered host
2750
- // stubs) since wasm validates arg count. Use ?? rather than || so a
2751
- // legitimate 0-arity callee isn't bypassed.
2752
- const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
2753
- const expected = func?.sig.params.length ?? args.length
2754
- while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2755
- if (args.length > expected) args.length = expected
2756
- // Multi-value return: materialize as heap array (caller expects single pointer)
2757
- if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
2758
- const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
2759
- if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2760
- if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2761
- // Unsigned-uint32 result (every tail was `>>>`): consumer's asF64 must use
2762
- // `f64.convert_i32_u` instead of `_s`, preserving the [0, 2^32) range.
2763
- if (func?.sig.unsignedResult) callIR.unsigned = true
2764
- return callIR
2765
- }
2766
-
2767
- // A3: const-bound, non-escaping closure → direct call to body (skip call_indirect).
2768
- // emitDecl registered name → bodyName when it saw the closure.make IR. Body signature
2769
- // is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64. We pass the closure
2770
- // NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
2771
- if (typeof callee === 'string' && !parsed.hasSpread
2772
- && ctx.func.directClosures?.has(callee)) {
2773
- const bodyName = ctx.func.directClosures.get(callee)
2774
- const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
2775
- const n = parsed.normal.length
2776
- if (n <= W) {
2777
- const slots = parsed.normal.map(a => asF64(emit(a)))
2778
- while (slots.length < W) slots.push(undefExpr())
2779
- return typed(['call', `$${bodyName}`,
2780
- asF64(emit(callee)),
2781
- typed(['i32.const', n], 'i32'),
2782
- ...slots], 'f64')
2783
- }
2784
- }
2785
-
2786
- // Closure call: callee is a variable holding a NaN-boxed closure pointer
2787
- // Uniform convention: fn.call packs all args into an array
2788
- if (ctx.closure.call) {
2789
- if (parsed.hasSpread) {
2790
- // Spread: build the args array directly (handles __spread markers)
2791
- const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2792
- const arrayIR = buildArrayWithSpreads(combined)
2793
- // Pass pre-built array as single already-emitted arg
2794
- return ctx.closure.call(emit(callee), [arrayIR], true)
2795
- }
2796
- return ctx.closure.call(emit(callee), parsed.normal)
2797
- }
2798
-
2799
- // Unknown callee — assume direct call. Match arg count to the declared
2800
- // signature when the callee is a registered env import (e.g. `_interp`-
2801
- // wired host stubs). JS calling convention drops extras and pads missing;
2802
- // wasm requires exact arity, so emitting raw argList against a fixed-arity
2803
- // import would invalidate the module.
2804
- let calleeArity = null
2805
- if (typeof callee === 'string') {
2806
- const imp = ctx.module.imports?.find(i =>
2807
- Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
2808
- if (imp) {
2809
- let n = 0
2810
- for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
2811
- calleeArity = n
2812
- }
2813
- }
2814
- const emittedArgs = argList.map(a => asF64(emit(a)))
2815
- if (calleeArity != null) {
2816
- while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
2817
- if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
2818
- }
2819
- return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
2820
- },
2821
- }
2822
-
2823
- // === Emit dispatch ===
2824
-
2825
- // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
2826
- // Per ECMAScript, an optional access short-circuits the entire continuation, not just
2827
- // its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
2828
- // nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
2829
- //
2830
- // At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
2831
- // the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
2832
- // with that optional replaced by a regular access. The single guard short-circuits the
2833
- // whole continuation. Nested optionals further inside the chain are left intact and
2834
- // handle their own short-circuiting on recursion.
2835
- function liftOptionalChain(node) {
2836
- const path = []
2837
- let cur = node
2838
- while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
2839
- cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
2840
- path.push(cur)
2841
- cur = cur[1]
2842
- }
2843
- // Find the deepest optional with continuation outside it. optIdx === 0 means the
2844
- // chain root itself is optional with no continuation — handled by the regular
2845
- // `?.` / `?.[]` / `?.()` emitters.
2846
- let optIdx = -1
2847
- for (let i = path.length - 1; i >= 1; i--) {
2848
- if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
2849
- optIdx = i
2850
- break
2851
- }
2852
- }
2853
- if (optIdx <= 0) return null
2854
- const opt = path[optIdx]
2855
- const headExpr = opt[1]
2856
- const t = temp('oc')
2857
- let rebuilt
2858
- if (opt[0] === '?.') rebuilt = ['.', t, opt[2]]
2859
- else if (opt[0] === '?.[]') rebuilt = ['[]', t, opt[2]]
2860
- else rebuilt = ['()', t, ...opt.slice(2)]
2861
- for (let i = optIdx - 1; i >= 0; i--) {
2862
- const outer = path[i]
2863
- rebuilt = [outer[0], rebuilt, ...outer.slice(2)]
2864
- }
2865
- return typed(['block', ['result', 'f64'],
2866
- ['local.set', `$${t}`, asF64(emit(headExpr))],
2867
- ['if', ['result', 'f64'],
2868
- ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
2869
- ['then', asF64(emit(rebuilt))],
2870
- ['else', undefExpr()]]], 'f64')
2871
- }
2872
-
2873
- /**
2874
- * Emit single AST node to typed WASM IR.
2875
- * Every returned node has .type = 'i32' | 'f64'.
2876
- * @param {import('./prepare.js').ASTNode} node
2877
- * @returns {Array} typed WASM S-expression
2878
- */
2879
- export function emit(node, expect) {
2880
- _expect = expect || null
2881
- if (Array.isArray(node)) {
2882
- ctx.error.node = node
2883
- if (node.loc != null) ctx.error.loc = node.loc
2884
- }
2885
- if (node == null) return null
2886
- // Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
2887
- // AST), but their working representation is the plain number 0/1 — identical
2888
- // codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
2889
- if (node === true) return emitNum(1)
2890
- if (node === false) return emitNum(0)
2891
- if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
2892
- return nullExpr()
2893
- if (typeof node === 'bigint') {
2894
- // Truncate to 64 bits — `BigInt.asUintN(64, …)` semantics, same as the
2895
- // explicit mask `node & 0xFFFFFFFFFFFFFFFFn`. Decimal form (vs. the prior
2896
- // unsigned-hex dance) is enough now that watr's optimize.js getConst
2897
- // handles signed strings correctly (4.6.8 W5 fix).
2898
- return typed(['f64.reinterpret_i64', ['i64.const', BigInt.asUintN(64, node).toString()]], 'f64')
2899
- }
2900
- if (typeof node === 'number') return emitNum(node)
2901
- if (typeof node === 'string') {
2902
- // Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
2903
- 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)
2904
- return readVar(node)
2905
- // Top-level function used as value → wrap as closure pointer for call_indirect
2906
- if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
2907
- // Trampoline signature: uniform closure ABI (env f64, argc i32, a0..a{MAX-1} f64) → f64.
2908
- // Forwards the first N inline slots to $func where N = func's fixed param count.
2909
- const func = ctx.func.map.get(node)
2910
- const sigParams = func?.sig.params || []
2911
- if (sigParams.length > MAX_CLOSURE_ARITY) err(`Function ${node} used as closure value has ${sigParams.length} params, exceeds MAX_CLOSURE_ARITY=${MAX_CLOSURE_ARITY}`)
2912
- const trampolineName = `${T}tramp_${node}`
2913
- if (!ctx.core.stdlib[trampolineName]) {
2914
- const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
2915
- const paramDecls = ['(param $__env f64)', '(param $__argc i32)']
2916
- for (let i = 0; i < W; i++) paramDecls.push(`(param $__a${i} f64)`)
2917
- // Forward fixed slots; if func expects i32, convert via trunc_sat
2918
- const fwd = sigParams.map((p, i) =>
2919
- p.type === 'i32'
2920
- ? `(i32.trunc_sat_f64_s (local.get $__a${i}))`
2921
- : `(local.get $__a${i})`).join(' ')
2922
- if ((func?.sig.results.length || 1) > 1) {
2923
- const n = func.sig.results.length
2924
- const arr = `${T}retarr`
2925
- const temps = Array.from({ length: n }, (_, i) => `${T}ret${i}`)
2926
- const tempLocals = temps.map(name => `(local $${name} f64)`).join(' ')
2927
- const stores = temps.map((name, i) =>
2928
- `(f64.store (i32.add (local.get $${arr}) (i32.const ${i * 8})) (local.get $${name}))`
2929
- ).join(' ')
2930
- const capture = temps.slice().reverse().map(name => `(local.set $${name})`).join(' ')
2931
- 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})))`
2932
- inc(trampolineName, '__alloc', '__mkptr')
2933
- } else {
2934
- // Convert i32/i64 results back to f64 — uniform closure ABI returns f64.
2935
- const resType = func?.sig.results[0]
2936
- const callExpr = `(call $${node} ${fwd})`
2937
- const wrapped = resType === 'i32'
2938
- ? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
2939
- : resType === 'i64'
2940
- ? `(f64.reinterpret_i64 ${callExpr})`
2941
- : callExpr
2942
- ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${wrapped})`
2943
- inc(trampolineName)
2944
- }
2945
- }
2946
- let idx = ctx.closure.table.indexOf(trampolineName)
2947
- if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
2948
- const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
2949
- ir.closureFuncIdx = idx
2950
- return ir
2951
- }
2952
- // Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
2953
- if (node.includes('.') && ctx.core.emit[node]) {
2954
- const handler = ctx.core.emit[node]
2955
- return handler.length > 0 ? builtinFunctionValue(node) : handler()
2956
- }
2957
- // Auto-import known host globals (WebAssembly, globalThis, etc.)
2958
- const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
2959
- if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
2960
- if (ctx.transform.host === 'wasi') err(`host:'wasi': reference to host global \`${node}\` requires an env import. Remove the reference or use host:'js'.`)
2961
- ctx.features.external = true
2962
- ctx.scope.globals.set(node, null)
2963
- // Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed
2964
- // external-ref payload across the wasm↔JS global boundary (same hazard
2965
- // as env.print/setTimeout — see module/console.js header). asF64()
2966
- // reinterprets to f64 at every read site.
2967
- ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, 'i64']])
2968
- ctx.scope.globalTypes.set(node, 'i64')
2969
- return typed(['global.get', `$${node}`], 'i64')
2970
- }
2971
- const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
2972
- return typed(['local.get', `$${node}`], t)
2973
- }
2974
- if (!Array.isArray(node)) return typed(['f64.const', 0], 'f64')
2975
-
2976
- const [op, ...args] = node
2977
- // WASM IR passthrough: internally-generated IR nodes (from statement flattening) pass through
2978
- if (typeof op === 'string' && !ctx.core.emit[op] && (op.includes('.') || WASM_OPS.has(op))) return node
2979
-
2980
- // Literal node [, value] — handle null/undefined values
2981
- if (op == null && args.length === 1) {
2982
- const v = args[0]
2983
- return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
2984
- }
2985
-
2986
- // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
2987
- // Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
2988
- // with the optional already replaced by a non-optional access on a guarded temp.
2989
- if (op === '.' || op === '[]' || op === '()') {
2990
- const lifted = liftOptionalChain(node)
2991
- if (lifted) return lifted
2992
- }
2993
-
2994
- const handler = ctx.core.emit[op]
2995
- if (!handler) err(`Unknown op: ${op}`)
2996
- return handler(...args)
2997
- }