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