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
|
@@ -0,0 +1,3512 @@
|
|
|
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
|
+
* `emitVoid(node)` emits + drops any value (statement context; routes block bodies to emitBlockBody).
|
|
14
|
+
* `emitBlockBody(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 live in `ir.js` and are imported below.
|
|
20
|
+
*
|
|
21
|
+
* @module emit
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
|
|
26
|
+
hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
|
|
27
|
+
} from '../ast.js'
|
|
28
|
+
import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
|
|
29
|
+
import { nonNegIntLiteral, intLiteralValue, staticPropertyKey } from '../static.js'
|
|
30
|
+
import { findFreeVars } from './analyze.js'
|
|
31
|
+
import {
|
|
32
|
+
containsNestedClosure, containsNestedLoop, nestedSmallLoopBudget,
|
|
33
|
+
containsDeclOf, cloneWithSubst, containsKnownTypedArrayIndex,
|
|
34
|
+
smallConstForTripCount, isTerminator, scanBoundedLoops, inBoundsCharCodeAt,
|
|
35
|
+
exprType, MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
|
|
36
|
+
} from '../type.js'
|
|
37
|
+
import { valTypeOf, shapeOf } from '../kind.js'
|
|
38
|
+
import { VAL, lookupValType, repOf, updateRep, repOfGlobal } from '../reps.js'
|
|
39
|
+
import {
|
|
40
|
+
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
41
|
+
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
42
|
+
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
43
|
+
mkPtrIR, ptrOffsetIR, ptrTypeIR, ptrTypeEq, dispatchByPtrType, sidecarOverride, valKindToPtr,
|
|
44
|
+
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64, maskBound,
|
|
45
|
+
truthyIR, toBoolFromEmitted, isPostfix,
|
|
46
|
+
isGlobal, isConst, usesDynProps, needsDynShadow,
|
|
47
|
+
temp, tempI32, tempI64, allocPtr,
|
|
48
|
+
block64, withTemp,
|
|
49
|
+
boxedAddr, readVar, writeVar, isNullish, isNull, isUndef, isBoolAtom,
|
|
50
|
+
boolBoxIR,
|
|
51
|
+
isLiteralStr, resolveValType, isFuncRef,
|
|
52
|
+
multiCount, loopTop, flat,
|
|
53
|
+
reconstructArgsWithSpreads, tcoTailRewrite,
|
|
54
|
+
} from '../ir.js'
|
|
55
|
+
import { isBoundName } from '../ir.js'
|
|
56
|
+
import { extractRefinements, withRefinements } from './flow-types.js'
|
|
57
|
+
import { emitElementAssign, emitPropertyAssign, persistBindingPtr } from './emit-assign.js'
|
|
58
|
+
|
|
59
|
+
const stringOps = (node) => {
|
|
60
|
+
const rep = typeof node === 'string' ? repOf(node) : null
|
|
61
|
+
return ctx.abi.resolve('string', rep)?.ops ?? ctx.abi.string.ops
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// === Emitter state & operand classification ===
|
|
66
|
+
|
|
67
|
+
// Current emission "expect" mode ('void' or null); set by emit(), read by
|
|
68
|
+
// compound-assignment emitters (here and in emit-assign.js — shared via ctx so
|
|
69
|
+
// the module graph stays acyclic) to decide between value-returning and
|
|
70
|
+
// side-effect-only forms. Transient: meaningful only within one dispatch.
|
|
71
|
+
|
|
72
|
+
// A genuine i32 *number* — safe for the i32 fast path in arithmetic/bitwise
|
|
73
|
+
// operators. An unboxed pointer (object/array/string/closure local kept as a
|
|
74
|
+
// raw i32 handle) is *also* i32-typed but carries `.ptrKind`; treating it as a
|
|
75
|
+
// number would compute on raw pointer bits. A ptrKind-carrying operand must
|
|
76
|
+
// instead route through ToNumber (`toNumF64`), which performs ToPrimitive.
|
|
77
|
+
const isI32Num = (v) => v.type === 'i32' && v.ptrKind == null
|
|
78
|
+
|
|
79
|
+
// f64 arithmetic that can MINT a sign-nondeterministic NaN (0/0, ∞−∞, 0·∞, x%0): on x86
|
|
80
|
+
// these are 0xFFF8…, on arm 0x7FF8…. sqrt/min/max/neg are NOT here — they canon at their
|
|
81
|
+
// own emit (math.js / unary `-`), so they reach canonNum already canonical.
|
|
82
|
+
const NAN_MINTING = new Set(['f64.div', 'f64.add', 'f64.sub', 'f64.mul'])
|
|
83
|
+
|
|
84
|
+
const canonNum = (node) => {
|
|
85
|
+
// Fold a possibly-non-canonical NaN to the canonical number-NaN before it reaches a
|
|
86
|
+
// bit-comparing consumer (__is_truthy / untyped === / typeof), which match the canonical
|
|
87
|
+
// NaN by bits and so misread x86's 0xFFF8 as truthy. ONLY an un-canon'd NaN-minting
|
|
88
|
+
// arithmetic op can carry such a value — literals, i32-conversions, opaque locals/calls
|
|
89
|
+
// (canonical by the canon-at-source invariant) and already-canon'd shapes don't — so
|
|
90
|
+
// skipping everything else keeps the size win. (The broken middle ground was
|
|
91
|
+
// `02873d0`'s `isNumericIR` skip, which dropped canon for f64.div too → x86 miscompile.)
|
|
92
|
+
const arith = Array.isArray(node) &&
|
|
93
|
+
(NAN_MINTING.has(node[0]) || (node[0] === 'call' && node[1] === '$__rem'))
|
|
94
|
+
if (!arith) return node
|
|
95
|
+
const t = temp('cn')
|
|
96
|
+
return typed(['block', ['result', 'f64'],
|
|
97
|
+
['local.set', `$${t}`, node],
|
|
98
|
+
['select',
|
|
99
|
+
['f64.const', 'nan'],
|
|
100
|
+
['local.get', `$${t}`],
|
|
101
|
+
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Is an emitted arm `v` (AST `node`) a plain NUMBER? The predicate the two-arm merges
|
|
105
|
+
// (?:, ??) share to decide canon: an i32 number, NUMBER-tagged IR, or a NUMBER
|
|
106
|
+
// value-type qualifies; a pointer/opaque arm does not. `vt` is the node's resolved
|
|
107
|
+
// value-type — pass it when already computed to avoid the re-resolve.
|
|
108
|
+
const isNumArm = (v, node, vt = resolveValType(node, valTypeOf, lookupValType)) =>
|
|
109
|
+
isI32Num(v) || v.valKind === VAL.NUMBER || vt === VAL.NUMBER
|
|
110
|
+
|
|
111
|
+
// One arm of a two-arm f64 merge (?:, ??, ||, &&) whose result may be bit-tested while
|
|
112
|
+
// untyped. Canon (canonNum, a no-op unless the arm is NaN-minting arithmetic) ONLY a
|
|
113
|
+
// LONE numeric arm: when both arms are numeric the merge is value-typed NUMBER and read
|
|
114
|
+
// NaN-by-value (no canon); when the other arm is opaque the result is untyped, so a
|
|
115
|
+
// non-canonical NaN here would be misread by __is_truthy — fold it. A pointer arm
|
|
116
|
+
// (isNum=false) is never touched (canon would destroy its NaN-box).
|
|
117
|
+
const canonArm = (f, isNum, otherNum) => isNum && !otherNum ? canonNum(f) : f
|
|
118
|
+
|
|
119
|
+
// Host globals auto-imported as `(import "env" "name" (global … i64))` when
|
|
120
|
+
// referenced as a value. Drained from ctx.core.hostGlobals at assembly.
|
|
121
|
+
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
122
|
+
|
|
123
|
+
// An operand whose uint32 value can be *observed as a JS number* — a `>>>`
|
|
124
|
+
// result, an `unsignedResult` call, or an unsigned i32.const. Its magnitude can
|
|
125
|
+
// exceed signed-i32 range, so wrapping i32 arithmetic would corrupt it; widen to
|
|
126
|
+
// f64 instead. A `.wrapSafe` operand is also unsigned but is a `narrowUint32`
|
|
127
|
+
// accumulator read proven to be re-truncated by a `>>>` (ToUint32) sink at every
|
|
128
|
+
// use — wrapping is exactly its intended semantics, so it stays on the i32 path.
|
|
129
|
+
const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
|
|
130
|
+
|
|
131
|
+
// Strip a redundant NaN-canon wrapper (math.js `canon`) from an operand that
|
|
132
|
+
// feeds a NaN-propagating f64 op. `f64.sqrt`/`min`/`max` mint a sign-nondeterministic
|
|
133
|
+
// NaN that math.js canon-izes so it can't be bit-confused with a NaN-boxed pointer in
|
|
134
|
+
// `===`/`typeof`. But when the result flows straight into `f64.add`/`sub`/`mul`/`div`,
|
|
135
|
+
// the consumer propagates that NaN identically and is itself canon-ized if IT escapes —
|
|
136
|
+
// so the inner per-op canon (local.set + select + f64.ne, ~3 ops) is dead on the
|
|
137
|
+
// critical path. This is THE gap that put sqrt-heavy kernels ~23% behind V8
|
|
138
|
+
// (julia/raymarcher/boids); stripping it makes them match native JS.
|
|
139
|
+
const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
|
|
140
|
+
|
|
141
|
+
const FIRST_CLASS_UNARY_MATH = {
|
|
142
|
+
'math.abs': 'f64.abs',
|
|
143
|
+
'math.sqrt': 'f64.sqrt',
|
|
144
|
+
'math.ceil': 'f64.ceil',
|
|
145
|
+
'math.floor': 'f64.floor',
|
|
146
|
+
'math.trunc': 'f64.trunc',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function builtinFunctionValue(name) {
|
|
150
|
+
const op = FIRST_CLASS_UNARY_MATH[name]
|
|
151
|
+
if (!op) err(`Builtin function '${name}' cannot be used as a first-class value`)
|
|
152
|
+
if (!ctx.closure.table) err(`Builtin function '${name}' used as value requires closure support`)
|
|
153
|
+
const fn = `${T}builtin_${name.replace(/\W/g, '_')}`
|
|
154
|
+
if (!ctx.core.stdlib[fn]) {
|
|
155
|
+
const width = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
156
|
+
const params = ['(param $__env f64)', '(param $__argc i32)']
|
|
157
|
+
for (let i = 0; i < width; i++) params.push(`(param $__a${i} f64)`)
|
|
158
|
+
ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) (${op} (local.get $__a0)))`
|
|
159
|
+
inc(fn)
|
|
160
|
+
}
|
|
161
|
+
let idx = ctx.closure.table.indexOf(fn)
|
|
162
|
+
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(fn) }
|
|
163
|
+
const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
164
|
+
ir.closureFuncIdx = idx
|
|
165
|
+
return ir
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Emit unary negation: constant-fold, or i32 sub from 0 / f64.neg. */
|
|
169
|
+
const emitNeg = (a) => {
|
|
170
|
+
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
171
|
+
const v = emit(a)
|
|
172
|
+
if (isLit(v)) return emitNum(-litVal(v))
|
|
173
|
+
if (isI32Num(v)) return typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32')
|
|
174
|
+
// f64.neg flips the sign bit, so negating a NaN yields 0xFFF8.. — a non-canonical
|
|
175
|
+
// number-NaN that overlaps the NaN-boxed value space (jz reserves 0x7FF8.. as THE
|
|
176
|
+
// number-NaN). `__is_truthy`/`__eq` compare against that exact pattern, so a sign-
|
|
177
|
+
// flipped NaN reads as a tagged value (truthy / not-NaN). Fold any NaN result back
|
|
178
|
+
// to canonical — the same invariant math.sqrt/min/max keep via `canon` (module/math.js).
|
|
179
|
+
const t = temp('ng')
|
|
180
|
+
return typed(['block', ['result', 'f64'],
|
|
181
|
+
['local.set', `$${t}`, ['f64.neg', toNumF64(a, v)]],
|
|
182
|
+
['select', ['f64.const', 'nan'], ['local.get', `$${t}`],
|
|
183
|
+
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Try constant-folding binary arith: returns emitNum(result) or null. */
|
|
187
|
+
// `.unsigned` literals carry a uint32 value whose i32 `litVal` is its *signed* bit
|
|
188
|
+
// pattern (e.g. `-1` for 4294967295), so folding them through `fn` numerically would
|
|
189
|
+
// be wrong. Bail to the runtime path — the arithmetic handlers widen unsigned operands
|
|
190
|
+
// to f64 (convert_i32_u), reproducing the JS-spec result.
|
|
191
|
+
const foldConst = (va, vb, fn, guard) =>
|
|
192
|
+
isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned && (!guard || guard(litVal(vb)))
|
|
193
|
+
? emitNum(fn(litVal(va), litVal(vb))) : null
|
|
194
|
+
|
|
195
|
+
// JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
|
|
196
|
+
// Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
|
|
197
|
+
// exact product stays f64-exact, i.e. |product| <= 2^53. Two i32 operands can
|
|
198
|
+
// reach 2^62, so `i32.mul` is sound only when one side is bounded small enough
|
|
199
|
+
// that, against the full i32 range (2^31) of the other, the product holds within
|
|
200
|
+
// 2^53 — i.e. its magnitude <= 2^22. A literal qualifies directly; so does a
|
|
201
|
+
// masked operand (`x & 63`, `x >>> k`) whose value is provably bounded. Keeps
|
|
202
|
+
// index arithmetic (`i*4`) and bitwise-masked scales (bytebeat's `t*(m&63)`) on
|
|
203
|
+
// `i32.mul` while routing hash-mix-scale products to `f64.mul`.
|
|
204
|
+
const FITS_I32_MAX = 0x400000 // 2^22 — see derivation above
|
|
205
|
+
const mulFitsI32 = (va, vb) =>
|
|
206
|
+
(isLit(va) && Math.abs(litVal(va)) <= FITS_I32_MAX) ||
|
|
207
|
+
(isLit(vb) && Math.abs(litVal(vb)) <= FITS_I32_MAX) ||
|
|
208
|
+
(!isLit(va) && maskBound(va) <= FITS_I32_MAX) ||
|
|
209
|
+
(!isLit(vb) && maskBound(vb) <= FITS_I32_MAX)
|
|
210
|
+
|
|
211
|
+
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
212
|
+
export function emitTypeofCmp(a, b, cmpOp) {
|
|
213
|
+
let typeofExpr, code
|
|
214
|
+
if (Array.isArray(a) && a[0] === 'typeof' && typeof b === 'number') { typeofExpr = a[1]; code = b }
|
|
215
|
+
else if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null) { typeofExpr = a[1]; code = b[1] }
|
|
216
|
+
else return null
|
|
217
|
+
if (typeof code !== 'number') return null
|
|
218
|
+
|
|
219
|
+
const t = temp()
|
|
220
|
+
const va = asF64(emit(typeofExpr))
|
|
221
|
+
const eq = cmpOp === 'eq'
|
|
222
|
+
// Trailing eqz-wrapper for atomic checks: `check` if eq, `!check` if ne.
|
|
223
|
+
const wrap = check => typed(eq ? check : ['i32.eqz', check], 'i32')
|
|
224
|
+
// De-Morgan'd `(X && Y)` vs `(!X || !Y)` — kept explicit so WAT output is
|
|
225
|
+
// byte-identical to the previous inlined form (watopt may shape it differently).
|
|
226
|
+
const both = (X, Y) => typed(eq ? ['i32.and', X, Y] : ['i32.or', ['i32.eqz', X], ['i32.eqz', Y]], 'i32')
|
|
227
|
+
// "isPtr AND ptr_type == kind" — shared by typeof "string" / "function" /
|
|
228
|
+
// user-supplied positive PTR codes. The tee in isPtr caches v in `t` for reuse.
|
|
229
|
+
const isPtrKind = kind => {
|
|
230
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
231
|
+
const isKind = ptrTypeEq(['local.get', `$${t}`], kind)
|
|
232
|
+
return both(isPtr, isKind)
|
|
233
|
+
}
|
|
234
|
+
// Static fold for known-VAL operands of "boolean"/"bigint" — saves a runtime branch.
|
|
235
|
+
const staticFold = (target) => {
|
|
236
|
+
const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
|
|
237
|
+
if (vt) return typed(['i32.const', (vt === target) === eq ? 1 : 0], 'i32')
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (code === TYPEOF.number) {
|
|
242
|
+
// typeof "number": v===v rejects NaN-box pointers; BOOL carrier is 0/1 → still typeof "boolean".
|
|
243
|
+
if (resolveValType(typeofExpr, valTypeOf, lookupValType) === VAL.BOOL) return typed(['i32.const', eq ? 0 : 1], 'i32')
|
|
244
|
+
return typed([eq ? 'f64.eq' : 'f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
|
|
245
|
+
}
|
|
246
|
+
if (code === TYPEOF.string) return isPtrKind(PTR.STRING)
|
|
247
|
+
if (code === TYPEOF.undefined) return wrap(isNullish(va))
|
|
248
|
+
if (code === TYPEOF.boolean) return staticFold(VAL.BOOL) ?? wrap(isBoolAtom(['local.tee', `$${t}`, va]))
|
|
249
|
+
if (code === TYPEOF.object) {
|
|
250
|
+
// object: a NaN-box whose ptr_type is a heap kind — NOT STRING (typeof "string"),
|
|
251
|
+
// NOT CLOSURE (typeof "function"), and NOT ATOM. The ATOM tag covers null AND undef
|
|
252
|
+
// AND the boolean atoms true/false: excluding it in one ptr_type check is both the
|
|
253
|
+
// null/undef guard and the (previously missing) boolean guard — without it
|
|
254
|
+
// `typeof aBool === "object"` wrongly returned true whenever the operand's static
|
|
255
|
+
// type was unknown (e.g. a value off JSON.parse), since a bool atom is a NaN-box
|
|
256
|
+
// that isn't STRING/CLOSURE/nullish. Numbers (incl. NaN) and bigint aren't NaN-box
|
|
257
|
+
// pointers, so isPtr already rejects them.
|
|
258
|
+
inc('__ptr_type')
|
|
259
|
+
const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
|
|
260
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
261
|
+
const heapKind = ['i32.and',
|
|
262
|
+
['i32.and',
|
|
263
|
+
['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]], ['i32.const', PTR.STRING]],
|
|
264
|
+
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]],
|
|
265
|
+
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.ATOM]]]
|
|
266
|
+
return wrap(['i32.and', isPtr, heapKind])
|
|
267
|
+
}
|
|
268
|
+
if (code === TYPEOF.function) return isPtrKind(PTR.CLOSURE)
|
|
269
|
+
if (code === TYPEOF.bigint) {
|
|
270
|
+
const fold = staticFold(VAL.BIGINT); if (fold) return fold
|
|
271
|
+
// bigint heuristic: finite, nonzero, sub-normal abs (boxed BigInt carrier).
|
|
272
|
+
const n = ['local.tee', `$${t}`, va]
|
|
273
|
+
return wrap(['i32.and',
|
|
274
|
+
['f64.eq', n, ['local.get', `$${t}`]],
|
|
275
|
+
['i32.and',
|
|
276
|
+
['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]],
|
|
277
|
+
['f64.lt', ['f64.abs', ['local.get', `$${t}`]], ['f64.const', 2.2250738585072014e-308]]]])
|
|
278
|
+
}
|
|
279
|
+
if (code >= 0) return isPtrKind(code)
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Stringify a VAL.BOOL operand to "true"/"false" (f64 string pointer). The
|
|
284
|
+
* boolean rides the cheap 0/1 carrier, so we runtime-select between the two
|
|
285
|
+
* interned literals; a constant operand folds to a single literal downstream. */
|
|
286
|
+
export const emitBoolStr = (node) =>
|
|
287
|
+
typed(['select', asF64(emit(['str', 'true'])), asF64(emit(['str', 'false'])), truthyIR(emit(node))], 'f64')
|
|
288
|
+
|
|
289
|
+
const CMP_SET = new Set(['>', '<', '>=', '<=', '==', '!=', '!'])
|
|
290
|
+
const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
|
|
291
|
+
|
|
292
|
+
// Map/Set methods whose generic (`.${method}`) emitter assumes a collection
|
|
293
|
+
// receiver and dereferences a key/value argument. Every one needs ≥1 argument
|
|
294
|
+
// (`.get(k)` / `.has(v)` / `.add(v)` / `.delete(v)` / `.set(k[,v])`), so a
|
|
295
|
+
// zero-arg call on a not-proven-collection receiver cannot be the collection
|
|
296
|
+
// op — it is a user/closure method and must not reach the collection emitter.
|
|
297
|
+
const COLLECTION_METHODS = new Set(['get', 'set', 'has', 'add', 'delete'])
|
|
298
|
+
|
|
299
|
+
// String char-index methods bound generically (no `.string:` qualifier — no array
|
|
300
|
+
// name collision). `String.prototype.{charCodeAt,charAt}` each take at most one
|
|
301
|
+
// argument (the index), so a call supplying ≥2 args on a not-proven-string receiver
|
|
302
|
+
// cannot be the string built-in — it is a user method that happens to share the
|
|
303
|
+
// name (e.g. the self-host abi's `ctx.abi.string.ops.charCodeAt(sF64,iI32,ctx,oobNan)`).
|
|
304
|
+
// It must fall through to dynamic dispatch, mirroring COLLECTION_METHODS' arity guard.
|
|
305
|
+
const STR_INDEX_METHODS = new Set(['charCodeAt', 'charAt'])
|
|
306
|
+
|
|
307
|
+
// Pointer kinds for which JS `==` / `!=` is pure reference equality — i.e. i64 bit
|
|
308
|
+
// compare of the NaN-box is equivalent to __eq. Excludes STRING (content compare for
|
|
309
|
+
// heap strings) and BIGINT (content compare).
|
|
310
|
+
const REF_EQ_KINDS = new Set([
|
|
311
|
+
VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
|
|
312
|
+
VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX, VAL.DATE,
|
|
313
|
+
])
|
|
314
|
+
|
|
315
|
+
function stringLiteral(node) {
|
|
316
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
317
|
+
if (Array.isArray(node) && node[0] == null && typeof node[1] === 'string') return node[1]
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Index expressions where peepholing `s[k] === 'X'` to char-byte compare is
|
|
322
|
+
// semantics-preserving: must produce a non-negative *integer* at run time so
|
|
323
|
+
// `__str_byteLen u> k` bounds-checks the same range JS would. Out-of-range
|
|
324
|
+
// (negative or ≥ len) falls into the `else 0` arm — matches `undefined === 'X'`.
|
|
325
|
+
function intIndexIR(key) {
|
|
326
|
+
const lit = nonNegIntLiteral(key)
|
|
327
|
+
if (lit != null) return ['i32.const', lit]
|
|
328
|
+
// intCertain name: forward-prop says every defining RHS is integer-shaped.
|
|
329
|
+
// Captures loop variables (`for(let i=0;;i++)`), `let k = j + 1`, etc.
|
|
330
|
+
if (typeof key === 'string' && repOf(key)?.intCertain) return asI32(emit(key))
|
|
331
|
+
// intCertain schema slot read `o.x`: every observed write is integer-shaped,
|
|
332
|
+
// so the loaded f64 represents an int — fold into the byte-compare fast path.
|
|
333
|
+
if (Array.isArray(key) && key[0] === '.' && typeof key[1] === 'string' && typeof key[2] === 'string' &&
|
|
334
|
+
ctx.schema.slotIntCertainAt?.(key[1], key[2]) === true) return asI32(emit(key))
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Emit an array-index expression in i32 arithmetic. A subscript is truncated to
|
|
340
|
+
* i32 at the memory boundary regardless, so `+`/`-`/`*` over i32-typed leaves are
|
|
341
|
+
* computed with wrapping i32 ops instead of the f64 round-trip
|
|
342
|
+
* (`convert_i32 … f64.mul/add … trunc_sat_f64_s`) that `*` of two non-literal
|
|
343
|
+
* i32s would otherwise force (see analyze.js exprType `*`).
|
|
344
|
+
*
|
|
345
|
+
* Correctness: i32 +/-/* preserve the residue mod 2^32, so the result equals the
|
|
346
|
+
* expression's true integer value mod 2^32 — even if an intermediate product
|
|
347
|
+
* overflows. Any valid index is in [0, 2^30) ⊂ [-2^31, 2^31), where two's
|
|
348
|
+
* complement reproduces the true value exactly; out-of-range indices are OOB
|
|
349
|
+
* (already UB — jz truncates the index to i32 at the boundary either way). Bails
|
|
350
|
+
* to the f64 path for any non-i32 leaf (an f64 leaf may be fractional, where
|
|
351
|
+
* trunc-then-add ≠ add-then-trunc) or non-{+,-,*} operator.
|
|
352
|
+
*/
|
|
353
|
+
const I32_INDEX_OP = { '+': 'i32.add', '-': 'i32.sub', '*': 'i32.mul' }
|
|
354
|
+
function tryI32Index(e) {
|
|
355
|
+
// Integer literal first — a prepare-wrapped literal `[null, k]` (and a const-int
|
|
356
|
+
// name) is itself an Array, so the operator dispatch below would reject it and
|
|
357
|
+
// bail the WHOLE index to the f64 round-trip. The classic victim is the `+ 1` /
|
|
358
|
+
// `(j + 1)` of a bilinear/stencil gather (`a[(j+1)*W + i + 1]`): one literal leaf
|
|
359
|
+
// forced `convert_i32 … f64.mul/add … trunc_sat_f64_s` across every term.
|
|
360
|
+
const lit = nonNegIntLiteral(e)
|
|
361
|
+
if (lit != null) return typed(['i32.const', lit], 'i32')
|
|
362
|
+
if (Array.isArray(e)) {
|
|
363
|
+
const inner = I32_INDEX_OP[e[0]]
|
|
364
|
+
if (inner && e[2] != null) {
|
|
365
|
+
const a = tryI32Index(e[1]); if (a == null) return null
|
|
366
|
+
const b = tryI32Index(e[2]); if (b == null) return null
|
|
367
|
+
return typed([inner, a, b], 'i32')
|
|
368
|
+
}
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
return exprType(e, ctx.func.locals) === 'i32' ? asI32(emit(e)) : null
|
|
372
|
+
}
|
|
373
|
+
export const emitIndex = (idx) => tryI32Index(idx) ?? asI32(emit(idx))
|
|
374
|
+
|
|
375
|
+
function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
376
|
+
const leftLit = stringLiteral(a)
|
|
377
|
+
const rightLit = stringLiteral(b)
|
|
378
|
+
const aIdx = Array.isArray(a) && a[0] === '[]'
|
|
379
|
+
const bIdx = Array.isArray(b) && b[0] === '[]'
|
|
380
|
+
let indexed, lit
|
|
381
|
+
if (bIdx && leftLit != null) { indexed = b; lit = leftLit }
|
|
382
|
+
else if (aIdx && rightLit != null) { indexed = a; lit = rightLit }
|
|
383
|
+
else return null
|
|
384
|
+
|
|
385
|
+
if (lit.length === 0) return null
|
|
386
|
+
if ([...lit].some(c => c.charCodeAt(0) > 0x7F)) return null
|
|
387
|
+
|
|
388
|
+
const [, obj, key] = indexed
|
|
389
|
+
const idxIR = intIndexIR(key)
|
|
390
|
+
if (idxIR == null) return null
|
|
391
|
+
|
|
392
|
+
const vt = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
|
|
393
|
+
if (vt && vt !== VAL.STRING) return null
|
|
394
|
+
|
|
395
|
+
const finish = expr => negate ? ['i32.eqz', expr] : expr
|
|
396
|
+
|
|
397
|
+
// Known STRING: s[i] always returns 1-char SSO. Multi-char literal → always false.
|
|
398
|
+
if (vt === VAL.STRING && lit.length > 1) return emitNum(negate ? 1 : 0)
|
|
399
|
+
|
|
400
|
+
// Single-char literal: compare byte directly, skipping __str_idx allocation.
|
|
401
|
+
if (lit.length !== 1 || !ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
|
|
402
|
+
|
|
403
|
+
// Stash the index in a local when it isn't a constant — bounds + load both reference it.
|
|
404
|
+
const isConstIdx = Array.isArray(idxIR) && idxIR[0] === 'i32.const'
|
|
405
|
+
let idxRefIR = idxIR, idxBindIR = null
|
|
406
|
+
if (!isConstIdx) {
|
|
407
|
+
const idxTmp = tempI32('si')
|
|
408
|
+
idxBindIR = ['local.set', `$${idxTmp}`, idxIR]
|
|
409
|
+
idxRefIR = ['local.get', `$${idxTmp}`]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const ptr = temp('sc')
|
|
413
|
+
inc('__str_byteLen', '__char_at')
|
|
414
|
+
const charEq = ['if', ['result', 'i32'],
|
|
415
|
+
['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], idxRefIR],
|
|
416
|
+
['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR], ['i32.const', lit.charCodeAt(0)]]],
|
|
417
|
+
['else', ['i32.const', 0]]]
|
|
418
|
+
|
|
419
|
+
const prelude = idxBindIR ? [['local.set', `$${ptr}`, asF64(emit(obj))], idxBindIR] : [['local.set', `$${ptr}`, asF64(emit(obj))]]
|
|
420
|
+
|
|
421
|
+
if (vt === VAL.STRING) {
|
|
422
|
+
return typed(['block', ['result', 'i32'], ...prelude, finish(charEq)], 'i32')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
inc('__ptr_type', '__typed_idx', '__eq')
|
|
426
|
+
const genericEq = ['call', '$__eq',
|
|
427
|
+
['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR]],
|
|
428
|
+
asI64(emit(['str', lit]))]
|
|
429
|
+
const cmp = ['if', ['result', 'i32'],
|
|
430
|
+
ptrTypeEq(['local.get', `$${ptr}`], PTR.STRING),
|
|
431
|
+
['then', charEq],
|
|
432
|
+
['else', genericEq]]
|
|
433
|
+
return typed(['block', ['result', 'i32'], ...prelude, finish(cmp)], 'i32')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// `<str>.{substr,substring,slice}(...) === <other>` whose substring is consumed
|
|
437
|
+
// only by the equality: materialising it (an __alloc + byte copy) is pure waste.
|
|
438
|
+
// Fuse to __str_{substring,slice}_eq, which clamp the range like the method then
|
|
439
|
+
// byte-compare it against `other` in place. Sibling to emitSingleCharIndexCmp,
|
|
440
|
+
// tried at the same `==`/`!=` sites. Motivating hot path: the parser keyword
|
|
441
|
+
// scan, `cur.substr(i,l) === keyword`.
|
|
442
|
+
function emitSubstringEqCmp(a, b, negate = false) {
|
|
443
|
+
// Post-prepare a multi-arg call keeps its args as one comma list; a single
|
|
444
|
+
// arg sits bare. Normalise either (and a flat tail, defensively) to a list.
|
|
445
|
+
const callInfo = node => {
|
|
446
|
+
if (!Array.isArray(node) || node[0] !== '()') return null
|
|
447
|
+
const callee = node[1]
|
|
448
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return null
|
|
449
|
+
const method = callee[2]
|
|
450
|
+
if (method !== 'substr' && method !== 'substring' && method !== 'slice') return null
|
|
451
|
+
let args = node.slice(2)
|
|
452
|
+
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') args = args[0].slice(1)
|
|
453
|
+
while (args.length && args[args.length - 1] == null) args = args.slice(0, -1)
|
|
454
|
+
return { recv: callee[1], method, args }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let info = callInfo(a), other = b, callIsLeft = true
|
|
458
|
+
if (!info) { info = callInfo(b); other = a; callIsLeft = false }
|
|
459
|
+
if (!info) return null
|
|
460
|
+
const { recv, method, args } = info
|
|
461
|
+
if (args.length > 2) return null
|
|
462
|
+
if (!ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
|
|
463
|
+
|
|
464
|
+
// The receiver must be a string. `substr`/`substring` name string-only methods,
|
|
465
|
+
// so an unknown receiver is safe — the normal `.substr`/`.substring` emitter
|
|
466
|
+
// assumes a string too. `slice` is also Array.prototype.slice — require a
|
|
467
|
+
// statically-known STRING there. A known non-string receiver bails always.
|
|
468
|
+
const vt = resolveValType(recv, valTypeOf, lookupValType)
|
|
469
|
+
if (vt && vt !== VAL.STRING) return null
|
|
470
|
+
if (method === 'slice' && vt !== VAL.STRING) return null
|
|
471
|
+
|
|
472
|
+
const helper = method === 'slice' ? '__str_slice_eq' : '__str_substring_eq'
|
|
473
|
+
inc(helper)
|
|
474
|
+
|
|
475
|
+
// Absent end → byteLen: pass i32 max — every clamp arm floors it to the length.
|
|
476
|
+
const TO_END = ['i32.const', 0x7FFFFFFF]
|
|
477
|
+
let startIR, endIR
|
|
478
|
+
if (method === 'substr' && args[1] != null) {
|
|
479
|
+
// substr's 2nd arg is a length: end = start + length, so start reads twice.
|
|
480
|
+
const s = tempI32('subS')
|
|
481
|
+
startIR = ['local.tee', `$${s}`, args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))]
|
|
482
|
+
endIR = ['i32.add', ['local.get', `$${s}`], asI32(emit(args[1]))]
|
|
483
|
+
} else {
|
|
484
|
+
startIR = args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))
|
|
485
|
+
endIR = args[1] == null ? TO_END : asI32(emit(args[1]))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const finish = expr => negate ? ['i32.eqz', expr] : expr
|
|
489
|
+
|
|
490
|
+
if (callIsLeft)
|
|
491
|
+
return typed(finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR, asI64(emit(other))]), 'i32')
|
|
492
|
+
|
|
493
|
+
// `other` is the source-left operand — evaluate it first to preserve order.
|
|
494
|
+
const o = temp('subO')
|
|
495
|
+
return typed(['block', ['result', 'i32'],
|
|
496
|
+
['local.set', `$${o}`, asF64(emit(other))],
|
|
497
|
+
finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR,
|
|
498
|
+
['i64.reinterpret_f64', ['local.get', `$${o}`]]])], 'i32')
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// One half of a two-sided range test against a compile-time constant, normalized to
|
|
502
|
+
// an inclusive bound on a *local* `x`: `{ x, lo }` (x ≥ lo) or `{ x, hi }` (x ≤ hi).
|
|
503
|
+
// `>`/`<` fold to the inclusive neighbor; a const on either side is accepted. Returns
|
|
504
|
+
// null for anything else (so the caller leaves the expression untouched).
|
|
505
|
+
function rangeBound(n) {
|
|
506
|
+
if (!Array.isArray(n) || n.length !== 3) return null
|
|
507
|
+
const lc = intLiteralValue(n[1]), rc = intLiteralValue(n[2])
|
|
508
|
+
if (rc != null && typeof n[1] === 'string') { // x op CONST
|
|
509
|
+
if (n[0] === '>=') return { x: n[1], lo: rc }
|
|
510
|
+
if (n[0] === '>') return { x: n[1], lo: rc + 1 }
|
|
511
|
+
if (n[0] === '<=') return { x: n[1], hi: rc }
|
|
512
|
+
if (n[0] === '<') return { x: n[1], hi: rc - 1 }
|
|
513
|
+
}
|
|
514
|
+
if (lc != null && typeof n[2] === 'string') { // CONST op x
|
|
515
|
+
if (n[0] === '<=') return { x: n[2], lo: lc }
|
|
516
|
+
if (n[0] === '<') return { x: n[2], lo: lc + 1 }
|
|
517
|
+
if (n[0] === '>=') return { x: n[2], hi: lc }
|
|
518
|
+
if (n[0] === '>') return { x: n[2], hi: lc - 1 }
|
|
519
|
+
}
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI constants) → `(x - LO) <=u (HI - LO)`.
|
|
524
|
+
// One subtract + one unsigned compare replaces two signed compares, an AND, and the
|
|
525
|
+
// short-circuit branch — the classic range-check trick (valid for any integers via
|
|
526
|
+
// wrapping subtraction). Returns the fused IR, or null to leave `&&` lowering unchanged.
|
|
527
|
+
function fuseRangeCheck(a, b) {
|
|
528
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
529
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
530
|
+
const lo = ba.lo ?? bb.lo, hi = ba.hi ?? bb.hi
|
|
531
|
+
if (lo > hi) return null
|
|
532
|
+
const xv = emit(ba.x)
|
|
533
|
+
if (xv.type !== 'i32') return null // f64 (fractional) would mis-fuse
|
|
534
|
+
return typed(['i32.le_u', ['i32.sub', xv, ['i32.const', lo]], ['i32.const', hi - lo]], 'i32')
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// The complement: `x < LO || x > HI` (the two outside half-checks — one upper-bounded,
|
|
538
|
+
// one lower-bounded, with a gap between) → `(x - LO) >u (HI - LO)`, where [LO, HI] is the
|
|
539
|
+
// inside range. Same trick, negated; returns null to leave `||` lowering unchanged.
|
|
540
|
+
function fuseRangeCheckOr(a, b) {
|
|
541
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
542
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
543
|
+
const insideLo = (ba.hi ?? bb.hi) + 1, insideHi = (ba.lo ?? bb.lo) - 1
|
|
544
|
+
if (insideLo > insideHi) return null
|
|
545
|
+
const xv = emit(ba.x)
|
|
546
|
+
if (xv.type !== 'i32') return null
|
|
547
|
+
return typed(['i32.gt_u', ['i32.sub', xv, ['i32.const', insideLo]], ['i32.const', insideHi - insideLo]], 'i32')
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Flow-sensitive type refinement moved to ./flow-types.js (extractRefinements,
|
|
551
|
+
// predicateRefinement, mergeRefinement, withRefinements). emit.js imports them
|
|
552
|
+
// from there — see the import block at the top of this file.
|
|
553
|
+
|
|
554
|
+
function unrollSmallConstFor(init, cond, step, body) {
|
|
555
|
+
const end = smallConstForTripCount(init, cond, step)
|
|
556
|
+
if (end == null) return null
|
|
557
|
+
const name = init[1][1]
|
|
558
|
+
if (containsNestedLoop(body)) {
|
|
559
|
+
const nestedMode = ctx.transform.optimize?.nestedSmallConstForUnroll
|
|
560
|
+
if (nestedMode !== true && (nestedMode !== 'auto' || !containsKnownTypedArrayIndex(body))) return null
|
|
561
|
+
if (end * nestedSmallLoopBudget(body) > MAX_NESTED_FOR_UNROLL) return null
|
|
562
|
+
}
|
|
563
|
+
if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsDeclOf(body, name)) return null
|
|
564
|
+
if (isReassigned(body, name)) return null
|
|
565
|
+
|
|
566
|
+
const out = []
|
|
567
|
+
for (let i = 0; i < end; i++) out.push(...emitVoid(cloneWithSubst(body, name, i)))
|
|
568
|
+
return out
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Max distinct keys a for-in unrolls over (bounds code size; larger key sets keep
|
|
572
|
+
// the pooled-keys loop, which is already allocation-free via __keys_ro).
|
|
573
|
+
const FORIN_UNROLL_MAX = 16
|
|
574
|
+
// Total-expansion ceiling: unroll emits one body copy per key, so the size cost is
|
|
575
|
+
// keys × body, not keys alone. A large body over many keys (e.g. watr's 15-key
|
|
576
|
+
// schema loop) blows up code size for no deopt win — the pooled fallback is already
|
|
577
|
+
// allocation-free. Cap keys × nodeSize(body); past it, keep the loop. (Tuned above
|
|
578
|
+
// every unroll the corpus actually wants — the 16-key cap test lands at 80.)
|
|
579
|
+
const FORIN_UNROLL_BUDGET = 128
|
|
580
|
+
const forInBodyCost = (node) => {
|
|
581
|
+
if (!Array.isArray(node)) return 1
|
|
582
|
+
let n = 1
|
|
583
|
+
for (let i = 1; i < node.length; i++) n += forInBodyCost(node[i])
|
|
584
|
+
return n
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Pull the for-in source out of prepare's keys expression: either a bare
|
|
588
|
+
// `__keys_ro(src)` call or the nullish-guarded `cond ? [] : __keys_ro(src)`.
|
|
589
|
+
function keysRoSrc(node) {
|
|
590
|
+
if (!Array.isArray(node)) return null
|
|
591
|
+
if (node[0] === '()' && node[1] === '__keys_ro') return node[2]
|
|
592
|
+
if (node[0] === '?:' || node[0] === '?') {
|
|
593
|
+
const last = node[node.length - 1]
|
|
594
|
+
if (Array.isArray(last) && last[0] === '()' && last[1] === '__keys_ro') return last[2]
|
|
595
|
+
}
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Unroll `for (k in o)` over a static schema. Prepare lowers for-in to a plain
|
|
600
|
+
// for-loop whose key array comes from the for-in-exclusive `__keys_ro` intrinsic,
|
|
601
|
+
// so a loop carrying it IS a for-in. When `o` is a bare OBJECT var with a complete
|
|
602
|
+
// static schema (no computed-key writes — same gate as __keys_ro pooling), replace
|
|
603
|
+
// the loop with one substituted copy of the body per key: the loop variable becomes
|
|
604
|
+
// a string literal, so `o[k]` folds to a static schema slot — no keys array, no
|
|
605
|
+
// per-element dynamic get. Falls back (returns null) to the pooled loop otherwise.
|
|
606
|
+
function unrollForIn(init, cond, step, body) {
|
|
607
|
+
if (!Array.isArray(init) || init[0] !== 'let' || !Array.isArray(init[1]) || init[1][0] !== '=') return null
|
|
608
|
+
const ksVar = init[1][1]
|
|
609
|
+
const src = keysRoSrc(init[1][2])
|
|
610
|
+
if (typeof src !== 'string') return null
|
|
611
|
+
if (!Array.isArray(cond) || cond[0] !== '<') return null
|
|
612
|
+
const ixVar = cond[1]
|
|
613
|
+
if (!Array.isArray(step) || step[0] !== '++' || step[1] !== ixVar) return null
|
|
614
|
+
// body = [';', ['let', ['=', target, ['[]', ksVar, ixVar]]], ...realBody]
|
|
615
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
616
|
+
const bind = body[1]
|
|
617
|
+
if (!Array.isArray(bind) || bind[0] !== 'let' || !Array.isArray(bind[1]) || bind[1][0] !== '=') return null
|
|
618
|
+
const target = bind[1][1]
|
|
619
|
+
const acc = bind[1][2]
|
|
620
|
+
if (!Array.isArray(acc) || acc[0] !== '[]' || acc[1] !== ksVar || acc[2] !== ixVar) return null
|
|
621
|
+
|
|
622
|
+
// Unroll only with PROOF the schema is complete: a computed-key write adds
|
|
623
|
+
// enumerable keys, so bail if `src` takes one — or if the fact is unavailable
|
|
624
|
+
// (no proof ⇒ no unroll; unrolling drops the dynamic path, so erring safe matters).
|
|
625
|
+
if (!ctx.types.dynWriteVars || ctx.types.dynWriteVars.has(src)) return null
|
|
626
|
+
if (lookupValType(src) !== VAL.OBJECT) return null
|
|
627
|
+
const keys = ctx.schema.resolve(src)
|
|
628
|
+
if (!keys || !keys.length || keys.length > FORIN_UNROLL_MAX) return null
|
|
629
|
+
|
|
630
|
+
const rest = body.slice(2)
|
|
631
|
+
const realBody = rest.length === 1 ? rest[0] : [';', ...rest]
|
|
632
|
+
// Keep the pooled loop when unrolling would multiply a heavy body across many keys.
|
|
633
|
+
if (keys.length * forInBodyCost(realBody) > FORIN_UNROLL_BUDGET) return null
|
|
634
|
+
// Substitution safety, mirroring unrollSmallConstFor: no reassignment/redeclare
|
|
635
|
+
// of the loop var, no nested closure capturing it (cloneWithSubst skips `=>`),
|
|
636
|
+
// and no break/continue targeting this loop.
|
|
637
|
+
if (hasOwnBreakOrContinue(realBody) || containsNestedClosure(realBody) || containsDeclOf(realBody, target)) return null
|
|
638
|
+
if (isReassigned(realBody, target)) return null
|
|
639
|
+
|
|
640
|
+
const out = []
|
|
641
|
+
for (const key of keys) out.push(...emitVoid(cloneWithSubst(realBody, new Map([[target, ['str', key]]]))))
|
|
642
|
+
return out.length ? out : ['nop']
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function canThrow(body, seen = new Set()) {
|
|
646
|
+
if (!Array.isArray(body)) return false
|
|
647
|
+
const op = body[0]
|
|
648
|
+
if (op === 'throw') return true
|
|
649
|
+
if (op === '=>') return false
|
|
650
|
+
if (op === '()') {
|
|
651
|
+
const callee = body[1]
|
|
652
|
+
// A call can throw unless we can see the whole callee and prove it can't:
|
|
653
|
+
// only direct calls into a resolvable, non-raw function body are traceable.
|
|
654
|
+
// Indirect/method/builtin calls (callee not a plain name, or a name we can't
|
|
655
|
+
// resolve) are conservatively throwing — a user `try` must wrap them.
|
|
656
|
+
if (typeof callee !== 'string') return true
|
|
657
|
+
const bodyName = ctx.func.directClosures?.get(callee)
|
|
658
|
+
const f = ctx.func.map?.get(bodyName || callee)
|
|
659
|
+
if (!f?.body || f.raw) return true
|
|
660
|
+
if (!seen.has(f.name)) {
|
|
661
|
+
seen.add(f.name)
|
|
662
|
+
if (canThrow(f.body, seen)) return true
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (let i = 1; i < body.length; i++) if (canThrow(body[i], seen)) return true
|
|
666
|
+
return false
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Loop-bound hoisting (see the 'for' emitter): comparison ops whose invariant side
|
|
670
|
+
// is worth lifting, and the test for an immutable, loop-stable `arr.length`. A typed
|
|
671
|
+
// array's length is fixed, so it is loop-invariant whenever `arr` is not reassigned.
|
|
672
|
+
// A plain array's length CAN change (push/pop/index-grow/length=), so it is hoistable
|
|
673
|
+
// only when the loop body provably never mutates it — `mutatesArrayLength` decides that.
|
|
674
|
+
const HOIST_CMP = new Set(['<', '<=', '>', '>='])
|
|
675
|
+
const immutableLenBound = (node, body) => {
|
|
676
|
+
// Unwrap the `| 0` i32 coercion jz wraps a loop bound in (`i < arr.length`
|
|
677
|
+
// emits `i < (arr.length | 0)`).
|
|
678
|
+
if (Array.isArray(node) && node[0] === '|' && Array.isArray(node[2]) && node[2][0] == null && node[2][1] === 0)
|
|
679
|
+
node = node[1]
|
|
680
|
+
if (!(Array.isArray(node) && node[0] === '.' && node[2] === 'length' && typeof node[1] === 'string')) return false
|
|
681
|
+
const vt = lookupValType(node[1])
|
|
682
|
+
if (vt === VAL.TYPED) return !isReassigned(body, node[1])
|
|
683
|
+
if (vt === VAL.ARRAY) return !mutatesArrayLength(body, node[1])
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Pull `const x = <array/object literal>` decls out of a loop body when the literal is
|
|
688
|
+
// deeply constant and `x` is provably read-only + non-escaping in the loop (so a single
|
|
689
|
+
// shared allocation is sound) — otherwise the constant table is re-allocated every
|
|
690
|
+
// iteration. Returns { hoisted: [decl…], body: strippedBody } or null. Only top-level
|
|
691
|
+
// statements of the loop body are considered.
|
|
692
|
+
const extractHoistableLiterals = (body) => {
|
|
693
|
+
let stmts, rebuild
|
|
694
|
+
if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';') {
|
|
695
|
+
stmts = body[1].slice(1); rebuild = kept => ['{}', [';', ...kept]]
|
|
696
|
+
} else if (Array.isArray(body) && body[0] === ';') {
|
|
697
|
+
stmts = body.slice(1); rebuild = kept => kept.length === 1 ? kept[0] : [';', ...kept]
|
|
698
|
+
} else return null
|
|
699
|
+
const hoisted = [], kept = []
|
|
700
|
+
for (const s of stmts) {
|
|
701
|
+
const lit = Array.isArray(s) && (s[0] === 'const' || s[0] === 'let') && s.length === 2
|
|
702
|
+
&& Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' ? s[1][2] : null
|
|
703
|
+
if (lit && Array.isArray(lit) && lit[0] === '[' && isConstLiteral(lit) && constLiteralHoistable(body, s[1][1]))
|
|
704
|
+
hoisted.push(s)
|
|
705
|
+
else kept.push(s)
|
|
706
|
+
}
|
|
707
|
+
return hoisted.length ? { hoisted, body: rebuild(kept) } : null
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// A source-defined function (carries a body) — as opposed to an imported name,
|
|
711
|
+
// which `ctx.func.names` also holds but which has no body and may legitimately
|
|
712
|
+
// share a name with a built-in emitter (e.g. an imported `parseInt`).
|
|
713
|
+
const isUserFunc = name => !!ctx.func.map.get(name)?.body
|
|
714
|
+
|
|
715
|
+
/** Emit pending `finally` cleanups for an abrupt control-flow exit.
|
|
716
|
+
* Inner cleanups run before outer cleanups. While emitting each cleanup, remove
|
|
717
|
+
* it from the active stack so `return` inside `finally` does not re-enter it. */
|
|
718
|
+
function emitFinalizers() {
|
|
719
|
+
const stack = ctx.func.finallyStack || []
|
|
720
|
+
if (stack.length === 0) return []
|
|
721
|
+
const saved = stack.slice()
|
|
722
|
+
const out = []
|
|
723
|
+
for (let i = saved.length - 1; i >= 0; i--) {
|
|
724
|
+
ctx.func.finallyStack = saved.slice(0, i)
|
|
725
|
+
out.push(...emitVoid(saved[i]))
|
|
726
|
+
}
|
|
727
|
+
ctx.func.finallyStack = saved
|
|
728
|
+
return out
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function withFinallyStack(stack, fn) {
|
|
732
|
+
const prev = ctx.func.finallyStack || []
|
|
733
|
+
ctx.func.finallyStack = stack
|
|
734
|
+
try { return fn() }
|
|
735
|
+
finally { ctx.func.finallyStack = prev }
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// withRefinements moved to ./flow-types.js
|
|
739
|
+
|
|
740
|
+
/** Coerce an AST node to an i32 boolean, folding && / || at the boolean boundary. */
|
|
741
|
+
export function toBool(node) {
|
|
742
|
+
const op = Array.isArray(node) ? node[0] : null
|
|
743
|
+
if (CMP_SET.has(op)) return emit(node)
|
|
744
|
+
if (op === '&&') {
|
|
745
|
+
const la = toBool(node[1]), lb = toBool(node[2])
|
|
746
|
+
if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.and', la, lb], 'i32')
|
|
747
|
+
return typed(['if', ['result', 'i32'], la, ['then', lb], ['else', ['i32.const', 0]]], 'i32')
|
|
748
|
+
}
|
|
749
|
+
if (op === '||') {
|
|
750
|
+
const la = toBool(node[1]), lb = toBool(node[2])
|
|
751
|
+
if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.or', la, lb], 'i32')
|
|
752
|
+
return typed(['if', ['result', 'i32'], la, ['then', ['i32.const', 1]], ['else', lb]], 'i32')
|
|
753
|
+
}
|
|
754
|
+
return toBoolFromEmitted(emit(node))
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// `(a / b) | 0` (the JS integer-division idiom) → i32.div_s. jz otherwise lowers `/`
|
|
758
|
+
// to f64.div + ToInt32, paying two i32→f64 converts and the trunc; i32.div_s is
|
|
759
|
+
// direct and lets the wasm backend magic-multiply a constant divisor. Bit-exact for
|
|
760
|
+
// all i32 a,b: |a|<2³³≪2⁵³ so the f64 quotient never rounds across the truncation
|
|
761
|
+
// boundary — EXCEPT b=0 (`(a/0)|0` is ToInt32(±Inf)=0, but i32.div_s traps) and
|
|
762
|
+
// INT_MIN/-1 (ToInt32 wraps to INT_MIN, i32.div_s traps); both guarded. A constant
|
|
763
|
+
// divisor folds the guards away. `exprType==='i32'` excludes unsigned operands
|
|
764
|
+
// (those return 'f64'), where div_s would misread the sign. Returns IR or null.
|
|
765
|
+
const INT_MIN_I32 = -2147483648
|
|
766
|
+
function tryIntDivTrunc(aNode, bNode) {
|
|
767
|
+
const o = ctx.transform.optimize
|
|
768
|
+
if (!o || o.intDivLower === false) return null
|
|
769
|
+
const L = ctx.func.locals
|
|
770
|
+
if (exprType(aNode, L) !== 'i32' || exprType(bNode, L) !== 'i32') return null
|
|
771
|
+
const dv = intLiteralValue(bNode)
|
|
772
|
+
if (dv != null) { // constant divisor — no runtime guard
|
|
773
|
+
const va = asI32(emit(aNode))
|
|
774
|
+
if (dv === 0) return typed(['block', ['result', 'i32'], ['drop', va], ['i32.const', 0]], 'i32')
|
|
775
|
+
if (dv === -1) return typed(['i32.sub', ['i32.const', 0], va], 'i32') // -a, wraps at INT_MIN
|
|
776
|
+
return typed(['i32.div_s', va, ['i32.const', dv | 0]], 'i32')
|
|
777
|
+
}
|
|
778
|
+
// Runtime divisor needs a,b repeated across the guard; only intercept when both are
|
|
779
|
+
// simple re-emittable operands (var / literal) so re-emit is pure and side-effect-free.
|
|
780
|
+
const simple = (n) => typeof n === 'string' || intLiteralValue(n) != null
|
|
781
|
+
if (!simple(aNode) || !simple(bNode)) return null
|
|
782
|
+
const A = () => asI32(emit(aNode)), B = () => asI32(emit(bNode))
|
|
783
|
+
return typed(['if', ['result', 'i32'], ['i32.eqz', B()],
|
|
784
|
+
['then', ['i32.const', 0]],
|
|
785
|
+
['else', ['if', ['result', 'i32'],
|
|
786
|
+
['i32.and', ['i32.eq', A(), ['i32.const', INT_MIN_I32]], ['i32.eq', B(), ['i32.const', -1]]],
|
|
787
|
+
['then', A()],
|
|
788
|
+
['else', ['i32.div_s', A(), B()]]]]], 'i32')
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
|
|
792
|
+
* i32 offset), else falls back to numeric WASM type coercion. */
|
|
793
|
+
function coerceArg(ir, param) {
|
|
794
|
+
if (param?.ptrKind != null) return ptrOffsetIR(ir, param.ptrKind)
|
|
795
|
+
return asParamType(ir, param?.type)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Pad an emitted-args array up to a signature's arity with type-appropriate
|
|
799
|
+
* defaults (`i32.const 0` for i32 params, `undefExpr()` for f64). Mutates and
|
|
800
|
+
* returns `args` for chaining. */
|
|
801
|
+
function padArgs(args, params) {
|
|
802
|
+
while (args.length < params.length)
|
|
803
|
+
args.push(params[args.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
804
|
+
return args
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Emit a node list as call arguments for the given param list: per-param
|
|
808
|
+
* coercion then arity padding. Used at every direct-call site. */
|
|
809
|
+
function emitCallArgs(argNodes, params) {
|
|
810
|
+
return padArgs(argNodes.map((a, k) => coerceArg(emit(a), params[k])), params)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Stamp a `call` IR with the pointer-ABI / sign metadata its signature carries.
|
|
814
|
+
* Returns `callIR` for chaining. Centralizes the three-property copy every
|
|
815
|
+
* direct-call emission did inline. */
|
|
816
|
+
function attachSigMeta(callIR, sig) {
|
|
817
|
+
if (sig?.ptrKind != null) callIR.ptrKind = sig.ptrKind
|
|
818
|
+
if (sig?.ptrAux != null) callIR.ptrAux = sig.ptrAux
|
|
819
|
+
if (sig?.unsignedResult) callIR.unsigned = true
|
|
820
|
+
return callIR
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Materialize a multi-value function call as a heap array.
|
|
825
|
+
* Call → store each result in temp → copy to allocated array → return pointer.
|
|
826
|
+
*/
|
|
827
|
+
export function materializeMulti(callNode) {
|
|
828
|
+
const name = callNode[1]
|
|
829
|
+
const func = ctx.func.map.get(name)
|
|
830
|
+
const n = func.sig.results.length
|
|
831
|
+
const argList = commaList(callNode[2])
|
|
832
|
+
const emittedArgs = emitCallArgs(argList, func.sig.params)
|
|
833
|
+
const temps = Array.from({ length: n }, () => temp())
|
|
834
|
+
const out = allocPtr({ type: 1, len: n, tag: 'marr' })
|
|
835
|
+
const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
|
|
836
|
+
for (let k = n - 1; k >= 0; k--) ir.push(['local.set', `$${temps[k]}`])
|
|
837
|
+
for (let k = 0; k < n; k++)
|
|
838
|
+
ir.push(['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.const', k * 8]], ['local.get', `$${temps[k]}`]])
|
|
839
|
+
ir.push(out.ptr)
|
|
840
|
+
return block64(...ir)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Fresh per-iteration heap cells for boxed (closure-captured) locals declared
|
|
845
|
+
* in a loop body. ECMAScript establishes the per-iteration environment at the
|
|
846
|
+
* START of each iteration, so the cell must exist before ANY body statement —
|
|
847
|
+
* including a closure declared *before* the binding (mutual recursion, or a
|
|
848
|
+
* `function` decl jzify hoists above its captures). Allocating at the decl point
|
|
849
|
+
* instead would let an earlier closure capture the previous iteration's (stale)
|
|
850
|
+
* cell while the binding reads/writes the freshly-allocated one. `emitDecl` then
|
|
851
|
+
* stores the initializer into this cell rather than re-allocating (see
|
|
852
|
+
* `frame.loopFresh`). Returns the alloc IR to splice at loop-body entry.
|
|
853
|
+
*/
|
|
854
|
+
export function emitLoopFreshBoxed(body, frame) {
|
|
855
|
+
if (!ctx.func.boxed?.size) return []
|
|
856
|
+
const names = new Set()
|
|
857
|
+
;(function scan(node) {
|
|
858
|
+
if (!Array.isArray(node)) return
|
|
859
|
+
const op = node[0]
|
|
860
|
+
if (op === '=>' || op === 'for' || op === 'for-of' || op === 'for-in' || op === 'while' || op === 'do') return
|
|
861
|
+
if (op === 'let' || op === 'const') {
|
|
862
|
+
for (let i = 1; i < node.length; i++) {
|
|
863
|
+
const d = node[i]
|
|
864
|
+
const nm = Array.isArray(d) && d[0] === '=' ? d[1] : d
|
|
865
|
+
if (typeof nm === 'string' && ctx.func.boxed.has(nm)) names.add(nm)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
869
|
+
})(body)
|
|
870
|
+
if (!names.size) return []
|
|
871
|
+
frame.loopFresh = names
|
|
872
|
+
const inits = []
|
|
873
|
+
for (const name of names) {
|
|
874
|
+
const cell = ctx.func.boxed.get(name)
|
|
875
|
+
ctx.func.locals.set(cell, 'i32')
|
|
876
|
+
inits.push(
|
|
877
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
878
|
+
['f64.store', ['local.get', `$${cell}`], undefExpr()])
|
|
879
|
+
}
|
|
880
|
+
return inits
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Emit let/const initializations as typed local.set instructions. */
|
|
884
|
+
export function emitDecl(...inits) {
|
|
885
|
+
const result = []
|
|
886
|
+
// A `let`/`const` declared inside a loop creates a *fresh* binding each
|
|
887
|
+
// iteration (ECMAScript per-iteration environment). Boxed (closure-captured)
|
|
888
|
+
// locals therefore need a fresh heap cell per iteration — but the cell is
|
|
889
|
+
// allocated at loop-body entry by `emitLoopFreshBoxed` (so a closure declared
|
|
890
|
+
// before the binding captures the right cell), recorded in `frame.loopFresh`.
|
|
891
|
+
// Here we only re-allocate when the loop body did NOT pre-allocate it; a
|
|
892
|
+
// function-level declaration keeps its preboxed cell (forward/mutual-recursion
|
|
893
|
+
// capture relies on it pre-existing).
|
|
894
|
+
const inLoop = ctx.func.stack.some(f => f.loop)
|
|
895
|
+
const loopPrebox = (name) => ctx.func.stack.some(f => f.loopFresh?.has(name))
|
|
896
|
+
for (let ii = 0; ii < inits.length; ii++) {
|
|
897
|
+
const i = inits[ii]
|
|
898
|
+
if (typeof i === 'string') {
|
|
899
|
+
const undef = undefExpr()
|
|
900
|
+
if (ctx.func.boxed.has(i)) {
|
|
901
|
+
const cell = ctx.func.boxed.get(i)
|
|
902
|
+
ctx.func.locals.set(cell, 'i32')
|
|
903
|
+
if (inLoop ? !loopPrebox(i) : !ctx.func.preboxed?.has(i))
|
|
904
|
+
result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
|
|
905
|
+
result.push(['f64.store', ['local.get', `$${cell}`], undef])
|
|
906
|
+
continue
|
|
907
|
+
}
|
|
908
|
+
if (isGlobal(i)) {
|
|
909
|
+
if (!ctx.scope.globalTypes.has(i)) result.push(['global.set', `$${i}`, undef])
|
|
910
|
+
continue
|
|
911
|
+
}
|
|
912
|
+
// An i32-typed local (a narrowed integer index feeder) can't hold the f64
|
|
913
|
+
// NaN-box undef sentinel — and wasm zero-inits locals anyway, so a 0 init is
|
|
914
|
+
// equivalent for the assigned-before-read pattern that earns i32.
|
|
915
|
+
result.push(['local.set', `$${i}`, ctx.func.locals.get(i) === 'i32' ? ['i32.const', 0] : undef])
|
|
916
|
+
continue
|
|
917
|
+
}
|
|
918
|
+
if (!Array.isArray(i) || i[0] !== '=') continue
|
|
919
|
+
const [, name, init] = i
|
|
920
|
+
if (typeof name !== 'string' || init == null) continue
|
|
921
|
+
// Flag bindings initialized to a nullish literal so arithmetic on them coerces (null→0,
|
|
922
|
+
// undefined→NaN) rather than propagating the raw sentinel. See toNumF64 / maybeNullish.
|
|
923
|
+
if (isNullishLit(init)) ctx.func.maybeNullish?.add(name)
|
|
924
|
+
|
|
925
|
+
// SRoA flat object: `let o = {a:1, b:2}` — dissolve fields into `o#i`
|
|
926
|
+
// locals, no heap alloc. Each field local ← asF64(value). Reads/writes are
|
|
927
|
+
// rewritten by the `.`/`[]` flat hooks. See scanFlatObjects (analyze.js).
|
|
928
|
+
// Monotonic-extension fields (`o.newProp = …`) carry no literal value —
|
|
929
|
+
// they init to undefined so a read before the write matches JS.
|
|
930
|
+
const flatDecl = ctx.func.flatObjects?.get(name)
|
|
931
|
+
if (flatDecl && Array.isArray(init) && (init[0] === '{}' || init[0] === '[' || init[0] === '[]')) {
|
|
932
|
+
for (let j = 0; j < flatDecl.names.length; j++)
|
|
933
|
+
result.push(['local.set', `$${name}#${j}`,
|
|
934
|
+
flatDecl.values[j] === undefined ? undefExpr() : asF64(emit(flatDecl.values[j]))])
|
|
935
|
+
continue
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Multi-value ephemeral destructuring — skip heap alloc when temp is
|
|
939
|
+
// assigned from a multi-value call then immediately destructured element-by-element.
|
|
940
|
+
if (name.startsWith(T) && Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string'
|
|
941
|
+
&& ctx.func.names?.has(init[1])) {
|
|
942
|
+
const func = ctx.func.map.get(init[1])
|
|
943
|
+
const n = func?.sig.results.length
|
|
944
|
+
if (n > 1) {
|
|
945
|
+
const targets = []
|
|
946
|
+
let match = true
|
|
947
|
+
for (let k = 0; k < n && match; k++) {
|
|
948
|
+
const next = inits[ii + 1 + k]
|
|
949
|
+
if (!Array.isArray(next) || next[0] !== '=' || typeof next[1] !== 'string') { match = false; break }
|
|
950
|
+
const rhs = next[2]
|
|
951
|
+
if (!Array.isArray(rhs) || rhs[0] !== '[]' || rhs[1] !== name) { match = false; break }
|
|
952
|
+
const idx = rhs[2]
|
|
953
|
+
if (!Array.isArray(idx) || idx[0] != null || idx[1] !== k) { match = false; break }
|
|
954
|
+
if (ctx.func.boxed.has(next[1]) || isGlobal(next[1])) { match = false; break }
|
|
955
|
+
targets.push(next[1])
|
|
956
|
+
}
|
|
957
|
+
if (match && targets.length === n) {
|
|
958
|
+
const argList = commaList(init[2])
|
|
959
|
+
const emittedArgs = emitCallArgs(argList, func.sig.params)
|
|
960
|
+
result.push(['call', `$${init[1]}`, ...emittedArgs])
|
|
961
|
+
for (let k = n - 1; k >= 0; k--)
|
|
962
|
+
result.push(['local.set', `$${targets[k]}`])
|
|
963
|
+
ii += n
|
|
964
|
+
continue
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// No-copy slice view: `let t = s.slice(...)` whose result scanSliceViews
|
|
969
|
+
// proved never escapes — lower the initializer to a SLICE_BIT view instead
|
|
970
|
+
// of a copying slice. Everything downstream treats `t` as an ordinary
|
|
971
|
+
// string. Gated here (not in the analysis) on a statically-known STRING
|
|
972
|
+
// receiver — param types are settled only by emit time — and on plain-local
|
|
973
|
+
// carriers (boxed/global escape); any miss falls back to the copying slice.
|
|
974
|
+
let viewInit = null
|
|
975
|
+
if (ctx.func.sliceViews?.has(name) && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
976
|
+
&& Array.isArray(init) && init[0] === '()'
|
|
977
|
+
&& Array.isArray(init[1]) && init[1][0] === '.' && init[1][2] === 'slice') {
|
|
978
|
+
const recv = init[1][1]
|
|
979
|
+
const recvVt = valTypeOf(recv)
|
|
980
|
+
if (recvVt === VAL.STRING) {
|
|
981
|
+
const raw = init[2]
|
|
982
|
+
const sa = raw == null ? [] : Array.isArray(raw) && raw[0] === ',' ? raw.slice(1) : [raw]
|
|
983
|
+
viewInit = ctx.core.emit['.string:slice#view'](recv, sa[0], sa[1])
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const isObjLit = Array.isArray(init) && init[0] === '{}'
|
|
988
|
+
if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
|
|
989
|
+
const val = viewInit || emit(init)
|
|
990
|
+
if (isObjLit) ctx.schema.targetStack.pop()
|
|
991
|
+
// Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
|
|
992
|
+
// Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
|
|
993
|
+
// isReassigned is conservative across nested arrow shadows — we miss the optimization
|
|
994
|
+
// rather than emit a wrong direct call.
|
|
995
|
+
if (Array.isArray(init) && init[0] === '=>' && val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
996
|
+
&& ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
997
|
+
if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
|
|
998
|
+
ctx.func.directClosures.set(name, val.closureBodyName)
|
|
999
|
+
}
|
|
1000
|
+
// Copy propagation of a direct closure: `let g = add`, where `add` is a non-escaping
|
|
1001
|
+
// directly-callable closure, makes `g` directly callable too — `g` holds the same
|
|
1002
|
+
// closure value, so `g(…)` calls add's body with g's value as env. This is what
|
|
1003
|
+
// devirtualizes `let arr = [add]; arr[0](…)`: array scalarization rewrites it to
|
|
1004
|
+
// `let g = add; g(…)` before emit (D3), and also covers the explicit `let g = arr[0]`.
|
|
1005
|
+
// Same soundness gate as the direct-closure case: stable binding (not reassigned),
|
|
1006
|
+
// not boxed, not global.
|
|
1007
|
+
if (typeof init === 'string' && ctx.func.directClosures?.has(init) && !ctx.func.boxed.has(name)
|
|
1008
|
+
&& !isGlobal(name) && ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
1009
|
+
ctx.func.directClosures.set(name, ctx.func.directClosures.get(init))
|
|
1010
|
+
}
|
|
1011
|
+
if (ctx.func.boxed.has(name)) {
|
|
1012
|
+
const cell = ctx.func.boxed.get(name)
|
|
1013
|
+
ctx.func.locals.set(cell, 'i32')
|
|
1014
|
+
if (inLoop ? !loopPrebox(name) : !ctx.func.preboxed?.has(name))
|
|
1015
|
+
result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
|
|
1016
|
+
// i32-narrowed cell stores the raw i32 (see readVar/writeVar). The undef
|
|
1017
|
+
// pre-store stays f64: its NaN atom's low word is 0, which is exactly the
|
|
1018
|
+
// plain-local default an i32 read of an uninitialized cell must see.
|
|
1019
|
+
result.push(ctx.func.cellTypes?.has(name)
|
|
1020
|
+
? ['i32.store', ['local.get', `$${cell}`], asI32(val)]
|
|
1021
|
+
: ['f64.store', ['local.get', `$${cell}`], asF64(val)])
|
|
1022
|
+
continue
|
|
1023
|
+
}
|
|
1024
|
+
if (isGlobal(name)) {
|
|
1025
|
+
// Unboxed pointer const globals carry the raw i32 offset; init coerces via asPtrOffset.
|
|
1026
|
+
// Only an i32-STORED global is a raw pointer carrier — an f64 global holds a
|
|
1027
|
+
// NaN-boxed value, so coercing its init to an i32 offset (asPtrOffset → i32.wrap)
|
|
1028
|
+
// would store i32 into an f64 global (invalid wasm). Mirror readVar's storage gate.
|
|
1029
|
+
const grep = repOfGlobal(name)
|
|
1030
|
+
if ((ctx.scope.globalTypes.get(name) || 'f64') === 'i32' && grep?.ptrKind != null) {
|
|
1031
|
+
result.push(['global.set', `$${name}`, asPtrOffset(val, grep.ptrKind)])
|
|
1032
|
+
continue
|
|
1033
|
+
}
|
|
1034
|
+
// Pre-folded numeric const globals have their init baked into an *immutable* decl
|
|
1035
|
+
// (`(global $x i32 (i32.const V))`) — skip the runtime init (global.set on an
|
|
1036
|
+
// immutable global is invalid anyway). But a const typed only by integer-global
|
|
1037
|
+
// inference (or a mutable global narrowed to i32) keeps the declareGlobal-default
|
|
1038
|
+
// `(mut … (i32.const 0))` decl, so its real — possibly non-foldable — initializer
|
|
1039
|
+
// must still run (e.g. `const V = NULLISH + 1` where NULLISH is a cross-module /
|
|
1040
|
+
// dynamic const: V is i32-typed but unfolded, and without this it stays 0).
|
|
1041
|
+
if (ctx.scope.globalTypes.has(name)) {
|
|
1042
|
+
if (ctx.scope.consts?.has(name) && !ctx.scope.globals.get(name)?.mut) continue
|
|
1043
|
+
const gt = ctx.scope.globalTypes.get(name)
|
|
1044
|
+
result.push(['global.set', `$${name}`, gt === 'i32' ? asI32(val) : asF64(val)])
|
|
1045
|
+
continue
|
|
1046
|
+
}
|
|
1047
|
+
result.push(['global.set', `$${name}`, asF64(val)])
|
|
1048
|
+
continue
|
|
1049
|
+
}
|
|
1050
|
+
const localType = ctx.func.locals.get(name) || 'f64'
|
|
1051
|
+
let ptrKind = repOf(name)?.ptrKind
|
|
1052
|
+
// Emit-time rep mutation (lifecycle: analysis → emit transition).
|
|
1053
|
+
// Inherit ptrKind from a pointer-ABI RHS: destructure temps (`__d0 = v`) and other
|
|
1054
|
+
// fresh let-bindings whose init is already an unboxed pointer. Without this, readVar
|
|
1055
|
+
// returns an untyped i32 local.get and later `asF64` emits a numeric convert instead
|
|
1056
|
+
// of a ptr-rebox. Safe because emitDecl runs once per let/const binding — no prior
|
|
1057
|
+
// emit-time read could have observed the unset rep.
|
|
1058
|
+
if (ptrKind == null && val.ptrKind != null && localType === 'i32' && !ctx.func.boxed?.has(name)) {
|
|
1059
|
+
updateRep(name, { ptrKind: val.ptrKind })
|
|
1060
|
+
ptrKind = val.ptrKind
|
|
1061
|
+
if (val.ptrAux != null) {
|
|
1062
|
+
updateRep(name, { ptrAux: val.ptrAux })
|
|
1063
|
+
// OBJECT-only: aux *is* the schemaId; mirror to ctx.schema.vars + rep.schemaId so
|
|
1064
|
+
// .prop slot resolution sees a precise binding. TYPED/CLOSURE aux carries other
|
|
1065
|
+
// semantics (elem code / funcIdx) and must not leak into schema lookups.
|
|
1066
|
+
// Poisoned names (shape-disagreeing assignments) must stay schema-free.
|
|
1067
|
+
if (val.ptrKind === VAL.OBJECT && !ctx.schema.vars?.has(name) && !ctx.schema.poisoned?.has(name)) {
|
|
1068
|
+
ctx.schema.vars.set(name, val.ptrAux)
|
|
1069
|
+
updateRep(name, { schemaId: val.ptrAux })
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
let coerced
|
|
1074
|
+
if (ptrKind != null) {
|
|
1075
|
+
// Unboxed pointer local — extract i32 offset from NaN-boxed f64 via reinterpret, not numeric trunc.
|
|
1076
|
+
// CLOSURE init carries funcIdx in val.closureFuncIdx; persist it on the rep so a later
|
|
1077
|
+
// asF64 (escape: store, return, indirect-call rebox) reconstructs the correct table slot.
|
|
1078
|
+
// Emit-time mutation — analyzeValTypes never sees closureFuncIdx.
|
|
1079
|
+
if (ptrKind === VAL.CLOSURE && val.closureFuncIdx != null && repOf(name)?.ptrAux == null)
|
|
1080
|
+
updateRep(name, { ptrAux: val.closureFuncIdx })
|
|
1081
|
+
coerced = val.ptrKind === ptrKind ? val
|
|
1082
|
+
: typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
|
|
1083
|
+
} else {
|
|
1084
|
+
coerced = localType === 'v128' ? val : localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
|
|
1085
|
+
}
|
|
1086
|
+
// `let x = 0` at function scope is normally elided — WASM zero-inits locals. But loop
|
|
1087
|
+
// unrolling flattens iteration bodies into one scope, so the 2nd+ `let x = 0` are
|
|
1088
|
+
// genuine RE-inits between iterations (e.g. a nested reduce's accumulator). Elide only
|
|
1089
|
+
// the FIRST per name; emit the rest as resets. (Names are preserved — no renaming.)
|
|
1090
|
+
const zeroInit = isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length
|
|
1091
|
+
if (!zeroInit || ctx.func.zeroInitSeen?.has(name))
|
|
1092
|
+
result.push(['local.set', `$${name}`, coerced])
|
|
1093
|
+
else (ctx.func.zeroInitSeen ??= new Set()).add(name)
|
|
1094
|
+
|
|
1095
|
+
const schemaId = ctx.schema.idOf?.(name)
|
|
1096
|
+
if (ctx.func.localProps?.has(name) && schemaId != null) {
|
|
1097
|
+
const schema = ctx.schema.resolve(name)
|
|
1098
|
+
if (schema?.[0] === '__inner__') {
|
|
1099
|
+
inc('__alloc_hdr', '__mkptr')
|
|
1100
|
+
const bt = `${T}bx${ctx.func.uniq++}`
|
|
1101
|
+
ctx.func.locals.set(bt, 'i32')
|
|
1102
|
+
const innerName = `${name}${T}inner`
|
|
1103
|
+
ctx.func.locals.set(innerName, 'f64')
|
|
1104
|
+
result.push(
|
|
1105
|
+
['local.set', `$${innerName}`, ['local.get', `$${name}`]],
|
|
1106
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
|
|
1107
|
+
['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
|
|
1108
|
+
...schema.slice(1).map((_, j) =>
|
|
1109
|
+
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
|
|
1110
|
+
['local.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return result.length === 0 ? null : result.length === 1 ? result[0] : result
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Copy a spread source's elements into a destination array.
|
|
1119
|
+
*
|
|
1120
|
+
* `dest` is the destination data-base i32 local; `posLocal` the element index to
|
|
1121
|
+
* start writing at — advanced by the source length on exit. An ARRAY source is a
|
|
1122
|
+
* contiguous block of f64 NaN-boxes, so it copies with a single `memory.copy`; a
|
|
1123
|
+
* string/typed source needs a per-element decode. The source's *type* is
|
|
1124
|
+
* loop-invariant — it cannot change while the spread runs — so when it is not
|
|
1125
|
+
* statically known it is resolved exactly once (one `__ptr_type`) and branched,
|
|
1126
|
+
* never re-checked per element. Returns a list of IR instructions.
|
|
1127
|
+
*/
|
|
1128
|
+
function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
|
|
1129
|
+
const srcI64 = () => ['i64.reinterpret_f64', ['local.get', `$${srcLocal}`]]
|
|
1130
|
+
const destAddr = idx => ['i32.add', ['local.get', `$${dest}`], ['i32.shl', idx, ['i32.const', 3]]]
|
|
1131
|
+
const arrCopy = () => (inc('__ptr_offset'),
|
|
1132
|
+
['memory.copy', destAddr(['local.get', `$${posLocal}`]),
|
|
1133
|
+
['call', '$__ptr_offset', srcI64()],
|
|
1134
|
+
['i32.shl', ['local.get', `$${srcLenLocal}`], ['i32.const', 3]]])
|
|
1135
|
+
const scalarLoop = () => {
|
|
1136
|
+
const sidx = `${T}sidx${ctx.func.uniq++}`
|
|
1137
|
+
ctx.func.locals.set(sidx, 'i32')
|
|
1138
|
+
const loopId = ctx.func.uniq++
|
|
1139
|
+
const elem = ctx.module.modules['string']
|
|
1140
|
+
? ['if', ['result', 'f64'],
|
|
1141
|
+
['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
|
|
1142
|
+
['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
|
|
1143
|
+
['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]]
|
|
1144
|
+
: (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
|
|
1145
|
+
// Reset the counter on each entry — WASM zeroes locals once at function
|
|
1146
|
+
// entry, but this loop re-executes when the spread sits inside a JS loop;
|
|
1147
|
+
// a stale `sidx` (= prior srcLen) would skip the copy entirely.
|
|
1148
|
+
return ['block', `$break${loopId}`,
|
|
1149
|
+
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
1150
|
+
['loop', `$loop${loopId}`,
|
|
1151
|
+
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
|
|
1152
|
+
['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
|
|
1153
|
+
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
1154
|
+
['br', `$loop${loopId}`]]]
|
|
1155
|
+
}
|
|
1156
|
+
const advance = ['local.set', `$${posLocal}`,
|
|
1157
|
+
['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
|
|
1158
|
+
if (staticVT === VAL.ARRAY) return [arrCopy(), advance]
|
|
1159
|
+
if (staticVT === VAL.STRING || staticVT === VAL.TYPED) return [scalarLoop(), advance]
|
|
1160
|
+
inc('__ptr_type')
|
|
1161
|
+
const tt = tempI32(`${T}spt`)
|
|
1162
|
+
return [
|
|
1163
|
+
['local.set', `$${tt}`, ['call', '$__ptr_type', srcI64()]],
|
|
1164
|
+
dispatchByPtrType(tt, [[PTR.ARRAY, arrCopy()]], scalarLoop(), null),
|
|
1165
|
+
advance,
|
|
1166
|
+
]
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Build an array from items, handling ['__spread', expr] markers.
|
|
1171
|
+
* Split into sections (normal arrays and spreads), then copy all into result.
|
|
1172
|
+
*/
|
|
1173
|
+
export function buildArrayWithSpreads(items) {
|
|
1174
|
+
const spreads = []
|
|
1175
|
+
for (let i = 0; i < items.length; i++) {
|
|
1176
|
+
if (Array.isArray(items[i]) && items[i][0] === '__spread') {
|
|
1177
|
+
spreads.push({ pos: i, expr: items[i][1] })
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (spreads.length === 0) {
|
|
1182
|
+
return emit(['[', ...items])
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const sections = []
|
|
1186
|
+
let currentArray = []
|
|
1187
|
+
|
|
1188
|
+
for (let i = 0; i < items.length; i++) {
|
|
1189
|
+
if (Array.isArray(items[i]) && items[i][0] === '__spread') {
|
|
1190
|
+
if (currentArray.length > 0) {
|
|
1191
|
+
sections.push({ type: 'array', items: currentArray })
|
|
1192
|
+
currentArray = []
|
|
1193
|
+
}
|
|
1194
|
+
sections.push({ type: 'spread', expr: items[i][1] })
|
|
1195
|
+
} else {
|
|
1196
|
+
currentArray.push(items[i])
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (currentArray.length > 0) {
|
|
1200
|
+
sections.push({ type: 'array', items: currentArray })
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// A single all-normal section is a plain literal — defer to the `[` emitter.
|
|
1204
|
+
// A single *spread* section is NOT shortcut to `emit(sec.expr)`: that would
|
|
1205
|
+
// alias the source, but `[...x]` must yield a fresh array. It falls through
|
|
1206
|
+
// to the alloc + emitSpreadCopy path below, which copies.
|
|
1207
|
+
if (sections.length === 1 && sections[0].type === 'array') {
|
|
1208
|
+
return emit(['[', ...sections[0].items])
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const len = tempI32('len')
|
|
1212
|
+
const pos = tempI32('pos')
|
|
1213
|
+
const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
|
|
1214
|
+
const result = out.local
|
|
1215
|
+
|
|
1216
|
+
const ir = []
|
|
1217
|
+
inc('__len')
|
|
1218
|
+
|
|
1219
|
+
// Pass 1 — evaluate every section IN SOURCE ORDER into temps. JS spread keeps
|
|
1220
|
+
// strict left-to-right order: a later spread whose source mutates an earlier
|
|
1221
|
+
// element's input must still observe the pre-mutation value. Array items
|
|
1222
|
+
// become per-item f64 temps; spreads become a ptr temp + a cached __len.
|
|
1223
|
+
for (const sec of sections) {
|
|
1224
|
+
if (sec.type === 'array') {
|
|
1225
|
+
sec.itemLocals = []
|
|
1226
|
+
for (let i = 0; i < sec.items.length; i++) {
|
|
1227
|
+
const it = `${T}ai${ctx.func.uniq++}`
|
|
1228
|
+
ctx.func.locals.set(it, 'f64')
|
|
1229
|
+
sec.itemLocals.push(it)
|
|
1230
|
+
ir.push(['local.set', `$${it}`, asF64(emit(sec.items[i]))])
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
sec.local = `${T}sp${ctx.func.uniq++}`
|
|
1234
|
+
ctx.func.locals.set(sec.local, 'f64')
|
|
1235
|
+
sec.lenLocal = `${T}spl${ctx.func.uniq++}`
|
|
1236
|
+
ctx.func.locals.set(sec.lenLocal, 'i32')
|
|
1237
|
+
const n = multiCount(sec.expr)
|
|
1238
|
+
// Normalize a (non-multi) spread source to an index-iterable: Set→keys /
|
|
1239
|
+
// Map→[k,v] arrays, others pass through. Only when `collection` is loaded —
|
|
1240
|
+
// otherwise no Set/Map can exist and the source is already index-iterable.
|
|
1241
|
+
const srcExpr = !n && ctx.module.modules.collection ? ['()', '__iter_arr', sec.expr] : sec.expr
|
|
1242
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
1243
|
+
// emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
|
|
1244
|
+
sec.val = n ? undefined : valTypeOf(srcExpr)
|
|
1245
|
+
ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(srcExpr))])
|
|
1246
|
+
// Cache __len once per spread; reused below for total-len sum and the copy.
|
|
1247
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Pass 2 — total length (array sections statically sized, spreads cached above).
|
|
1252
|
+
ir.push(['local.set', `$${len}`, ['i32.const', 0]])
|
|
1253
|
+
for (const sec of sections) {
|
|
1254
|
+
if (sec.type === 'array') {
|
|
1255
|
+
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
|
|
1256
|
+
} else {
|
|
1257
|
+
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['local.get', `$${sec.lenLocal}`]]])
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Pass 3 — allocate exact, then store the pre-evaluated temps.
|
|
1262
|
+
ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
|
|
1263
|
+
for (const sec of sections) {
|
|
1264
|
+
if (sec.type === 'array') {
|
|
1265
|
+
for (const it of sec.itemLocals) {
|
|
1266
|
+
ir.push(
|
|
1267
|
+
['f64.store',
|
|
1268
|
+
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
1269
|
+
['local.get', `$${it}`]],
|
|
1270
|
+
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
|
|
1271
|
+
)
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
ir.push(...emitSpreadCopy(result, pos, sec.local, sec.lenLocal, sec.val))
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
ir.push(out.ptr)
|
|
1279
|
+
return block64(...ir)
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Emit node in void context: emit + drop any value. Block bodies route through emitBlockBody. */
|
|
1283
|
+
export function emitVoid(node) {
|
|
1284
|
+
if (isBlockBody(node)) return emitBlockBody(node)
|
|
1285
|
+
const ir = emit(node, 'void')
|
|
1286
|
+
const items = flat(ir)
|
|
1287
|
+
if (ir?.type && ir.type !== 'void') items.push('drop')
|
|
1288
|
+
return items
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitVoid per statement.
|
|
1292
|
+
* Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
|
|
1293
|
+
* rest of the enclosing block. Refinements added here are rolled back on block exit. */
|
|
1294
|
+
export function emitBlockBody(node) {
|
|
1295
|
+
const inner = node[1]
|
|
1296
|
+
const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
|
|
1297
|
+
const out = []
|
|
1298
|
+
const accumulated = []
|
|
1299
|
+
const prevValOverlay = ctx.func.localValTypesOverlay
|
|
1300
|
+
ctx.func.localValTypesOverlay = new Map(prevValOverlay || [])
|
|
1301
|
+
const setFlowVal = (name, vt) => {
|
|
1302
|
+
if (!isBoundName(name)) return
|
|
1303
|
+
if (vt) ctx.func.localValTypesOverlay.set(name, vt)
|
|
1304
|
+
else ctx.func.localValTypesOverlay.delete(name)
|
|
1305
|
+
}
|
|
1306
|
+
const updateFlowVal = (stmt) => {
|
|
1307
|
+
if (!Array.isArray(stmt)) return
|
|
1308
|
+
const op = stmt[0]
|
|
1309
|
+
if (op === '=' && typeof stmt[1] === 'string') {
|
|
1310
|
+
setFlowVal(stmt[1], valTypeOf(stmt[2]))
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
if (op === 'let' || op === 'const') {
|
|
1314
|
+
for (let i = 1; i < stmt.length; i++) {
|
|
1315
|
+
const d = stmt[i]
|
|
1316
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string')
|
|
1317
|
+
setFlowVal(d[1], valTypeOf(d[2]))
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
1323
|
+
const s = stmts[i]
|
|
1324
|
+
if (s == null || typeof s === 'number') continue
|
|
1325
|
+
out.push(...emitVoid(s))
|
|
1326
|
+
updateFlowVal(s)
|
|
1327
|
+
// After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
|
|
1328
|
+
// Skip names that are reassigned later — refinement would be unsound past the assignment.
|
|
1329
|
+
if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
|
|
1330
|
+
const refs = extractRefinements(s[1], new Map(), false)
|
|
1331
|
+
for (const [name, fact] of refs) {
|
|
1332
|
+
let reassigned = false
|
|
1333
|
+
for (let j = i + 1; j < stmts.length; j++)
|
|
1334
|
+
if (isReassigned(stmts[j], name)) { reassigned = true; break }
|
|
1335
|
+
if (reassigned) continue
|
|
1336
|
+
const cur = ctx.func.refinements.get(name)
|
|
1337
|
+
accumulated.push([name, cur])
|
|
1338
|
+
// Merge so sibling early-returns layering on the same name compose
|
|
1339
|
+
// (e.g. `if (typeof x === 'string') return; if (Array.isArray(x)) return;`
|
|
1340
|
+
// leaves both `notString: true` and would-be array exclusion stacked).
|
|
1341
|
+
ctx.func.refinements.set(name, cur ? { ...cur, ...fact } : fact)
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} finally {
|
|
1346
|
+
ctx.func.localValTypesOverlay = prevValOverlay
|
|
1347
|
+
// Restore prior refinements on block exit.
|
|
1348
|
+
for (let i = accumulated.length - 1; i >= 0; i--) {
|
|
1349
|
+
const [name, prev] = accumulated[i]
|
|
1350
|
+
if (prev === undefined) ctx.func.refinements.delete(name); else ctx.func.refinements.set(name, prev)
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return out
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// A VAL.BOOL value can ride either the cheap 0/1 numeric carrier or, after it has
|
|
1357
|
+
// escaped into an object slot, a boxed boolean atom. `ToNumber(bool)` normalizes
|
|
1358
|
+
// both to 0/1, so for relational / loose-equality coercion a boolean behaves
|
|
1359
|
+
// identically to a number. Normalize it before the type-directed compare dispatch
|
|
1360
|
+
// (the BOOL fact still drives typeof / String / boundary boxing).
|
|
1361
|
+
const numericVal = vt => vt === VAL.BOOL ? VAL.NUMBER : vt
|
|
1362
|
+
|
|
1363
|
+
// Primitive value-type classes for strict-equality type-mismatch folding. Two
|
|
1364
|
+
// operands of different known classes — when at least one is a primitive — can
|
|
1365
|
+
// never be `===` (number/boolean/string/bigint don't cross-coerce under `===`).
|
|
1366
|
+
// Two *reference* kinds (array vs object, …) fall through to the shared ref-eq
|
|
1367
|
+
// path instead, which already resolves distinct pointers to `false`.
|
|
1368
|
+
const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Strict `===`/`!==`. Unlike loose `==`, no coercion: a statically-known type
|
|
1372
|
+
* mismatch folds to a constant (`true === 1` → false, `"1" === 1` → false). When
|
|
1373
|
+
* the types match — or one side is statically unknown — the result is bit-for-bit
|
|
1374
|
+
* identical to loose `==` on same-type operands, so we delegate to it.
|
|
1375
|
+
*
|
|
1376
|
+
* `null` and `undefined` are distinct NaN-boxed sentinels, so `===` tells them
|
|
1377
|
+
* apart (`null === undefined` is false) even though loose `==` treats both nullish.
|
|
1378
|
+
*
|
|
1379
|
+
* One carrier-level limitation remains (documented gap, not a regression): booleans
|
|
1380
|
+
* and numbers share the 0/1 carrier, so `1 === trueDynamic` can only be told apart
|
|
1381
|
+
* when the boolean's type is statically known.
|
|
1382
|
+
*/
|
|
1383
|
+
// A binding the analyzer marked `nullable` (its init or some assignment was a
|
|
1384
|
+
// nullish literal) can hold null/undefined at runtime, so `x === null` / `x == null`
|
|
1385
|
+
// must NOT fold to a constant even when `val` is a definite non-null kind. Only bare
|
|
1386
|
+
// variable reads carry the flag; literals/fresh allocations are inherently non-null.
|
|
1387
|
+
const nullableOperand = (n) =>
|
|
1388
|
+
typeof n === 'string' && !!(repOf(n)?.nullable || repOfGlobal(n)?.nullable)
|
|
1389
|
+
|
|
1390
|
+
function emitLooseEq(a, b, negate) {
|
|
1391
|
+
const eqOp = negate ? 'ne' : 'eq'
|
|
1392
|
+
const sentinel = emitNum(negate ? 1 : 0)
|
|
1393
|
+
const charCmp = emitSingleCharIndexCmp(a, b, negate); if (charCmp) return charCmp
|
|
1394
|
+
const subCmp = emitSubstringEqCmp(a, b, negate); if (subCmp) return subCmp
|
|
1395
|
+
// JS loose nullish equality: x == null / x == undefined.
|
|
1396
|
+
// If the non-literal side has a known non-null VAL type, fold to the sentinel.
|
|
1397
|
+
const nullishOf = (other) => {
|
|
1398
|
+
if (valTypeOf(other) && !nullableOperand(other)) return sentinel
|
|
1399
|
+
const chk = isNullish(asF64(emit(other)))
|
|
1400
|
+
return negate ? typed(['i32.eqz', chk], 'i32') : chk
|
|
1401
|
+
}
|
|
1402
|
+
if (isNullishLit(a)) return nullishOf(b)
|
|
1403
|
+
if (isNullishLit(b)) return nullishOf(a)
|
|
1404
|
+
// typeof x == 'string' → compile-time type check (prepare rewrites string to type code)
|
|
1405
|
+
const tc = emitTypeofCmp(a, b, eqOp); if (tc) return tc
|
|
1406
|
+
const va = emit(a), vb = emit(b)
|
|
1407
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${eqOp}`, va, vb], 'i32')
|
|
1408
|
+
// Either side known-pure NUMBER (literal or typed) → f64.eq/ne is correct regardless
|
|
1409
|
+
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1410
|
+
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
1411
|
+
// Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
|
|
1412
|
+
const rawA = resolveValType(a, valTypeOf, lookupValType)
|
|
1413
|
+
const rawB = resolveValType(b, valTypeOf, lookupValType)
|
|
1414
|
+
const vta = numericVal(rawA)
|
|
1415
|
+
const vtb = numericVal(rawB)
|
|
1416
|
+
const numA = () => rawA === VAL.BOOL ? toNumF64(a, va) : asF64(va)
|
|
1417
|
+
const numB = () => rawB === VAL.BOOL ? toNumF64(b, vb) : asF64(vb)
|
|
1418
|
+
if (vta === VAL.NUMBER && needsToNumberCoercion(b, vtb)) return looseNumberEq(numA(), b, vb, negate)
|
|
1419
|
+
if (vtb === VAL.NUMBER && needsToNumberCoercion(a, vta)) return looseNumberEq(numB(), a, va, negate)
|
|
1420
|
+
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed([`f64.${eqOp}`, numA(), numB()], 'i32')
|
|
1421
|
+
// Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
|
|
1422
|
+
// JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
|
|
1423
|
+
// STRING needs __eq (heap strings can be equal by content but different pointers).
|
|
1424
|
+
// BIGINT needs __eq (heap-allocated, content compare).
|
|
1425
|
+
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1426
|
+
return typed([`i64.${eqOp}`, ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1427
|
+
}
|
|
1428
|
+
inc('__eq')
|
|
1429
|
+
const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
1430
|
+
return negate ? typed(['i32.eqz', call], 'i32') : call
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function emitStrictEq(a, b, negate) {
|
|
1434
|
+
// `typeof x === 'type'` (prepare rewrote the literal to a numeric code) — typeof
|
|
1435
|
+
// always yields a string, so strict and loose agree; reuse the loose lowering.
|
|
1436
|
+
const tc = emitTypeofCmp(a, b, negate ? 'ne' : 'eq'); if (tc) return tc
|
|
1437
|
+
// Strict equality against a `null` or `undefined` literal must match ONLY that
|
|
1438
|
+
// exact sentinel — `undefined === null` is false, unlike loose `==`. prepare
|
|
1439
|
+
// normalizes both to the value-wrapper form `[, v]` (op==null) where the *strict*
|
|
1440
|
+
// value of node[1] is the discriminator (=== null vs === undefined); the loose
|
|
1441
|
+
// isNullLit/isUndefLit predicates use `== null` and can't tell them apart, so key
|
|
1442
|
+
// off node[1] here — exactly as emit()'s literal value path does. A statically
|
|
1443
|
+
// non-nullish operand (known VAL) is neither sentinel, so fold to a constant.
|
|
1444
|
+
const sentinelOf = (n) => {
|
|
1445
|
+
if (!Array.isArray(n) || n[0] != null) return null
|
|
1446
|
+
if (n.length < 2 || n[1] === undefined) return 'undef'
|
|
1447
|
+
if (n[1] === null) return 'null'
|
|
1448
|
+
return null // numeric / string literal value — not a nullish sentinel
|
|
1449
|
+
}
|
|
1450
|
+
const strictSentinel = (other, undef) => {
|
|
1451
|
+
if (valTypeOf(other) && !nullableOperand(other)) return emitNum(negate ? 1 : 0)
|
|
1452
|
+
const chk = (undef ? isUndef : isNull)(asF64(emit(other)))
|
|
1453
|
+
return negate ? typed(['i32.eqz', chk], 'i32') : chk
|
|
1454
|
+
}
|
|
1455
|
+
const sa = sentinelOf(a), sb = sentinelOf(b)
|
|
1456
|
+
if (sb) return strictSentinel(a, sb === 'undef')
|
|
1457
|
+
if (sa) return strictSentinel(b, sa === 'undef')
|
|
1458
|
+
// Known, differing primitive classes can never be strictly equal.
|
|
1459
|
+
const strictA = resolveValType(a, valTypeOf, lookupValType)
|
|
1460
|
+
const strictB = resolveValType(b, valTypeOf, lookupValType)
|
|
1461
|
+
if (strictA && strictB && strictA !== strictB && (STRICT_PRIM.has(strictA) || STRICT_PRIM.has(strictB)))
|
|
1462
|
+
return emitNum(negate ? 1 : 0)
|
|
1463
|
+
// Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
|
|
1464
|
+
return emitter[negate ? '!=' : '=='](a, b)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/** Comparison op factory with constant folding. */
|
|
1468
|
+
const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
1469
|
+
const va = emit(a), vb = emit(b)
|
|
1470
|
+
// Skip the const-fold for `.unsigned` operands: `litVal` is the signed bit pattern
|
|
1471
|
+
// (-1, not 4294967295), so folding the order would be wrong. Fall through to the
|
|
1472
|
+
// f64 widen path below, which converts each operand by its own signedness.
|
|
1473
|
+
if (isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
|
|
1474
|
+
// String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
|
|
1475
|
+
// (always false), so without this the spec-correct `"a" < "b"` returns 0.
|
|
1476
|
+
// Route both-STRING operands through __str_cmp's three-way result, then apply
|
|
1477
|
+
// the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
|
|
1478
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1479
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1480
|
+
if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
|
|
1481
|
+
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
1482
|
+
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
1483
|
+
}
|
|
1484
|
+
if (vta === VAL.STRING && vtb === VAL.STRING) {
|
|
1485
|
+
return typed([`i32.${i32op}`, stringOps(a).cmp(asF64(va), asF64(vb), ctx), ['i32.const', 0]], 'i32')
|
|
1486
|
+
}
|
|
1487
|
+
// Exactly one operand is a known string; the other has no static type, so it
|
|
1488
|
+
// may hold a string pointer at runtime (e.g. `c >= '0'` where `c` came from
|
|
1489
|
+
// `s[i]` on an untyped receiver). JS relational compare is lexicographic only
|
|
1490
|
+
// when *both* sides are strings, else it ToNumbers both. The f64 path below
|
|
1491
|
+
// would compare the unknown side's NaN-boxed string bits as a float (NaN ⇒
|
|
1492
|
+
// always false), so dispatch at runtime on the unknown side: string → __str_cmp
|
|
1493
|
+
// three-way; else ToNumber both. Mirrors `+`'s __is_str_key string dispatch.
|
|
1494
|
+
// Gated on a *known-string* counterpart, so numeric loops (`i < n`) never pay
|
|
1495
|
+
// the check — comparing against a string literal signals string intent.
|
|
1496
|
+
if (((vta === VAL.STRING && vtb == null) || (vtb === VAL.STRING && vta == null)) && stringOps(a)?.cmp) {
|
|
1497
|
+
const unkIsA = vta == null
|
|
1498
|
+
const ta = temp('cmp'), tb = temp('cmp')
|
|
1499
|
+
inc('__is_str_key')
|
|
1500
|
+
const getA = typed(['local.get', `$${ta}`], 'f64'), getB = typed(['local.get', `$${tb}`], 'f64')
|
|
1501
|
+
const check = ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${unkIsA ? ta : tb}`]]]
|
|
1502
|
+
const strCmp = [`i32.${i32op}`, stringOps(a).cmp(getA, getB, ctx), ['i32.const', 0]]
|
|
1503
|
+
const numCmp = [`f64.${f64op}`, toNumF64(a, getA), toNumF64(b, getB)]
|
|
1504
|
+
return typed(['block', ['result', 'i32'],
|
|
1505
|
+
['local.set', `$${ta}`, asF64(va)],
|
|
1506
|
+
['local.set', `$${tb}`, asF64(vb)],
|
|
1507
|
+
['if', ['result', 'i32'], check, ['then', strCmp], ['else', numCmp]]], 'i32')
|
|
1508
|
+
}
|
|
1509
|
+
if (vta === VAL.DATE || vtb === VAL.DATE) {
|
|
1510
|
+
const dateNum = (node, v, vt) => {
|
|
1511
|
+
if (vt !== VAL.DATE) return toNumF64(node, v)
|
|
1512
|
+
const ptr = v.ptrKind === VAL.DATE
|
|
1513
|
+
? v
|
|
1514
|
+
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
1515
|
+
return typed(['f64.load', ptr], 'f64')
|
|
1516
|
+
}
|
|
1517
|
+
return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
|
|
1518
|
+
}
|
|
1519
|
+
if (vtb === VAL.NUMBER && needsToNumberCoercion(a, vta))
|
|
1520
|
+
return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
|
|
1521
|
+
if (vta === VAL.NUMBER && needsToNumberCoercion(b, vtb))
|
|
1522
|
+
return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
|
|
1523
|
+
// An `.unsigned` i32 operand ([0, 2^32)) can't share a signed i32 compare with a
|
|
1524
|
+
// possibly-signed one: mixed sign inverts the order (3 < 0xFFFFFFFF unsigned, but
|
|
1525
|
+
// 3 > -1 signed). Widen to f64, where asF64 converts each operand by its own
|
|
1526
|
+
// signedness (convert_i32_u for unsigned, _s otherwise) to its true numeric value.
|
|
1527
|
+
if (!va.unsigned && !vb.unsigned) {
|
|
1528
|
+
const ai = intConstValue(a), bi = intConstValue(b)
|
|
1529
|
+
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
1530
|
+
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
1531
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${i32op}`, va, vb], 'i32')
|
|
1532
|
+
}
|
|
1533
|
+
return typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/** Both relational (`<` `>=` …) and loose `==`/`!=` need ToNumber on the
|
|
1537
|
+
* unknown side iff it's known-string or might dereference a boxed value. */
|
|
1538
|
+
function needsToNumberCoercion(expr, vt) {
|
|
1539
|
+
if (vt === VAL.STRING) return true
|
|
1540
|
+
if (vt != null) return false
|
|
1541
|
+
return mayReadBoxedValue(expr)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function looseNumberEq(numIR, otherNode, otherIR, negate = false) {
|
|
1545
|
+
const t = temp('eq')
|
|
1546
|
+
const other = typed(['local.get', `$${t}`], 'f64')
|
|
1547
|
+
const cmp = ['f64.eq', asF64(numIR), toNumF64(otherNode, other)]
|
|
1548
|
+
return typed(['block', ['result', 'i32'],
|
|
1549
|
+
['local.set', `$${t}`, asF64(otherIR)],
|
|
1550
|
+
['if', ['result', 'i32'], isNullish(other),
|
|
1551
|
+
['then', ['i32.const', negate ? 1 : 0]],
|
|
1552
|
+
['else', negate ? ['i32.eqz', cmp] : cmp]]], 'i32')
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function mayReadBoxedValue(expr) {
|
|
1556
|
+
return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function intConstValue(expr) {
|
|
1560
|
+
if (typeof expr === 'number' && Number.isInteger(expr)) return expr
|
|
1561
|
+
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
|
|
1562
|
+
if (typeof expr === 'string') {
|
|
1563
|
+
const v = repOf(expr)?.intConst
|
|
1564
|
+
if (v != null) return v
|
|
1565
|
+
}
|
|
1566
|
+
return null
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function bigintUnsignedBound(expr) {
|
|
1570
|
+
// Self-describing literal carries the unsigned-64 decimal (`BigInt.asUintN(64,…)`,
|
|
1571
|
+
// so 1–20 digits, always ≤ 2^64-1). Detect the high-unsigned range (> 2^63-1) by
|
|
1572
|
+
// decimal magnitude — the kernel can't parse large decimals back to BigInt.
|
|
1573
|
+
if (Array.isArray(expr) && expr[0] === 'bigint') {
|
|
1574
|
+
const s = expr[1]
|
|
1575
|
+
return s.length > 19 || (s.length === 19 && s > '9223372036854775807')
|
|
1576
|
+
}
|
|
1577
|
+
const n = bigintConstValue(expr)
|
|
1578
|
+
return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function bigintConstValue(expr) {
|
|
1582
|
+
if (typeof expr === 'bigint') return expr
|
|
1583
|
+
if (!Array.isArray(expr)) return null
|
|
1584
|
+
if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
|
|
1585
|
+
if (expr[0] === 'u-') {
|
|
1586
|
+
const n = bigintConstValue(expr[1])
|
|
1587
|
+
return n == null ? null : -n
|
|
1588
|
+
}
|
|
1589
|
+
return null
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// === Call IR helpers ===
|
|
1593
|
+
|
|
1594
|
+
/** Split a flat argList into normal positional args + spread positions. */
|
|
1595
|
+
function parseCallArgs(args) {
|
|
1596
|
+
const normal = []
|
|
1597
|
+
const spreads = []
|
|
1598
|
+
for (let i = 0; i < args.length; i++) {
|
|
1599
|
+
const arg = args[i]
|
|
1600
|
+
if (Array.isArray(arg) && arg[0] === '...') {
|
|
1601
|
+
spreads.push({ pos: normal.length, expr: arg[1] })
|
|
1602
|
+
} else {
|
|
1603
|
+
normal.push(arg)
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return { normal, spreads, hasSpread: spreads.length > 0 }
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/** Bulk `obj.push(...src)` fast path — single trailing spread, no normal args, named
|
|
1610
|
+
* receiver. Amortizes the per-element grow + set_len of the generic loop into one
|
|
1611
|
+
* __arr_grow / __set_len pair, then bulk-copies the source via emitSpreadCopy.
|
|
1612
|
+
* Hot path in watr's `out.push(...HANDLER[op](...))` (~24M bytes/iter on raycast). */
|
|
1613
|
+
function emitBulkPushSpread(objArg, parsed) {
|
|
1614
|
+
const spreadExpr = parsed.spreads[0].expr
|
|
1615
|
+
inc('__len'); inc('__arr_grow'); inc('__set_len'); inc('__ptr_offset')
|
|
1616
|
+
const o = `${T}po${ctx.func.uniq++}`,
|
|
1617
|
+
sa = `${T}psa${ctx.func.uniq++}`,
|
|
1618
|
+
sl = `${T}psl${ctx.func.uniq++}`,
|
|
1619
|
+
ol = `${T}pol${ctx.func.uniq++}`,
|
|
1620
|
+
si = `${T}psi${ctx.func.uniq++}`,
|
|
1621
|
+
base = `${T}pb${ctx.func.uniq++}`
|
|
1622
|
+
ctx.func.locals.set(o, 'f64'); ctx.func.locals.set(sa, 'f64')
|
|
1623
|
+
ctx.func.locals.set(sl, 'i32'); ctx.func.locals.set(ol, 'i32')
|
|
1624
|
+
ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
|
|
1625
|
+
|
|
1626
|
+
const objIsArr = lookupValType(objArg) === VAL.ARRAY
|
|
1627
|
+
const n = multiCount(spreadExpr)
|
|
1628
|
+
// Normalize a (non-multi) spread source to an index-iterable: Set→keys /
|
|
1629
|
+
// Map→[k,v] arrays, others pass through. Only when `collection` is loaded.
|
|
1630
|
+
const srcExpr = !n && ctx.module.modules.collection ? ['()', '__iter_arr', spreadExpr] : spreadExpr
|
|
1631
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
1632
|
+
// emitSpreadCopy resolve its kind once at runtime.
|
|
1633
|
+
const srcVT = n ? undefined : valTypeOf(srcExpr)
|
|
1634
|
+
const ir = []
|
|
1635
|
+
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1636
|
+
ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(srcExpr))])
|
|
1637
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1638
|
+
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1639
|
+
if (objIsArr) {
|
|
1640
|
+
ir.push(['local.set', `$${ol}`,
|
|
1641
|
+
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
|
|
1642
|
+
} else {
|
|
1643
|
+
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1644
|
+
}
|
|
1645
|
+
// Single grow for the full spread (vs per-element grow check in the generic loop).
|
|
1646
|
+
ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1647
|
+
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1648
|
+
// base captured AFTER grow (grow may relocate the array).
|
|
1649
|
+
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1650
|
+
// Bulk-copy the spread: an ARRAY source is a contiguous f64 block → memory.copy.
|
|
1651
|
+
ir.push(['local.set', `$${si}`, ['local.get', `$${ol}`]])
|
|
1652
|
+
ir.push(...emitSpreadCopy(base, si, sa, sl, srcVT))
|
|
1653
|
+
// Single set_len for the full spread.
|
|
1654
|
+
ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1655
|
+
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1656
|
+
// Update source variable: grow may have moved the pointer.
|
|
1657
|
+
ir.push(persistBindingPtr(objArg, ['local.get', `$${o}`]))
|
|
1658
|
+
ir.push(['f64.convert_i32_s', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1659
|
+
return block64(...ir)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/** Single trailing spread, with optional preceding normal args. Calls methodEmitter
|
|
1663
|
+
* once for the normal args (if any), then loops methodEmitter over each spread
|
|
1664
|
+
* element. `unshift` walks the spread end-to-start so prepend order matches JS. */
|
|
1665
|
+
/** Emit a per-element loop over `spreadExpr`: allocate arr/len/idx locals, seed
|
|
1666
|
+
* the arr rep when the spread VT is known, run `bodyFn(arr, idx, len)` once per
|
|
1667
|
+
* element. When `reverse` is set, walks the spread from end to start (used by
|
|
1668
|
+
* `unshift` to preserve argument order under successive prepends). Returns the
|
|
1669
|
+
* IR instruction list (caller embeds it into its own block64). */
|
|
1670
|
+
function emitSpreadElementLoop(spreadExpr, bodyFn, { reverse = false } = {}) {
|
|
1671
|
+
const arr = `${T}sp${ctx.func.uniq++}`
|
|
1672
|
+
const len = `${T}splen${ctx.func.uniq++}`
|
|
1673
|
+
const idx = `${T}spidx${ctx.func.uniq++}`
|
|
1674
|
+
ctx.func.locals.set(arr, 'f64'); ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
|
|
1675
|
+
// Emit-time rep seeding for a fresh spread-staging local (no prior reader).
|
|
1676
|
+
// Without this, the loop body's `[]` read on `arr` falls back to polymorphic
|
|
1677
|
+
// dispatch — VAL.* on the rep elides STRING gate for ARRAY/TYPED spreads.
|
|
1678
|
+
const spreadVT = valTypeOf(spreadExpr)
|
|
1679
|
+
if (spreadVT) updateRep(arr, { val: spreadVT })
|
|
1680
|
+
inc('__len')
|
|
1681
|
+
const n = multiCount(spreadExpr)
|
|
1682
|
+
const loopId = ctx.func.uniq++
|
|
1683
|
+
const exhausted = reverse
|
|
1684
|
+
? ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]
|
|
1685
|
+
: ['i32.ge_u', ['local.get', `$${idx}`], ['local.get', `$${len}`]]
|
|
1686
|
+
return [
|
|
1687
|
+
['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1688
|
+
['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]],
|
|
1689
|
+
['local.set', `$${idx}`, reverse ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]],
|
|
1690
|
+
['block', `$break${loopId}`,
|
|
1691
|
+
['loop', `$continue${loopId}`,
|
|
1692
|
+
['br_if', `$break${loopId}`, exhausted],
|
|
1693
|
+
...bodyFn(arr, idx, len),
|
|
1694
|
+
['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['i32.const', reverse ? -1 : 1]]],
|
|
1695
|
+
['br', `$continue${loopId}`]]],
|
|
1696
|
+
]
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
1700
|
+
const inPlace = SPREAD_MUTATORS.has(method)
|
|
1701
|
+
// unshift prepends each arg to the front — forward iteration reverses intent.
|
|
1702
|
+
const reverse = method === 'unshift'
|
|
1703
|
+
const acc = `${T}acc${ctx.func.uniq++}`
|
|
1704
|
+
ctx.func.locals.set(acc, 'f64')
|
|
1705
|
+
const ir = [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
1706
|
+
if (parsed.normal.length > 0) {
|
|
1707
|
+
const r = asF64(methodEmitter(objArg, ...parsed.normal))
|
|
1708
|
+
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1709
|
+
}
|
|
1710
|
+
ir.push(...emitSpreadElementLoop(parsed.spreads[0].expr, (arr, idx) => {
|
|
1711
|
+
const body = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
|
|
1712
|
+
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1713
|
+
}, { reverse }))
|
|
1714
|
+
ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
|
|
1715
|
+
return block64(...ir)
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/** General spread mix: iterate combined args in original order, batch contiguous
|
|
1719
|
+
* normal args into a single methodEmitter call, emit a per-element loop for each
|
|
1720
|
+
* spread. For in-place methods chains via `objArg` (source variable); otherwise
|
|
1721
|
+
* threads through an accumulator local. */
|
|
1722
|
+
function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
1723
|
+
const inPlace = SPREAD_MUTATORS.has(method)
|
|
1724
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1725
|
+
// Accumulator (only used when not in-place); recv passed to methodEmitter is the live target.
|
|
1726
|
+
const acc = inPlace ? null : `${T}acc${ctx.func.uniq++}`
|
|
1727
|
+
if (acc) ctx.func.locals.set(acc, 'f64')
|
|
1728
|
+
const recv = inPlace ? objArg : acc
|
|
1729
|
+
const ir = inPlace ? [] : [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
1730
|
+
let batch = []
|
|
1731
|
+
const flushBatch = () => {
|
|
1732
|
+
if (!batch.length) return
|
|
1733
|
+
const r = asF64(methodEmitter(recv, ...batch))
|
|
1734
|
+
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1735
|
+
batch = []
|
|
1736
|
+
}
|
|
1737
|
+
for (const item of combined) {
|
|
1738
|
+
if (Array.isArray(item) && item[0] === '__spread') {
|
|
1739
|
+
flushBatch()
|
|
1740
|
+
ir.push(...emitSpreadElementLoop(item[1], (arr, idx) => {
|
|
1741
|
+
const body = asF64(methodEmitter(recv, ['[]', arr, idx]))
|
|
1742
|
+
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1743
|
+
}))
|
|
1744
|
+
} else {
|
|
1745
|
+
batch.push(item)
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
flushBatch()
|
|
1749
|
+
ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
|
|
1750
|
+
return block64(...ir)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** Method-emitter call: directly, or via one of the spread fast paths. */
|
|
1754
|
+
function emitMethodCallSpread(objArg, methodEmitter, parsed, method) {
|
|
1755
|
+
if (!parsed.hasSpread) return methodEmitter(objArg, ...parsed.normal)
|
|
1756
|
+
if (method === 'push' && parsed.normal.length === 0 &&
|
|
1757
|
+
parsed.spreads.length === 1 && typeof objArg === 'string')
|
|
1758
|
+
return emitBulkPushSpread(objArg, parsed)
|
|
1759
|
+
if (parsed.spreads.length === 1 && parsed.spreads[0].pos === parsed.normal.length)
|
|
1760
|
+
return emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter)
|
|
1761
|
+
return emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/** Hoist `headExpr` into a temp, evaluate it once, and yield `body(t)` when the
|
|
1765
|
+
* temp is non-nullish, else `undefined`. Shared by every `?.`-shaped optional
|
|
1766
|
+
* emitter (chain-lift, `?.`, `?.[]`, `?.()` via `evalOnce` + this helper) so
|
|
1767
|
+
* the nullish-guard scaffold stays in one place. */
|
|
1768
|
+
function withNullGuard(headExpr, body, tag = 'ng') {
|
|
1769
|
+
const t = temp(tag)
|
|
1770
|
+
return block64(
|
|
1771
|
+
['local.set', `$${t}`, headExpr],
|
|
1772
|
+
['if', ['result', 'f64'],
|
|
1773
|
+
['i32.eqz', isNullish(['local.get', `$${t}`])],
|
|
1774
|
+
['then', body(t)],
|
|
1775
|
+
['else', undefExpr()]])
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Leading method-call strategies (chain positions 1–4). Each is *context-free* —
|
|
1779
|
+
// it depends only on the parsed call, not on the receiver-type analysis (`vt` /
|
|
1780
|
+
// `callMethod`) that emitMethodCall computes below — so they factor out into an
|
|
1781
|
+
// ordered, first-match-wins table. A strategy returns its IR, or `undefined` to
|
|
1782
|
+
// fall through to the next. (Positions 5–12 thread shared mid-function state and
|
|
1783
|
+
// stay inline.) New context-free strategies just push onto LEADING_STRATEGIES.
|
|
1784
|
+
|
|
1785
|
+
// 1. SRoA flat object: `o.method(args)` — scanFlatObjects dissolved `o` into
|
|
1786
|
+
// `o#i` field locals and deleted `$o`, so the method closure lives in the field
|
|
1787
|
+
// local, not a heap slot. Read it directly and dispatch. Without this, every
|
|
1788
|
+
// path below loads from `local.get $o`, which no longer exists (watr then reports
|
|
1789
|
+
// "Unknown local $o"). Mirrors the flat `.`/`[]` hooks.
|
|
1790
|
+
function tryFlatObjectMethod(callee, obj, method, parsed) {
|
|
1791
|
+
if (typeof obj === 'string' && ctx.closure.call) {
|
|
1792
|
+
const flat = ctx.func.flatObjects?.get(obj)
|
|
1793
|
+
const fi = flat ? flat.names.indexOf(method) : -1
|
|
1794
|
+
if (fi >= 0) {
|
|
1795
|
+
const propRead = typed(['local.get', `$${obj}#${fi}`], 'f64')
|
|
1796
|
+
if (parsed.hasSpread)
|
|
1797
|
+
return ctx.closure.call(propRead, [buildArrayWithSpreads(reconstructArgsWithSpreads(parsed.normal, parsed.spreads))], true)
|
|
1798
|
+
return ctx.closure.call(propRead, parsed.normal)
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// 2. charCodeAt with a statically in-bounds index — emit the i32 (OOB-impossible)
|
|
1804
|
+
// contract directly; the generic path keeps the f64/NaN JS-spec result. See
|
|
1805
|
+
// analyze.js inBoundsCharCodeAt.
|
|
1806
|
+
function tryCharCodeAtFast(callee, obj, method, parsed) {
|
|
1807
|
+
if (method === 'charCodeAt' && !parsed.hasSpread && parsed.normal.length === 1
|
|
1808
|
+
&& stringOps(obj)?.charCodeAt && inBoundsCharCodeAt(ctx).has(callee)) {
|
|
1809
|
+
const recv = emit(obj)
|
|
1810
|
+
// jsstring carrier: receiver is an externref boundary param. Route to
|
|
1811
|
+
// `wasm:js-string.charCodeAt` directly — the in-bounds proof rules out the
|
|
1812
|
+
// OOB trap the builtin would otherwise raise.
|
|
1813
|
+
if (recv?.type === 'externref') {
|
|
1814
|
+
ctx.core.jsstring.add('charCodeAt')
|
|
1815
|
+
return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
|
|
1816
|
+
}
|
|
1817
|
+
return typed(stringOps(obj).charCodeAt(
|
|
1818
|
+
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// 3. splice(start, deleteCount, ...items): the one array method that both deletes
|
|
1823
|
+
// and inserts. callMethod's spread machinery models per-element mutators
|
|
1824
|
+
// (push/concat), not a single delete+insert, so a spread of inserts would be
|
|
1825
|
+
// misapplied. Handle the full arg list here: delete-only (no inserts) falls
|
|
1826
|
+
// through to the inline `.splice` emitter; any insert items route through
|
|
1827
|
+
// __arr_splice, which grows/shifts in place (the caller's pointer stays valid via
|
|
1828
|
+
// array forwarding) and returns the removed elements. Guard against a spread in
|
|
1829
|
+
// the start/deleteCount slots (`splice(...x)`) — that form has no static arity.
|
|
1830
|
+
function trySpliceInsert(callee, obj, method, parsed) {
|
|
1831
|
+
if (method === 'splice' && ctx.core.emit['.splice']) {
|
|
1832
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1833
|
+
const inserts = combined.slice(2)
|
|
1834
|
+
const headSpread = combined[0]?.[0] === '__spread' || combined[1]?.[0] === '__spread'
|
|
1835
|
+
if (inserts.length && !headSpread) {
|
|
1836
|
+
inc('__arr_splice')
|
|
1837
|
+
return typed(['call', '$__arr_splice',
|
|
1838
|
+
asI64(emit(obj)),
|
|
1839
|
+
asI32(emit(combined[0])),
|
|
1840
|
+
asI32(emit(combined[1])),
|
|
1841
|
+
asI64(buildArrayWithSpreads(inserts))], 'f64')
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// 4. Function property call: fn.prop(args) → direct call to fn$prop. Skipped when
|
|
1847
|
+
// the property was reassigned (wrapper composition) — then it is a mutable slot
|
|
1848
|
+
// and must be read dynamically before the call.
|
|
1849
|
+
function tryFnPropCall(callee, obj, method, parsed) {
|
|
1850
|
+
if (typeof obj === 'string' && ctx.func.names.has(obj) && !ctx.func.multiProp.has(`${obj}.${method}`)) {
|
|
1851
|
+
const fname = `${obj}$${method}`
|
|
1852
|
+
if (ctx.func.names.has(fname)) {
|
|
1853
|
+
const func = ctx.func.map.get(fname)
|
|
1854
|
+
const emittedArgs = emitCallArgs(parsed.normal, func.sig.params)
|
|
1855
|
+
return attachSigMeta(typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0]), func.sig)
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const LEADING_STRATEGIES = [tryFlatObjectMethod, tryCharCodeAtFast, trySpliceInsert, tryFnPropCall]
|
|
1861
|
+
|
|
1862
|
+
// Strategies 5–12 share the receiver's resolved value type and the
|
|
1863
|
+
// `callMethod` shim — packaged once into a dispatch-context record `c` =
|
|
1864
|
+
// `{ obj, method, parsed, vt, callMethod }` so each strategy is a named
|
|
1865
|
+
// function in TYPED_STRATEGIES, same first-match-wins contract as
|
|
1866
|
+
// LEADING_STRATEGIES. The last entry (external fallback) is total.
|
|
1867
|
+
|
|
1868
|
+
// 5. Boxed object: delegate method to inner value (slot 0)
|
|
1869
|
+
function tryBoxedDelegate({ obj, method, callMethod }) {
|
|
1870
|
+
if (typeof obj === 'string' && ctx.schema.isBoxed?.(obj)) {
|
|
1871
|
+
const innerVt = repOf(obj)?.val
|
|
1872
|
+
const innerEmitter = ctx.core.emit[`.${innerVt}:${method}`] || ctx.core.emit[`.${method}`]
|
|
1873
|
+
if (innerEmitter) {
|
|
1874
|
+
const innerName = `${obj}${T}inner`
|
|
1875
|
+
if (!ctx.func.locals.has(innerName)) ctx.func.locals.set(innerName, 'f64')
|
|
1876
|
+
const boxBase = tempI32('bb')
|
|
1877
|
+
// Load current inner value from boxed object's slot 0 (may have been updated by prior mutations)
|
|
1878
|
+
// Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
|
|
1879
|
+
const loadInner = [
|
|
1880
|
+
['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
|
|
1881
|
+
['local.set', `$${innerName}`, ctx.abi.object.ops.load(['local.get', `$${boxBase}`], 0)]]
|
|
1882
|
+
const result = callMethod(innerName, innerEmitter)
|
|
1883
|
+
// Mutating methods may reallocate; writeback inner value to boxed slot
|
|
1884
|
+
if (BOXED_MUTATORS.has(method)) {
|
|
1885
|
+
const wb = ctx.abi.object.ops.store(['local.get', `$${boxBase}`], 0, ['local.get', `$${innerName}`])
|
|
1886
|
+
return block64(...loadInner, asF64(result), wb)
|
|
1887
|
+
}
|
|
1888
|
+
// Non-mutating: just load inner and call
|
|
1889
|
+
return block64(...loadInner, asF64(result))
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// 6. valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
|
|
1895
|
+
// property shadows. An assigned `obj.valueOf`/`obj.toString` must win over
|
|
1896
|
+
// the builtin emitter for any receiver that can carry a dynamic-prop
|
|
1897
|
+
// sidecar — a sidecar-bearing static type (array/typed/object) OR a
|
|
1898
|
+
// statically-unknown receiver (e.g. an array-element read `arr[0]`, whose
|
|
1899
|
+
// type is only known at runtime). Probe the sidecar and call it when it
|
|
1900
|
+
// holds a closure, else fall back to the builtin (generic when untyped:
|
|
1901
|
+
// `.valueOf` returns the receiver, `.toString` runs type-aware __to_str).
|
|
1902
|
+
// Parallels the member-READ check in module/core.js emitPropAccess (which
|
|
1903
|
+
// stays scoped to known sidecar types). (watr's `str()` attaches
|
|
1904
|
+
// `bytes.valueOf = () => s`, recovered via `.valueOf()`.)
|
|
1905
|
+
function trySidecarToPrimitive({ obj, method, parsed, vt, callMethod }) {
|
|
1906
|
+
if ((method === 'valueOf' || method === 'toString') && ctx.closure.call
|
|
1907
|
+
&& !parsed.hasSpread && parsed.normal.length === 0
|
|
1908
|
+
&& (vt === VAL.ARRAY || vt === VAL.TYPED || vt === VAL.OBJECT || !vt)) {
|
|
1909
|
+
const builtin = (vt && ctx.core.emit[`.${vt}:${method}`]) || ctx.core.emit[`.${method}`]
|
|
1910
|
+
if (builtin) {
|
|
1911
|
+
return sidecarOverride(emit(obj), asI64(emit(['str', method])),
|
|
1912
|
+
(p) => ctx.closure.call(typed(['local.get', `$${p}`], 'f64'), []), // CALL the override
|
|
1913
|
+
(o) => asF64(callMethod(o, builtin))) // else the builtin method
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// 7. Known type → static dispatch
|
|
1919
|
+
function tryStaticDispatch({ obj, method, vt, callMethod }) {
|
|
1920
|
+
if (vt && ctx.core.emit[`.${vt}:${method}`]) {
|
|
1921
|
+
return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// 8. Unknown / guessed-array type, both string + generic exist → runtime dispatch by ptr type.
|
|
1926
|
+
// analyze.js defaults untyped `.slice()` results to VAL.ARRAY, which is a guess, not a proof;
|
|
1927
|
+
// runtime dispatch resolves whether the operand is actually a string or an array.
|
|
1928
|
+
// Concretely-typed non-string values (BUFFER, TYPED, MAP, …) fall through to the generic
|
|
1929
|
+
// emitter which already knows how to handle them.
|
|
1930
|
+
function tryRuntimeStringFork({ obj, method, vt, callMethod }) {
|
|
1931
|
+
const strKey = `.string:${method}`, genKey = `.${method}`
|
|
1932
|
+
// VAL.ARRAY is structurally incompatible with PTR.STRING — no fork needed.
|
|
1933
|
+
// Only fork when vt is truly unknown (!vt), not for proven types.
|
|
1934
|
+
if (!vt && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
|
|
1935
|
+
const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
|
|
1936
|
+
ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
|
|
1937
|
+
const strEmitter = ctx.core.emit[strKey]
|
|
1938
|
+
const genEmitter = ctx.core.emit[genKey]
|
|
1939
|
+
// A string/array method is only valid on a NaN-boxed pointer (string/array/…).
|
|
1940
|
+
// `f64.eq(t,t)` is true only for a non-NaN value, so guard the dispatch with
|
|
1941
|
+
// it. A plain-number receiver dispatches the `.number:` emitter when the
|
|
1942
|
+
// method has one (`x.toString(16)` on an untyped x — the kernel-L2 ratchet's
|
|
1943
|
+
// data-segment corruption root: this used to yield `undefined`, and
|
|
1944
|
+
// `'\\' + undefined.padStart(2,'0')` collapsed every escaped byte to \\00);
|
|
1945
|
+
// methods numbers don't have keep yielding `undefined` (spec: `(5).indexOf`
|
|
1946
|
+
// is undefined) instead of feeding number bits to `__ptr_type` → OOB.
|
|
1947
|
+
// Every NaN-boxed receiver still reaches the string-vs-generic fork unchanged.
|
|
1948
|
+
const numEmitter = ctx.core.emit[`.number:${method}`]
|
|
1949
|
+
return block64(
|
|
1950
|
+
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1951
|
+
['if', ['result', 'f64'],
|
|
1952
|
+
['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
|
|
1953
|
+
['then', numEmitter ? asF64(callMethod(t, numEmitter)) : undefExpr()],
|
|
1954
|
+
['else', block64(
|
|
1955
|
+
['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1956
|
+
['if', ['result', 'f64'],
|
|
1957
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
|
|
1958
|
+
['then', callMethod(t, strEmitter)],
|
|
1959
|
+
['else', callMethod(t, genEmitter)]])]])
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// 8b. Number-only method (toFixed/toPrecision/toExponential/toString-with-radix
|
|
1964
|
+
// when no string fork applies) on an untyped receiver: a runtime number check
|
|
1965
|
+
// routes to the `.number:` emitter; a NaN-boxed receiver probes the dynamic-prop
|
|
1966
|
+
// sidecar (a user's own `.toFixed` closure must win — ES own-property shadowing)
|
|
1967
|
+
// and otherwise yields `undefined`, the same result the dynamic path produced.
|
|
1968
|
+
function tryRuntimeNumberMethod({ obj, method, parsed, vt, callMethod }) {
|
|
1969
|
+
const numEmitter = ctx.core.emit[`.number:${method}`]
|
|
1970
|
+
if (vt || !numEmitter || parsed.hasSpread || !ctx.closure.call) return
|
|
1971
|
+
const t = `${T}rn${ctx.func.uniq++}`
|
|
1972
|
+
ctx.func.locals.set(t, 'f64')
|
|
1973
|
+
return block64(
|
|
1974
|
+
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1975
|
+
['if', ['result', 'f64'],
|
|
1976
|
+
['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
|
|
1977
|
+
['then', asF64(callMethod(t, numEmitter))],
|
|
1978
|
+
['else', sidecarOverride(typed(['local.get', `$${t}`], 'f64'), asI64(emit(['str', method])),
|
|
1979
|
+
(p) => ctx.closure.call(typed(['local.get', `$${p}`], 'f64'), parsed.normal),
|
|
1980
|
+
() => undefExpr())]])
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// 9. Schema property closure call: `x.prop(args)` where prop is a closure slot in
|
|
1984
|
+
// x's schema. Boxed schemas don't currently support spread callers (each box
|
|
1985
|
+
// hands the inner value through), so spread is restricted to the non-boxed path.
|
|
1986
|
+
function trySchemaClosureCall({ obj, method, parsed }) {
|
|
1987
|
+
if (typeof obj === 'string' && ctx.schema.slotOf && ctx.closure.call) {
|
|
1988
|
+
const idx = ctx.schema.slotOf(obj, method)
|
|
1989
|
+
if (idx >= 0) {
|
|
1990
|
+
const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
|
|
1991
|
+
if (parsed.hasSpread && !ctx.schema.isBoxed?.(obj)) {
|
|
1992
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1993
|
+
return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
|
|
1994
|
+
}
|
|
1995
|
+
return ctx.closure.call(propRead, parsed.normal)
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// 10. Generic only — but a collection emitter (`.get`/`.set`/`.has`/`.add`/
|
|
2001
|
+
// `.delete`) assumes a Map/Set receiver: a proven collection already
|
|
2002
|
+
// dispatched via `.${vt}:${method}` above, so reaching here means the
|
|
2003
|
+
// receiver is not a proven collection. A zero-arg call then cannot be the
|
|
2004
|
+
// collection op (each needs ≥1 key/value arg) — it is a user/closure
|
|
2005
|
+
// method (e.g. `new C().get()`). Skip the collection emitter so it falls
|
|
2006
|
+
// through to closure/dynamic dispatch instead of crashing on `emit(key)`.
|
|
2007
|
+
function tryGenericEmitter({ obj, method, parsed, vt, callMethod }) {
|
|
2008
|
+
const collectionMisfit = COLLECTION_METHODS.has(method) &&
|
|
2009
|
+
!parsed.hasSpread && parsed.normal.length === 0
|
|
2010
|
+
const strIndexMisfit = STR_INDEX_METHODS.has(method) &&
|
|
2011
|
+
!parsed.hasSpread && parsed.normal.length > 1
|
|
2012
|
+
// A proven plain-object/dict receiver never inherits the Array/collection
|
|
2013
|
+
// builtins these generic emitters serve — an own property of the same name
|
|
2014
|
+
// shadows them (ES prototype semantics). Skip the builtin so the dynamic
|
|
2015
|
+
// property-call dispatch below reads the actual slot/sidecar closure. This
|
|
2016
|
+
// is the type-based generalization of the collection/strIndex arity guards
|
|
2017
|
+
// above: it is what lets self-host user methods whose names collide with
|
|
2018
|
+
// builtins — `ctx.schema.slotOf(o,p)`, `node.map(...)`, `s.get(k)` — dispatch
|
|
2019
|
+
// correctly instead of being hijacked by `Array.prototype.{find,map,…}`.
|
|
2020
|
+
const objectShadow = vt === VAL.OBJECT || vt === VAL.HASH
|
|
2021
|
+
if (ctx.core.emit[`.${method}`] && !collectionMisfit && !strIndexMisfit && !objectShadow) {
|
|
2022
|
+
return callMethod(obj, ctx.core.emit[`.${method}`])
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// 11. Dynamic property function call on non-external values. Two emission shapes:
|
|
2027
|
+
// (1) closure-only fork — receiver carries no PTR.EXTERNAL (sidecar-bearing static
|
|
2028
|
+
// types OR wasi target, where __ext_call doesn't exist); and (2) full fork
|
|
2029
|
+
// adding a PTR.EXTERNAL → __ext_call leg for opaque js receivers.
|
|
2030
|
+
function tryDynamicPropCall({ obj, method, parsed, vt }) {
|
|
2031
|
+
if (ctx.closure.call) {
|
|
2032
|
+
if (ctx.transform.strict)
|
|
2033
|
+
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 }.`)
|
|
2034
|
+
const objTmp = temp('mobj')
|
|
2035
|
+
const propTmp = temp('mprop')
|
|
2036
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2037
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
2038
|
+
const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
|
|
2039
|
+
const closureOnly = usesDynProps(vt) || ctx.transform.host === 'wasi'
|
|
2040
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2041
|
+
if (!closureOnly) { inc('__ext_call'); ctx.features.external = true }
|
|
2042
|
+
const extFallback = closureOnly ? undefExpr()
|
|
2043
|
+
: ['if', ['result', 'f64'],
|
|
2044
|
+
ptrTypeEq(['local.get', `$${objTmp}`], PTR.EXTERNAL),
|
|
2045
|
+
['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2046
|
+
['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
|
|
2047
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2048
|
+
['i64.reinterpret_f64', arrayIR]]]],
|
|
2049
|
+
['else', undefExpr()]]
|
|
2050
|
+
return block64(
|
|
2051
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
2052
|
+
['local.set', `$${propTmp}`, propRead],
|
|
2053
|
+
['if', ['result', 'f64'],
|
|
2054
|
+
ptrTypeEq(['local.get', `$${propTmp}`], PTR.CLOSURE),
|
|
2055
|
+
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
2056
|
+
['else', extFallback]])
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// 12. Unknown callee — assume external method. Total: always returns.
|
|
2061
|
+
function externalMethodFallback({ obj, method, parsed }) {
|
|
2062
|
+
if (ctx.transform.strict)
|
|
2063
|
+
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 }.`)
|
|
2064
|
+
// Under wasi there is no host `__ext_call` — the call lowers to a
|
|
2065
|
+
// no-op returning `undefined`. This is by-design so polymorphic code
|
|
2066
|
+
// can target js and wasi from one source; users who want fail-fast
|
|
2067
|
+
// pass `strict: true` (handled above).
|
|
2068
|
+
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
2069
|
+
warnDeopt('deopt-method', `method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(…)\` on a value whose type couldn't be resolved dispatches through the JS host (\`__ext_call\`) — a wasm→JS round-trip per call, orders of magnitude slower than a direct call. Restructure so the receiver's type is provable, or keep it off the hot path.`)
|
|
2070
|
+
inc('__ext_call')
|
|
2071
|
+
ctx.features.external = true
|
|
2072
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2073
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
2074
|
+
return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2075
|
+
['i64.reinterpret_f64', asF64(emit(obj))],
|
|
2076
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2077
|
+
['i64.reinterpret_f64', arrayIR]]], 'f64')
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const TYPED_STRATEGIES = [
|
|
2081
|
+
tryBoxedDelegate, trySidecarToPrimitive, tryStaticDispatch, tryRuntimeStringFork,
|
|
2082
|
+
tryRuntimeNumberMethod, trySchemaClosureCall, tryGenericEmitter, tryDynamicPropCall,
|
|
2083
|
+
externalMethodFallback,
|
|
2084
|
+
]
|
|
2085
|
+
|
|
2086
|
+
/** Method-call dispatch: `obj.method(args)`. Linear strategy chain, first
|
|
2087
|
+
* match wins. 1–4 are context-free (LEADING_STRATEGIES); 5–12 share the
|
|
2088
|
+
* resolved receiver type + callMethod shim via the dispatch-context record
|
|
2089
|
+
* (TYPED_STRATEGIES — the last entry is total):
|
|
2090
|
+
* 1. SRoA flat-object method (read closure from `o#i` local)
|
|
2091
|
+
* 2. charCodeAt with statically-proven in-bounds index → i32 fast path
|
|
2092
|
+
* 3. splice with insert items → __arr_splice (the one method that delete+insert)
|
|
2093
|
+
* 4. fn.prop direct call to fn$prop (skipped for reassigned wrapper-composition)
|
|
2094
|
+
* 5. Boxed-schema receiver → delegate to inner value at slot 0 (+ writeback)
|
|
2095
|
+
* 6. valueOf / toString — sidecar own-property shadow check
|
|
2096
|
+
* 7. Known-type static dispatch via .${vt}:${method}
|
|
2097
|
+
* 8. Unknown / guessed-ARRAY runtime ptr-type fork over string vs generic
|
|
2098
|
+
* 9. Schema property closure call
|
|
2099
|
+
* 10. Generic emitter (with collection/strIndex arity guards + object shadow)
|
|
2100
|
+
* 11. Dynamic property closure call (with PTR.EXTERNAL fallback if non-wasi)
|
|
2101
|
+
* 12. External method fallback via __ext_call (or undefined under wasi)
|
|
2102
|
+
*/
|
|
2103
|
+
function emitMethodCall(callee, parsed, callArgs) {
|
|
2104
|
+
const [, obj, method] = callee
|
|
2105
|
+
|
|
2106
|
+
// Strategies 1–4 (context-free, order-sensitive, first match wins).
|
|
2107
|
+
for (const strategy of LEADING_STRATEGIES) {
|
|
2108
|
+
const r = strategy(callee, obj, method, parsed)
|
|
2109
|
+
if (r !== undefined) return r
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
let vt = valTypeOf(obj)
|
|
2113
|
+
// A reassigned slice/concat receiver may carry a stale `vt` — a reassignment
|
|
2114
|
+
// inside a nested closure escapes analyzeValTypes' poisoning (its walk stops
|
|
2115
|
+
// at `=>`). Drop to runtime dispatch, but only for guessy types: STRING/ARRAY
|
|
2116
|
+
// dispatch correctly either way, and BUFFER/TYPED are construction proofs
|
|
2117
|
+
// (`new ArrayBuffer`/`new XxxArray`) — the runtime String/Array fallback has
|
|
2118
|
+
// no branch for them, so nulling `vt` would miscompile `ab.slice()` into an
|
|
2119
|
+
// f64-array copy. jzify also splits every `var x = init` into `let x; x = init`,
|
|
2120
|
+
// marking single-assignment vars "reassigned"; keeping definite BUFFER/TYPED
|
|
2121
|
+
// is what keeps `var`-declared buffers correct.
|
|
2122
|
+
if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
|
|
2123
|
+
&& (method === 'slice' || method === 'concat')
|
|
2124
|
+
&& vt !== VAL.STRING && vt !== VAL.ARRAY
|
|
2125
|
+
&& vt !== VAL.BUFFER && vt !== VAL.TYPED) vt = null
|
|
2126
|
+
|
|
2127
|
+
// Method-emitter shim — threads parsed/method through the shared dispatcher so
|
|
2128
|
+
// strategies keep the simple `callMethod(receiver, emitter)` shape.
|
|
2129
|
+
const c = {
|
|
2130
|
+
obj, method, parsed, vt,
|
|
2131
|
+
callMethod: (objArg, methodEmitter) => emitMethodCallSpread(objArg, methodEmitter, parsed, method),
|
|
2132
|
+
}
|
|
2133
|
+
for (const strategy of TYPED_STRATEGIES) {
|
|
2134
|
+
const r = strategy(c)
|
|
2135
|
+
if (r !== undefined) return r
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/** Builtin / module-emitter call: `Math.max(...)`, `JSON.parse(...)`, etc. The
|
|
2140
|
+
* emitter accepts the same `...args` flat shape as the AST (with `['...', x]`
|
|
2141
|
+
* spread markers re-inserted in original position). */
|
|
2142
|
+
function emitBuiltinCall(callee, parsed) {
|
|
2143
|
+
if (parsed.hasSpread) {
|
|
2144
|
+
const allArgs = []
|
|
2145
|
+
let ni = 0
|
|
2146
|
+
for (const s of parsed.spreads) {
|
|
2147
|
+
while (ni < s.pos) allArgs.push(parsed.normal[ni++])
|
|
2148
|
+
allArgs.push(['...', s.expr])
|
|
2149
|
+
}
|
|
2150
|
+
while (ni < parsed.normal.length) allArgs.push(parsed.normal[ni++])
|
|
2151
|
+
return ctx.core.emit[callee](...allArgs)
|
|
2152
|
+
}
|
|
2153
|
+
return ctx.core.emit[callee](...parsed.normal)
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/** Direct call to a known top-level user function — emits `(call $callee args)`.
|
|
2157
|
+
* Handles rest params (collect into trailing array), in-spread fixed params
|
|
2158
|
+
* (runtime split), default-param padding, multi-value return materialization. */
|
|
2159
|
+
function emitDirectFunctionCall(callee, parsed, callArgs) {
|
|
2160
|
+
const func = ctx.func.map.get(callee)
|
|
2161
|
+
|
|
2162
|
+
// Rest param case: collect all args (including expanded spreads) into array
|
|
2163
|
+
if (func?.rest) {
|
|
2164
|
+
const fixedParamCount = func.sig.params.length - 1
|
|
2165
|
+
// A spread positioned within the fixed-param range supplies fixed params from
|
|
2166
|
+
// inside the spread — they can't be sliced out statically. Build the full args
|
|
2167
|
+
// array A and split it at runtime: fixed[k] = A[k], rest = A.slice(fixedParamCount).
|
|
2168
|
+
// (Otherwise the static slice below is exact and skips the extra alloc + copy.)
|
|
2169
|
+
if (fixedParamCount > 0 && parsed.spreads.some(s => s.pos < fixedParamCount)) {
|
|
2170
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2171
|
+
const aVal = temp('ra'), aOff = tempI32('rao'), aLen = tempI32('ral'), rLen = tempI32('rln')
|
|
2172
|
+
const rest = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${rLen}`], tag: 'rr' })
|
|
2173
|
+
const fixedLoads = []
|
|
2174
|
+
for (let k = 0; k < fixedParamCount; k++) {
|
|
2175
|
+
const load = typed(['if', ['result', 'f64'],
|
|
2176
|
+
['i32.gt_s', ['local.get', `$${aLen}`], ['i32.const', k]],
|
|
2177
|
+
['then', ['f64.load', ['i32.add', ['local.get', `$${aOff}`], ['i32.const', k * 8]]]],
|
|
2178
|
+
['else', undefExpr()]], 'f64')
|
|
2179
|
+
fixedLoads.push(coerceArg(load, func.sig.params[k]))
|
|
2180
|
+
}
|
|
2181
|
+
const callIR = typed(['block', ['result', func.sig.results[0]],
|
|
2182
|
+
['local.set', `$${aVal}`, asF64(buildArrayWithSpreads(combined))],
|
|
2183
|
+
['local.set', `$${aOff}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${aVal}`]]]],
|
|
2184
|
+
['local.set', `$${aLen}`, ['i32.load', ['i32.sub', ['local.get', `$${aOff}`], ['i32.const', 8]]]],
|
|
2185
|
+
['local.set', `$${rLen}`, ['select',
|
|
2186
|
+
['i32.sub', ['local.get', `$${aLen}`], ['i32.const', fixedParamCount]],
|
|
2187
|
+
['i32.const', 0],
|
|
2188
|
+
['i32.gt_s', ['local.get', `$${aLen}`], ['i32.const', fixedParamCount]]]],
|
|
2189
|
+
rest.init,
|
|
2190
|
+
['memory.copy', ['local.get', `$${rest.local}`],
|
|
2191
|
+
['i32.add', ['local.get', `$${aOff}`], ['i32.const', fixedParamCount * 8]],
|
|
2192
|
+
['i32.shl', ['local.get', `$${rLen}`], ['i32.const', 3]]],
|
|
2193
|
+
['call', `$${callee}`, ...fixedLoads, rest.ptr]], func.sig.results[0])
|
|
2194
|
+
return attachSigMeta(callIR, func.sig)
|
|
2195
|
+
}
|
|
2196
|
+
// Pad missing fixed args with `undefined` so default-param init triggers per spec.
|
|
2197
|
+
const fixedParams = func.sig.params.slice(0, fixedParamCount)
|
|
2198
|
+
const emittedFixed = emitCallArgs(parsed.normal.slice(0, fixedParamCount), fixedParams)
|
|
2199
|
+
|
|
2200
|
+
// Reconstruct with spreads, then take rest args
|
|
2201
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2202
|
+
const restArgsFinal = combined.slice(fixedParamCount)
|
|
2203
|
+
|
|
2204
|
+
// Build array: emit code for normal args + code to expand spreads
|
|
2205
|
+
const arrayIR = buildArrayWithSpreads(restArgsFinal)
|
|
2206
|
+
return attachSigMeta(typed(['call', `$${callee}`, ...emittedFixed, arrayIR], func.sig.results[0]), func.sig)
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// Regular function call without rest params
|
|
2210
|
+
if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
|
|
2211
|
+
// Pad missing args with `undefined` so default-param init triggers per spec
|
|
2212
|
+
// (only undefined, not null, should trigger defaults). Drop extras to match
|
|
2213
|
+
// JS calling convention — emitting them anyway produces an invalid call
|
|
2214
|
+
// when the callee is a fixed-arity import (e.g. `_interp`-registered host
|
|
2215
|
+
// stubs) since wasm validates arg count. Use ?? rather than || so a
|
|
2216
|
+
// legitimate 0-arity callee isn't bypassed.
|
|
2217
|
+
const params = func?.sig.params ?? []
|
|
2218
|
+
const args = func ? emitCallArgs(parsed.normal, params)
|
|
2219
|
+
: parsed.normal.map(a => coerceArg(emit(a), undefined))
|
|
2220
|
+
if (func && args.length > params.length) args.length = params.length
|
|
2221
|
+
// Multi-value return: materialize as heap array (caller expects single pointer).
|
|
2222
|
+
// Reuse the canonical comma-wrapped arg slot — materializeMulti re-reads args
|
|
2223
|
+
// via commaList(node[2]); a spread-form `[…, ...parsed.normal]` would drop every
|
|
2224
|
+
// argument past the first.
|
|
2225
|
+
if (func?.sig.results.length > 1) return materializeMulti(['()', callee, callArgs])
|
|
2226
|
+
// attachSigMeta also handles the unsigned-uint32 flag (every tail was `>>>`),
|
|
2227
|
+
// so consumer's asF64 uses `f64.convert_i32_u` instead of `_s` ([0, 2^32) range).
|
|
2228
|
+
const callIR = attachSigMeta(typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64'), func?.sig)
|
|
2229
|
+
return callIR
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
/** Const-bound, non-escaping closure — direct call to its body, skipping
|
|
2233
|
+
* call_indirect. emitDecl registered name→bodyName when it saw the closure.make
|
|
2234
|
+
* IR. Returns null if arity exceeds the closure-table slot width (caller falls
|
|
2235
|
+
* through to the generic closure path). */
|
|
2236
|
+
function tryDirectClosureCall(callee, parsed) {
|
|
2237
|
+
const bodyName = ctx.func.directClosures.get(callee)
|
|
2238
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
2239
|
+
const n = parsed.normal.length
|
|
2240
|
+
if (n > W) return null
|
|
2241
|
+
// Per-param "every direct call site passed a number" lattice. Every call to a
|
|
2242
|
+
// direct (non-escaping) closure flows through here, so once the body is emitted
|
|
2243
|
+
// (module end, after all calls) a param only ever seen with numeric args is marked
|
|
2244
|
+
// VAL.NUMBER — its body uses then skip __to_num, the same boxing win the numeric
|
|
2245
|
+
// export-param path gives. An arg we can't prove numeric poisons the slot to false.
|
|
2246
|
+
const pt = (ctx.closure.paramTypes ||= new Map())
|
|
2247
|
+
let row = pt.get(bodyName); if (!row) pt.set(bodyName, row = [])
|
|
2248
|
+
// Parallel typed-array ctor lattice: a param passed the SAME typed-array ctor at
|
|
2249
|
+
// every direct call site is a TYPED param, so its body reads (`buf[i]`) take the
|
|
2250
|
+
// typed fast-path instead of the dynamic `__typed_idx`/`__len` route that drags in
|
|
2251
|
+
// the string runtime. `null` (sticky) once two sites disagree or an arg isn't a
|
|
2252
|
+
// known typed array — the same monotone meet as the numeric row. Mirrors the named-fn
|
|
2253
|
+
// applyTypedPointerParamAbi, restricted to non-escaping (directly-called) closures.
|
|
2254
|
+
const tc = (ctx.closure.paramTypedCtors ||= new Map())
|
|
2255
|
+
let tcRow = tc.get(bodyName); if (!tcRow) tc.set(bodyName, tcRow = [])
|
|
2256
|
+
for (let i = 0; i < n; i++) {
|
|
2257
|
+
const numeric = valTypeOf(parsed.normal[i]) === VAL.NUMBER
|
|
2258
|
+
row[i] = row[i] === undefined ? numeric : (row[i] && numeric)
|
|
2259
|
+
const arg = parsed.normal[i]
|
|
2260
|
+
const ctor = typeof arg === 'string' && valTypeOf(arg) === VAL.TYPED ? (ctx.types.typedElem?.get(arg) ?? null) : null
|
|
2261
|
+
if (tcRow[i] === undefined) tcRow[i] = ctor
|
|
2262
|
+
else if (tcRow[i] !== ctor) tcRow[i] = null
|
|
2263
|
+
}
|
|
2264
|
+
// Track the fewest args any call passed: a slot at index ≥ minArgc is omitted by some call
|
|
2265
|
+
// site (padded with UNDEF_NAN), so it may be undefined — emitClosureBody flags it nullable.
|
|
2266
|
+
const mn = (ctx.closure.minArgc ||= new Map())
|
|
2267
|
+
const prev = mn.get(bodyName)
|
|
2268
|
+
mn.set(bodyName, prev === undefined ? n : (n < prev ? n : prev))
|
|
2269
|
+
// Body signature is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64.
|
|
2270
|
+
// We pass the closure NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
|
|
2271
|
+
const slots = parsed.normal.map(a => asF64(emit(a)))
|
|
2272
|
+
while (slots.length < W) slots.push(undefExpr())
|
|
2273
|
+
return typed(['call', `$${bodyName}`,
|
|
2274
|
+
asF64(emit(callee)),
|
|
2275
|
+
typed(['i32.const', n], 'i32'),
|
|
2276
|
+
...slots], 'f64')
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/** Generic closure call: callee is a value holding a NaN-boxed closure pointer.
|
|
2280
|
+
* Uniform convention: fn.call packs all args into an array and trampolines. */
|
|
2281
|
+
function emitGenericClosureCall(callee, parsed) {
|
|
2282
|
+
if (parsed.hasSpread) {
|
|
2283
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2284
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
2285
|
+
// Pass pre-built array as single already-emitted arg
|
|
2286
|
+
return ctx.closure.call(emit(callee), [arrayIR], true)
|
|
2287
|
+
}
|
|
2288
|
+
return ctx.closure.call(emit(callee), parsed.normal)
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/** Last-resort fallback: assume `(call $callee args)` against an import / unknown
|
|
2292
|
+
* identifier. Matches arg count to the env-import signature when known — wasm
|
|
2293
|
+
* validates arity strictly, so JS-style "pad missing / drop extra" needs to be
|
|
2294
|
+
* done here rather than by the host. */
|
|
2295
|
+
function emitUnknownCalleeCall(callee, argList) {
|
|
2296
|
+
let calleeArity = null
|
|
2297
|
+
if (typeof callee === 'string') {
|
|
2298
|
+
const imp = ctx.module.imports?.find(i =>
|
|
2299
|
+
Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
|
|
2300
|
+
if (imp) {
|
|
2301
|
+
let n = 0
|
|
2302
|
+
for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
|
|
2303
|
+
calleeArity = n
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const emittedArgs = argList.map(a => asF64(emit(a)))
|
|
2307
|
+
if (calleeArity != null) {
|
|
2308
|
+
while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
|
|
2309
|
+
if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
|
|
2310
|
+
}
|
|
2311
|
+
return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
/** Compound assignment: read → op → write back (via readVar/writeVar). */
|
|
2315
|
+
function compoundAssign(name, val, f64op, i32op) {
|
|
2316
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
2317
|
+
const void_ = ctx.func._expect === 'void'
|
|
2318
|
+
const va = readVar(name), vb = emit(val)
|
|
2319
|
+
// Peel f64.convert_i32_s/u when va is i32 — typed-array integer reads wrap their
|
|
2320
|
+
// i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
|
|
2321
|
+
// raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
|
|
2322
|
+
// hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
|
|
2323
|
+
let vbi = vb
|
|
2324
|
+
if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
|
|
2325
|
+
Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
|
|
2326
|
+
const inner = vb[1]
|
|
2327
|
+
vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
|
|
2328
|
+
}
|
|
2329
|
+
if (i32op && va.type === 'i32' && vbi.type === 'i32')
|
|
2330
|
+
return writeVar(name, i32op(va, vbi), void_)
|
|
2331
|
+
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// === Core emitter dispatch table ===
|
|
2335
|
+
// ctx.core.emit is seeded with a flat copy of this object on reset;
|
|
2336
|
+
// language modules add or override ops on ctx.core.emit directly.
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* Core emitter table. Maps AST ops to WASM IR generators.
|
|
2340
|
+
* @type {Record<string, (...args: any[]) => Array>}
|
|
2341
|
+
*/
|
|
2342
|
+
export const emitter = {
|
|
2343
|
+
// === Spread operator ===
|
|
2344
|
+
// Note: spread is handled specially in call contexts; this catches stray uses
|
|
2345
|
+
'...': () => err('Spread (...) can only be used in function/method calls or array literals'),
|
|
2346
|
+
|
|
2347
|
+
// === Statements ===
|
|
2348
|
+
|
|
2349
|
+
';': (...args) => {
|
|
2350
|
+
const out = []
|
|
2351
|
+
for (const a of args) {
|
|
2352
|
+
out.push(...emitVoid(a))
|
|
2353
|
+
}
|
|
2354
|
+
return out
|
|
2355
|
+
},
|
|
2356
|
+
'{': (...args) => args.map(emit).filter(x => x != null),
|
|
2357
|
+
',': (...args) => {
|
|
2358
|
+
const results = args.map(emit).filter(x => x != null)
|
|
2359
|
+
if (results.length === 0) return null
|
|
2360
|
+
if (results.length === 1) return results[0]
|
|
2361
|
+
const last = results[results.length - 1]
|
|
2362
|
+
// Flatten: multi-instruction arrays (from ';') need spreading, typed nodes need drop
|
|
2363
|
+
const spread = r => Array.isArray(r) && Array.isArray(r[0]) ? r : [r]
|
|
2364
|
+
const dropSpread = r => r.type ? [['drop', r]] : spread(r)
|
|
2365
|
+
// If last expression is void (store, etc.), add explicit return value
|
|
2366
|
+
if (!last.type) {
|
|
2367
|
+
return block64(
|
|
2368
|
+
...results.flatMap(dropSpread),
|
|
2369
|
+
['f64.const', 0])
|
|
2370
|
+
}
|
|
2371
|
+
return typed(['block', ['result', last.type],
|
|
2372
|
+
...results.slice(0, -1).flatMap(dropSpread), last], last.type)
|
|
2373
|
+
},
|
|
2374
|
+
'let': emitDecl,
|
|
2375
|
+
'const': emitDecl,
|
|
2376
|
+
'export': () => null,
|
|
2377
|
+
// 'block' can appear from jzify transforming labeled blocks or as WASM block IR
|
|
2378
|
+
'block': (...args) => {
|
|
2379
|
+
// WASM block IR: first arg is ['result', type] → pass through, preserve type
|
|
2380
|
+
if (Array.isArray(args[0]) && args[0][0] === 'result')
|
|
2381
|
+
return typed(['block', ...args], args[0][1])
|
|
2382
|
+
const inner = args.length === 1 ? args[0] : [';', ...args]
|
|
2383
|
+
return emitVoid(['{}', inner])
|
|
2384
|
+
},
|
|
2385
|
+
|
|
2386
|
+
'throw': expr => {
|
|
2387
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
2388
|
+
const thrown = temp()
|
|
2389
|
+
return typed(['block',
|
|
2390
|
+
['local.set', `$${thrown}`, asF64(emit(expr))],
|
|
2391
|
+
['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${thrown}`]]],
|
|
2392
|
+
['throw', '$__jz_err', ['local.get', `$${thrown}`]]], 'void')
|
|
2393
|
+
},
|
|
2394
|
+
|
|
2395
|
+
'catch': (body, errName, handler) => {
|
|
2396
|
+
if (!canThrow(body)) return emitVoid(body)
|
|
2397
|
+
|
|
2398
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
2399
|
+
const id = ctx.func.uniq++
|
|
2400
|
+
ctx.func.locals.set(errName, 'f64')
|
|
2401
|
+
const prev = ctx.func.inTry; ctx.func.inTry = true
|
|
2402
|
+
let bodyIR; try { bodyIR = emitVoid(body) } finally { ctx.func.inTry = prev }
|
|
2403
|
+
const handlerIR = emitVoid(handler)
|
|
2404
|
+
return typed(['block', `$outer${id}`, ['result', 'f64'],
|
|
2405
|
+
['block', `$catch${id}`, ['result', 'f64'],
|
|
2406
|
+
['try_table', ['catch', '$__jz_err', `$catch${id}`],
|
|
2407
|
+
...bodyIR],
|
|
2408
|
+
['f64.const', 0],
|
|
2409
|
+
['br', `$outer${id}`]],
|
|
2410
|
+
['local.set', `$${errName}`],
|
|
2411
|
+
...handlerIR,
|
|
2412
|
+
['f64.const', 0]], 'f64')
|
|
2413
|
+
},
|
|
2414
|
+
|
|
2415
|
+
'finally': (body, cleanup) => {
|
|
2416
|
+
if (!canThrow(body)) {
|
|
2417
|
+
const parentStack = ctx.func.finallyStack || []
|
|
2418
|
+
const activeStack = parentStack.concat([cleanup])
|
|
2419
|
+
const bodyIR = withFinallyStack(activeStack, () => emitVoid(body))
|
|
2420
|
+
const cleanupIR = isTerminator(body) ? [] : withFinallyStack(parentStack, () => emitVoid(cleanup))
|
|
2421
|
+
return [...bodyIR, ...cleanupIR]
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
2425
|
+
const id = ctx.func.uniq++
|
|
2426
|
+
const errLocal = temp('err')
|
|
2427
|
+
const parentStack = ctx.func.finallyStack || []
|
|
2428
|
+
const activeStack = parentStack.concat([cleanup])
|
|
2429
|
+
|
|
2430
|
+
const prevTry = ctx.func.inTry
|
|
2431
|
+
ctx.func.inTry = true
|
|
2432
|
+
const bodyIR = withFinallyStack(activeStack, () => {
|
|
2433
|
+
try { return emitVoid(body) }
|
|
2434
|
+
finally { ctx.func.inTry = prevTry }
|
|
2435
|
+
})
|
|
2436
|
+
const normalCleanup = withFinallyStack(parentStack, () => emitVoid(cleanup))
|
|
2437
|
+
const throwCleanup = withFinallyStack(parentStack, () => emitVoid(cleanup))
|
|
2438
|
+
|
|
2439
|
+
return ['block', `$fin_done${id}`,
|
|
2440
|
+
['block', `$fin_catch${id}`, ['result', 'f64'],
|
|
2441
|
+
['try_table', ['catch', '$__jz_err', `$fin_catch${id}`],
|
|
2442
|
+
...bodyIR],
|
|
2443
|
+
...normalCleanup,
|
|
2444
|
+
['br', `$fin_done${id}`]],
|
|
2445
|
+
['local.set', `$${errLocal}`],
|
|
2446
|
+
...throwCleanup,
|
|
2447
|
+
['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${errLocal}`]]],
|
|
2448
|
+
['throw', '$__jz_err', ['local.get', `$${errLocal}`]]]
|
|
2449
|
+
},
|
|
2450
|
+
|
|
2451
|
+
'return': expr => {
|
|
2452
|
+
const finalizers = emitFinalizers()
|
|
2453
|
+
const finalizerBlock = () => [['block', ...finalizers]]
|
|
2454
|
+
if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
|
|
2455
|
+
const vals = expr.slice(1).map(e => asF64(emit(e)))
|
|
2456
|
+
if (finalizers.length === 0) return typed(['return', ...vals], 'void')
|
|
2457
|
+
const names = vals.map(() => temp('ret'))
|
|
2458
|
+
return [
|
|
2459
|
+
...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
|
|
2460
|
+
...finalizerBlock(),
|
|
2461
|
+
typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
|
|
2462
|
+
]
|
|
2463
|
+
}
|
|
2464
|
+
// A value-less `return;` yields `undefined` per spec (not null). The function
|
|
2465
|
+
// result is never i32-narrowed when a bare return is present (see hasBareReturn
|
|
2466
|
+
// guard in narrowI32Results), so the f64 UNDEF carrier is type-compatible.
|
|
2467
|
+
if (expr == null) return [...finalizers, typed(['return', undefExpr()], 'void')]
|
|
2468
|
+
const rt = ctx.func.current?.results[0] || 'f64'
|
|
2469
|
+
const pk = ctx.func.current?.ptrKind
|
|
2470
|
+
const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
|
|
2471
|
+
const ty = pk != null ? 'i32' : rt
|
|
2472
|
+
const tcoed = tcoTailRewrite(ir, ty)
|
|
2473
|
+
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
2474
|
+
return typed(tcoed, 'void')
|
|
2475
|
+
}
|
|
2476
|
+
if (finalizers.length > 0) {
|
|
2477
|
+
const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
|
|
2478
|
+
return [
|
|
2479
|
+
['local.set', `$${name}`, tcoed],
|
|
2480
|
+
...finalizerBlock(),
|
|
2481
|
+
typed(['return', ['local.get', `$${name}`]], 'void'),
|
|
2482
|
+
]
|
|
2483
|
+
}
|
|
2484
|
+
return typed(['return', tcoed], 'void')
|
|
2485
|
+
},
|
|
2486
|
+
|
|
2487
|
+
// === Assignment ===
|
|
2488
|
+
|
|
2489
|
+
'=': (name, val) => {
|
|
2490
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
2491
|
+
if (Array.isArray(name) && name[0] === '[]') return emitElementAssign(name[1], name[2], val)
|
|
2492
|
+
if (Array.isArray(name) && name[0] === '.') return emitPropertyAssign(name[1], name[2], val)
|
|
2493
|
+
if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
|
|
2494
|
+
if (isNullishLit(val)) ctx.func.maybeNullish?.add(name) // null-flow: later arithmetic on this var coerces
|
|
2495
|
+
const void_ = ctx.func._expect === 'void'
|
|
2496
|
+
if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
|
|
2497
|
+
inc('__to_num')
|
|
2498
|
+
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
2499
|
+
}
|
|
2500
|
+
return writeVar(name, emit(val), void_)
|
|
2501
|
+
},
|
|
2502
|
+
|
|
2503
|
+
// Compound assignments: read-modify-write with type coercion
|
|
2504
|
+
'+=': (name, val) => {
|
|
2505
|
+
// Complex LHS (obj.prop, arr[i]) → desugar to side-effect-safe `name = name + val`
|
|
2506
|
+
if (typeof name !== 'string') return emit(['=', name, ['+', name, val]])
|
|
2507
|
+
// String concatenation: desugar to name = name + val (+ handler knows about strings).
|
|
2508
|
+
// Also desugar when either side has unknown type — the `+` operator picks runtime
|
|
2509
|
+
// string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
|
|
2510
|
+
// silently corrupt string concatenations through unknown-typed values.
|
|
2511
|
+
const vt = typeof name === 'string' ? valTypeOf(name) : null
|
|
2512
|
+
const vtB = valTypeOf(val)
|
|
2513
|
+
if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
|
|
2514
|
+
if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
|
|
2515
|
+
return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
|
|
2516
|
+
},
|
|
2517
|
+
...Object.fromEntries([
|
|
2518
|
+
['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
|
|
2519
|
+
].map(([op, fn]) => [op, (name, val) => {
|
|
2520
|
+
if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
|
|
2521
|
+
return compoundAssign(name, val,
|
|
2522
|
+
(a, b) => typed([`f64.${fn}`, a, b], 'f64'),
|
|
2523
|
+
fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
|
|
2524
|
+
)
|
|
2525
|
+
}])),
|
|
2526
|
+
'%=': (name, val) => {
|
|
2527
|
+
if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
|
|
2528
|
+
return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
|
|
2529
|
+
},
|
|
2530
|
+
|
|
2531
|
+
// Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
|
|
2532
|
+
...Object.fromEntries([
|
|
2533
|
+
['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
|
|
2534
|
+
['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
|
|
2535
|
+
].map(([op, fn]) => [op, (name, val) => {
|
|
2536
|
+
if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
|
|
2537
|
+
if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
|
|
2538
|
+
const void_ = ctx.func._expect === 'void'
|
|
2539
|
+
const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
|
|
2540
|
+
return writeVar(name, result, void_)
|
|
2541
|
+
}
|
|
2542
|
+
return compoundAssign(name, val,
|
|
2543
|
+
(a, b) => asF64(typed([`i32.${fn}`, toI32(a), toI32(b)], 'i32')),
|
|
2544
|
+
(a, b) => typed([`i32.${fn}`, a, b], 'i32')
|
|
2545
|
+
)
|
|
2546
|
+
}])),
|
|
2547
|
+
|
|
2548
|
+
// Logical compound assignments: a ||= b → a = a || b, a &&= b → a = a && b
|
|
2549
|
+
// Logical/nullish compound assignments: read → check → conditionally write
|
|
2550
|
+
// For complex LHS (obj.prop, arr[i]): emit as check(read(lhs)) ? write(lhs, val) : read(lhs)
|
|
2551
|
+
...Object.fromEntries(['||=', '&&=', '??='].map(op => [op, (name, val) => {
|
|
2552
|
+
// Complex LHS → desugar (side-effect-safe since obj/arr/idx are locals)
|
|
2553
|
+
if (typeof name !== 'string') {
|
|
2554
|
+
const baseOp = op.slice(0, -1) // '||', '&&', '??'
|
|
2555
|
+
return emit([baseOp, name, ['=', name, val]])
|
|
2556
|
+
}
|
|
2557
|
+
if (isConst(name)) err(`Assignment to const '${name}'`)
|
|
2558
|
+
const void_ = ctx.func._expect === 'void'
|
|
2559
|
+
const t = temp()
|
|
2560
|
+
const va = readVar(name)
|
|
2561
|
+
// Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
|
|
2562
|
+
const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
2563
|
+
const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
|
|
2564
|
+
// &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
|
|
2565
|
+
const [thenExpr, elseExpr] = op === '||='
|
|
2566
|
+
? [['local.get', `$${t}`], asF64(emit(val))]
|
|
2567
|
+
: [asF64(emit(val)), ['local.get', `$${t}`]]
|
|
2568
|
+
const result = typed(['if', ['result', 'f64'], cond, ['then', thenExpr], ['else', elseExpr]], 'f64')
|
|
2569
|
+
// Write back — writeVar owns the cell/global/local discipline INCLUDING the
|
|
2570
|
+
// i32-narrowed-cell width (a direct f64.store here desynced narrowed cells).
|
|
2571
|
+
return writeVar(name, result, void_)
|
|
2572
|
+
}])),
|
|
2573
|
+
|
|
2574
|
+
// === Increment/Decrement ===
|
|
2575
|
+
// Postfix resolved in prepare: i++ → (++i) - 1
|
|
2576
|
+
|
|
2577
|
+
...Object.fromEntries([['++', 'add'], ['--', 'sub']].map(([op, fn]) => [op, name => {
|
|
2578
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
2579
|
+
const void_ = ctx.func._expect === 'void'
|
|
2580
|
+
const v = readVar(name)
|
|
2581
|
+
const one = v.type === 'i32' ? ['i32.const', 1] : ['f64.const', 1]
|
|
2582
|
+
return writeVar(name, typed([`${v.type}.${fn}`, v, one], v.type), void_)
|
|
2583
|
+
}])),
|
|
2584
|
+
|
|
2585
|
+
// === Arithmetic (type-preserving) ===
|
|
2586
|
+
|
|
2587
|
+
// Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
|
|
2588
|
+
'+': (a, b) => {
|
|
2589
|
+
if (ctx.func._expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
|
|
2590
|
+
// String concatenation: pure string operands skip generic ToString coercion.
|
|
2591
|
+
const vtA = valTypeOf(a)
|
|
2592
|
+
const vtB = valTypeOf(b)
|
|
2593
|
+
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
2594
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
2595
|
+
// generic concat dispatch when rhs is a string-index. The byte flows
|
|
2596
|
+
// straight from __char_at into memory, and the bump-extend path elides
|
|
2597
|
+
// the alloc+copy when lhs is the heap-top STRING.
|
|
2598
|
+
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2599
|
+
if (valTypeOf(b[1]) === VAL.STRING) {
|
|
2600
|
+
inc('__str_append_byte', '__char_at')
|
|
2601
|
+
return typed(['call', '$__str_append_byte',
|
|
2602
|
+
asI64(emit(a)),
|
|
2603
|
+
ctx.abi.string.ops.charCodeAt(asF64(emit(b[1])), asI32(emit(b[2])), ctx),
|
|
2604
|
+
], 'f64')
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
2608
|
+
}
|
|
2609
|
+
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
2610
|
+
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
2611
|
+
// __str_concat's runtime __to_str cannot invoke a user-defined toString.
|
|
2612
|
+
// A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
|
|
2613
|
+
const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2614
|
+
: vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
|
|
2615
|
+
// Coercion-free sides are already strings: a known STRING is raw; OBJECT/BOOL
|
|
2616
|
+
// were stringified by `strOperand`. An unknown side still needs ToString, but
|
|
2617
|
+
// we can apply it *once* (explicit `__to_str` via `strI64`) and join with
|
|
2618
|
+
// concatRaw — equivalent to `__str_concat`'s internal `__to_str` on that side,
|
|
2619
|
+
// while NOT re-coercing the already-string side. This drops the redundant
|
|
2620
|
+
// per-append `__to_str` on the accumulator in `s += part` (s proven STRING):
|
|
2621
|
+
// - both coercion-free → concatRaw(ea, eb)
|
|
2622
|
+
// - one unknown → concatRaw(known, __to_str(unknown))
|
|
2623
|
+
// - both unknown → cat (unchanged; its runtime __to_str covers both)
|
|
2624
|
+
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2625
|
+
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2626
|
+
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2627
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2628
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2629
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2630
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2631
|
+
}
|
|
2632
|
+
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
2633
|
+
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
2634
|
+
// Runtime string dispatch when at least one side could be a string. When one side has
|
|
2635
|
+
// a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
|
|
2636
|
+
// chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
|
|
2637
|
+
// operand needs the runtime check.
|
|
2638
|
+
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
2639
|
+
const tA = temp('add'), tB = temp('add')
|
|
2640
|
+
inc('__str_concat', '__is_str_key')
|
|
2641
|
+
const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
|
|
2642
|
+
const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
|
|
2643
|
+
const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
|
|
2644
|
+
const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
2645
|
+
if (checkA && checkB) {
|
|
2646
|
+
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
2647
|
+
}
|
|
2648
|
+
// Exactly one side is checked. Pre-eval the known side first, then the if branches on the unknown.
|
|
2649
|
+
const preEval = vtA == null ? ['local.set', `$${tB}`, asF64(emit(b))] : ['local.set', `$${tA}`, asF64(emit(a))]
|
|
2650
|
+
return block64(
|
|
2651
|
+
preEval,
|
|
2652
|
+
['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]])
|
|
2653
|
+
}
|
|
2654
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
|
|
2655
|
+
if (_f) return _f
|
|
2656
|
+
// Neither side is a string here (string paths handled above), but either may
|
|
2657
|
+
// still be null/undefined/pointer — numeric `+` performs ToNumber like `-`/`*`.
|
|
2658
|
+
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
2659
|
+
if (isLit(va) && litVal(va) === 0) return toNumF64(b, vb)
|
|
2660
|
+
// An `.unsigned` operand is a uint32 (range [0, 2^32)); JS `+` is a float
|
|
2661
|
+
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
2662
|
+
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
2663
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
2664
|
+
return typed(['f64.add', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2665
|
+
},
|
|
2666
|
+
'-': (a, b) => {
|
|
2667
|
+
if (ctx.func._expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
2668
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2669
|
+
return b === undefined
|
|
2670
|
+
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
2671
|
+
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
2672
|
+
if (b === undefined) return emitNeg(a)
|
|
2673
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
2674
|
+
if (_f) return _f
|
|
2675
|
+
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
2676
|
+
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
2677
|
+
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
2678
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
2679
|
+
return typed(['f64.sub', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2680
|
+
},
|
|
2681
|
+
'u+': a => {
|
|
2682
|
+
if (valTypeOf(a) === VAL.BIGINT)
|
|
2683
|
+
return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
|
|
2684
|
+
const v = emit(a)
|
|
2685
|
+
if (v.type === 'i32') return asF64(v)
|
|
2686
|
+
if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
|
|
2687
|
+
inc('__to_num')
|
|
2688
|
+
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
2689
|
+
},
|
|
2690
|
+
'u-': a => emitNeg(a),
|
|
2691
|
+
'*': (a, b) => {
|
|
2692
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2693
|
+
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
2694
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
|
|
2695
|
+
if (_f) return _f
|
|
2696
|
+
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2697
|
+
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
2698
|
+
// `x * 0` → 0 only when the other factor is provably finite (i32, or a finite
|
|
2699
|
+
// literal): JS `NaN*0` / `±Inf*0` are NaN, so a non-finite f64 must fall
|
|
2700
|
+
// through to `f64.mul` (which yields NaN). For finite x the dropped product is
|
|
2701
|
+
// ±0 — and -0 === +0, so consumers are unaffected. The block evaluates x for
|
|
2702
|
+
// its side effects before dropping.
|
|
2703
|
+
const finiteFactor = (v) => isI32Num(v) || (isLit(v) && Number.isFinite(litVal(v)))
|
|
2704
|
+
if (isLit(vb) && litVal(vb) === 0 && finiteFactor(va)) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
2705
|
+
if (isLit(va) && litVal(va) === 0 && finiteFactor(vb)) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
2706
|
+
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2707
|
+
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
2708
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
|
|
2709
|
+
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2710
|
+
},
|
|
2711
|
+
'/': (a, b) => {
|
|
2712
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2713
|
+
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
2714
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
2715
|
+
if (_f) return _f
|
|
2716
|
+
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2717
|
+
return typed(['f64.div', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2718
|
+
},
|
|
2719
|
+
'%': (a, b) => {
|
|
2720
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2721
|
+
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
2722
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
2723
|
+
if (_f) return _f
|
|
2724
|
+
// ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
|
|
2725
|
+
// The i32.rem_s fast path traps on a zero divisor, so divert a literal-zero divisor.
|
|
2726
|
+
if (isLit(vb) && litVal(vb) === 0) return emitNum(NaN)
|
|
2727
|
+
// i32.rem_s is exact for integer operands AND fast, but it TRAPS on a zero
|
|
2728
|
+
// divisor where JS yields NaN. Only take it when the divisor is a literal
|
|
2729
|
+
// integer (necessarily nonzero — literal 0 is handled above); a runtime i32
|
|
2730
|
+
// divisor could be 0, so route it to f64rem (exact for in-range integers,
|
|
2731
|
+
// NaN for 0). The dividend may be a bare i32 or a FAITHFUL signed-convert
|
|
2732
|
+
// wrapper (f64.convert_i32_s X — the i32 view equals the JS value): peel it.
|
|
2733
|
+
// `.unsigned` operand: `i32.rem_s` reads the uint32 as a negative signed value
|
|
2734
|
+
// ((2^32-1)%7 → rem_s(-1,7) = -1, not 3). Widen to f64 — see `+` above.
|
|
2735
|
+
if (isLit(vb) && Number.isInteger(litVal(vb)) && Math.abs(litVal(vb)) < 2 ** 31 && !vb.unsigned) {
|
|
2736
|
+
const pa = isI32Num(va) && !va.unsigned ? va
|
|
2737
|
+
: Array.isArray(va) && va[0] === 'f64.convert_i32_s' && !va.unsigned
|
|
2738
|
+
? (Array.isArray(va[1]) ? typed(va[1], 'i32') : va[1]) : null
|
|
2739
|
+
if (pa) return typed(['i32.rem_s', pa, ['i32.const', litVal(vb) | 0]], 'i32')
|
|
2740
|
+
}
|
|
2741
|
+
// Fast path: positive literal divisor → inline a - trunc(a/b) * b.
|
|
2742
|
+
// Exact when |a| < 2^53 × |b| (all practical audio/control-range values).
|
|
2743
|
+
// The full __rem handles NaN/±Inf/0 edges exactly; this avoids the call overhead.
|
|
2744
|
+
if (isLit(vb) && litVal(vb) > 0) {
|
|
2745
|
+
const fa = toNumF64(a, va), fb = toNumF64(b, vb)
|
|
2746
|
+
const rem = ta => typed(['f64.sub', ta, ['f64.mul', ['f64.trunc', ['f64.div', ta, fb]], fb]], 'f64')
|
|
2747
|
+
if (isPureIR(fa)) return rem(fa)
|
|
2748
|
+
return withTemp(fa, t => rem(['local.get', `$${t}`]), 'rem')
|
|
2749
|
+
}
|
|
2750
|
+
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
2751
|
+
},
|
|
2752
|
+
// === Comparisons (always i32 result) ===
|
|
2753
|
+
|
|
2754
|
+
'==': (a, b) => emitLooseEq(a, b, false),
|
|
2755
|
+
'!=': (a, b) => emitLooseEq(a, b, true),
|
|
2756
|
+
'===': (a, b) => emitStrictEq(a, b, false),
|
|
2757
|
+
'!==': (a, b) => emitStrictEq(a, b, true),
|
|
2758
|
+
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
2759
|
+
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
2760
|
+
'<=': cmpOp('le_s', 'le', (a, b) => a <= b),
|
|
2761
|
+
'>=': cmpOp('ge_s', 'ge', (a, b) => a >= b),
|
|
2762
|
+
|
|
2763
|
+
// === Logical ===
|
|
2764
|
+
|
|
2765
|
+
'!': a => {
|
|
2766
|
+
const v = emit(a)
|
|
2767
|
+
if (v.type === 'i32') return typed(['i32.eqz', v], 'i32')
|
|
2768
|
+
// Unboxed pointer offsets: falsy iff zero offset.
|
|
2769
|
+
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
2770
|
+
// Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
|
|
2771
|
+
// Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
|
|
2772
|
+
// VAL.BOOL rides the 0/1 numeric carrier (not a pointer), so normalize it to
|
|
2773
|
+
// NUMBER and let it fall to the truthy path — `!false` must be `true`.
|
|
2774
|
+
const vt = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
2775
|
+
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
|
|
2776
|
+
return isNullish(asF64(v))
|
|
2777
|
+
}
|
|
2778
|
+
// Route through truthyIR (not a bare __is_truthy) so a NUMBER operand uses the
|
|
2779
|
+
// NaN-safe f64 test — `!(0/0)` must be `true` on every platform (x86's sign-set
|
|
2780
|
+
// NaN would read as a truthy box through the bit-based __is_truthy).
|
|
2781
|
+
return typed(['i32.eqz', truthyIR(v)], 'i32')
|
|
2782
|
+
},
|
|
2783
|
+
|
|
2784
|
+
'?:': (a, b, c) => {
|
|
2785
|
+
// Constant condition → emit only the live branch
|
|
2786
|
+
const ca = emit(a)
|
|
2787
|
+
if (isLit(ca)) { const v = litVal(ca); return (v !== 0 && v === v) ? emit(b) : emit(c) }
|
|
2788
|
+
const cond = toBoolFromEmitted(ca)
|
|
2789
|
+
// Flow-sensitive refinement: each arm sees narrowing consistent with `a` being truthy / falsy.
|
|
2790
|
+
const thenRefs = extractRefinements(a, new Map(), true)
|
|
2791
|
+
const elseRefs = extractRefinements(a, new Map(), false)
|
|
2792
|
+
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
2793
|
+
const vc = withRefinements(elseRefs, c, () => emit(c))
|
|
2794
|
+
// `cond ? 1 : 0` is the condition bit itself; `cond ? 0 : 1` its negation. `cond`
|
|
2795
|
+
// (truthyIR) is already canonical 0/1, so the select + two const arms collapse to
|
|
2796
|
+
// the bit. (Both arms are literals here, so dropping their emitted IR is side-effect
|
|
2797
|
+
// free.) Mirrors what `+(x > 0)` already produces.
|
|
2798
|
+
if (isLit(vb) && isLit(vc)) {
|
|
2799
|
+
const lb = litVal(vb), lc = litVal(vc)
|
|
2800
|
+
if (lb === 1 && lc === 0) return typed(cond, 'i32')
|
|
2801
|
+
if (lb === 0 && lc === 1) return typed(['i32.eqz', cond], 'i32')
|
|
2802
|
+
}
|
|
2803
|
+
// L: Use WASM select for pure ternaries — branchless, smaller bytecode
|
|
2804
|
+
if (vb.type === 'i32' && vc.type === 'i32') {
|
|
2805
|
+
// A single i32 select is only sound when BOTH arms' i32 carriers mean the same
|
|
2806
|
+
// thing to the downstream asF64 — otherwise the result is interpreted one way and
|
|
2807
|
+
// the other arm's value is corrupted. Two compatible shapes:
|
|
2808
|
+
// • both non-pointer i32 (numbers/bools) → asF64 numeric-converts, correct; or
|
|
2809
|
+
// • both the SAME pointer kind+aux → result carries that ptrKind so asF64 takes
|
|
2810
|
+
// the NaN-rebox path (and boxPtrIR's single aux slot is the shared one).
|
|
2811
|
+
// Anything else — a pointer arm beside a number/bool arm, two different pointer
|
|
2812
|
+
// kinds, or the same kind with diverging aux (polymorphic OBJECT schemaIds, TYPED
|
|
2813
|
+
// element types) — must fall through to the f64 path, where each arm is asF64'd
|
|
2814
|
+
// independently and reboxed with its own kind/aux in the NaN-box. The pre-4.x bug:
|
|
2815
|
+
// a pointer arm vs a `true`/number arm took the i32 select, dropped the ptrKind,
|
|
2816
|
+
// and `f64.convert_i32_s` numeric-converted the pointer bits — so `cond ? obj : 1`
|
|
2817
|
+
// lost its object-ness (typeof → "number").
|
|
2818
|
+
const bothPlain = vb.ptrKind == null && vc.ptrKind == null
|
|
2819
|
+
const samePtr = vb.ptrKind != null && vb.ptrKind === vc.ptrKind
|
|
2820
|
+
&& (vb.ptrAux ?? null) === (vc.ptrAux ?? null)
|
|
2821
|
+
if (bothPlain || samePtr) {
|
|
2822
|
+
const tagPtr = (n) => {
|
|
2823
|
+
if (vb.ptrKind != null && vb.ptrKind === vc.ptrKind) {
|
|
2824
|
+
n.ptrKind = vb.ptrKind
|
|
2825
|
+
if (vb.ptrAux != null && vb.ptrAux === vc.ptrAux) n.ptrAux = vb.ptrAux
|
|
2826
|
+
}
|
|
2827
|
+
return n
|
|
2828
|
+
}
|
|
2829
|
+
if (isPureIR(vb) && isPureIR(vc))
|
|
2830
|
+
return tagPtr(typed(['select', vb, vc, cond], 'i32'))
|
|
2831
|
+
return tagPtr(typed(['if', ['result', 'i32'], cond, ['then', vb], ['else', vc]], 'i32'))
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
const fb = asF64(vb), fc = asF64(vc)
|
|
2835
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
2836
|
+
const vtc = resolveValType(c, valTypeOf, lookupValType)
|
|
2837
|
+
const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
|
|
2838
|
+
const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
|
|
2839
|
+
|| vb.closureFuncIdx != null || vc.closureFuncIdx != null
|
|
2840
|
+
|| isNaNBoxLit(fb) || isNaNBoxLit(fc)
|
|
2841
|
+
const numericB = isNumArm(vb, b, vtb)
|
|
2842
|
+
const numericC = isNumArm(vc, c, vtc)
|
|
2843
|
+
// Peephole: `cond ? 1 : 0` (or `cond ? 0 : 1`) is just `f64.convert_i32_s(cond)` —
|
|
2844
|
+
// the select collapses because cond is already 0/1. Saves 5 instructions.
|
|
2845
|
+
const isOneZero = (one, zero) => {
|
|
2846
|
+
const o = one, z = zero
|
|
2847
|
+
return o.type === 'i32' && Array.isArray(o) && o[0] === 'i32.const' && o[1] === 1 &&
|
|
2848
|
+
z.type === 'i32' && Array.isArray(z) && z[0] === 'i32.const' && z[1] === 0
|
|
2849
|
+
}
|
|
2850
|
+
if ((isOneZero(vb, vc) || isOneZero(vc, vb)) && !numericB && !numericC) {
|
|
2851
|
+
const condBool = truthyIR(emit(a))
|
|
2852
|
+
const n = isOneZero(vb, vc)
|
|
2853
|
+
? typed(['f64.convert_i32_s', condBool], 'f64')
|
|
2854
|
+
: typed(['f64.convert_i32_s', ['i32.eqz', condBool]], 'f64')
|
|
2855
|
+
n.valKind = VAL.NUMBER
|
|
2856
|
+
return n
|
|
2857
|
+
}
|
|
2858
|
+
const branchB = canonArm(fb, numericB, numericC), branchC = canonArm(fc, numericC, numericB)
|
|
2859
|
+
const markNumeric = (n) => {
|
|
2860
|
+
if (numericB && numericC) n.valKind = VAL.NUMBER
|
|
2861
|
+
return n
|
|
2862
|
+
}
|
|
2863
|
+
if (refPayload) {
|
|
2864
|
+
const ib = ['i64.reinterpret_f64', branchB]
|
|
2865
|
+
const ic = ['i64.reinterpret_f64', branchC]
|
|
2866
|
+
const bits = isPureIR(branchB) && isPureIR(branchC)
|
|
2867
|
+
? ['select', ib, ic, cond]
|
|
2868
|
+
: ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
|
|
2869
|
+
return typed(['f64.reinterpret_i64', bits], 'f64')
|
|
2870
|
+
}
|
|
2871
|
+
if (!refPayload && isPureIR(branchB) && isPureIR(branchC))
|
|
2872
|
+
return markNumeric(typed(['select', branchB, branchC, cond], 'f64'))
|
|
2873
|
+
return markNumeric(typed(['if', ['result', 'f64'], cond, ['then', branchB], ['else', branchC]], 'f64'))
|
|
2874
|
+
},
|
|
2875
|
+
|
|
2876
|
+
'&&': (a, b) => {
|
|
2877
|
+
// Range-check fusion: `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI compile-time
|
|
2878
|
+
// constants) collapses to one unsigned compare `(x - LO) <=u (HI - LO)` — a subtract
|
|
2879
|
+
// plus a branch instead of two compares, an AND, and a short-circuit branch. This is
|
|
2880
|
+
// the per-char cost in scanners/parsers (digit/alpha classification) and in any
|
|
2881
|
+
// two-sided bounds check. Restricted to a local `x` so evaluating it once (the fused
|
|
2882
|
+
// form) matches the original's twice-read, side-effect-free semantics.
|
|
2883
|
+
const fused = fuseRangeCheck(a, b)
|
|
2884
|
+
if (fused) return fused
|
|
2885
|
+
const va = emit(a)
|
|
2886
|
+
// Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
|
|
2887
|
+
if (isLit(va)) {
|
|
2888
|
+
const v = litVal(va)
|
|
2889
|
+
if (v !== 0 && v === v) {
|
|
2890
|
+
const refs = extractRefinements(a, new Map(), true)
|
|
2891
|
+
return withRefinements(refs, b, () => emit(b))
|
|
2892
|
+
}
|
|
2893
|
+
return va
|
|
2894
|
+
}
|
|
2895
|
+
// a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
|
|
2896
|
+
// (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
|
|
2897
|
+
const rightRefs = extractRefinements(a, new Map(), true)
|
|
2898
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
2899
|
+
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
2900
|
+
// skip f64 round-trip and __is_truthy call entirely.
|
|
2901
|
+
if (va.type === 'i32') {
|
|
2902
|
+
const vb = emitRight()
|
|
2903
|
+
const t = tempI32()
|
|
2904
|
+
if (vb.type === 'i32') {
|
|
2905
|
+
return typed(['if', ['result', 'i32'],
|
|
2906
|
+
['local.tee', `$${t}`, va],
|
|
2907
|
+
['then', vb],
|
|
2908
|
+
['else', ['local.get', `$${t}`]]], 'i32')
|
|
2909
|
+
}
|
|
2910
|
+
return typed(['if', ['result', 'f64'],
|
|
2911
|
+
['local.tee', `$${t}`, va],
|
|
2912
|
+
['then', asF64(vb)],
|
|
2913
|
+
['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
|
|
2914
|
+
}
|
|
2915
|
+
const t = temp()
|
|
2916
|
+
const numA = isNumArm(va, a)
|
|
2917
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
2918
|
+
// `a` is the else-arm result (returned when falsy — incl NaN), so canon a lone-numeric
|
|
2919
|
+
// `a` before the tee: `$t` then feeds both the result and the cond canonically.
|
|
2920
|
+
const teed = typed(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)], 'f64')
|
|
2921
|
+
// A numeric left arm tests truthiness NaN-by-value (not __is_truthy, which mis-reads
|
|
2922
|
+
// x86's sign-set NaN as truthy) — tag it so truthyIR takes that path.
|
|
2923
|
+
if (numA) teed.valKind = VAL.NUMBER
|
|
2924
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
2925
|
+
['then', canonArm(asF64(vb), numB, numA)],
|
|
2926
|
+
['else', ['local.get', `$${t}`]]], 'f64')
|
|
2927
|
+
},
|
|
2928
|
+
|
|
2929
|
+
'||': (a, b) => {
|
|
2930
|
+
// Outside-range fusion (the complement of `&&`): `x < LO || x > HI` → one unsigned
|
|
2931
|
+
// compare `(x - LO) >u (HI - LO)`. Common in validation (`if (c < 'a' || c > 'z') …`).
|
|
2932
|
+
const fusedOr = fuseRangeCheckOr(a, b)
|
|
2933
|
+
if (fusedOr) return fusedOr
|
|
2934
|
+
const va = emit(a)
|
|
2935
|
+
// Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
|
|
2936
|
+
if (isLit(va)) {
|
|
2937
|
+
const v = litVal(va)
|
|
2938
|
+
if (v !== 0 && v === v) return va
|
|
2939
|
+
const refs = extractRefinements(a, new Map(), false)
|
|
2940
|
+
return withRefinements(refs, b, () => emit(b))
|
|
2941
|
+
}
|
|
2942
|
+
// a is falsy in the right-arm — `x == null || ...` proves x is null/undefined in b;
|
|
2943
|
+
// De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
|
|
2944
|
+
const rightRefs = extractRefinements(a, new Map(), false)
|
|
2945
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
2946
|
+
if (va.type === 'i32') {
|
|
2947
|
+
const vb = emitRight()
|
|
2948
|
+
const t = tempI32()
|
|
2949
|
+
if (vb.type === 'i32') {
|
|
2950
|
+
return typed(['if', ['result', 'i32'],
|
|
2951
|
+
['local.tee', `$${t}`, va],
|
|
2952
|
+
['then', ['local.get', `$${t}`]],
|
|
2953
|
+
['else', vb]], 'i32')
|
|
2954
|
+
}
|
|
2955
|
+
return typed(['if', ['result', 'f64'],
|
|
2956
|
+
['local.tee', `$${t}`, va],
|
|
2957
|
+
['then', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')],
|
|
2958
|
+
['else', asF64(vb)]], 'f64')
|
|
2959
|
+
}
|
|
2960
|
+
const t = temp()
|
|
2961
|
+
const numA = isNumArm(va, a)
|
|
2962
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
2963
|
+
// `a` (then-arm) is returned only when truthy — hence never NaN — so it needs no canon;
|
|
2964
|
+
// the cond's NaN-safety comes from the valKind tag. Only the else (b) arm can surface
|
|
2965
|
+
// as a numeric NaN.
|
|
2966
|
+
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
2967
|
+
if (numA) teed.valKind = VAL.NUMBER // numeric left arm: NaN-safe truthiness (see `&&`)
|
|
2968
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
2969
|
+
['then', ['local.get', `$${t}`]],
|
|
2970
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2971
|
+
},
|
|
2972
|
+
|
|
2973
|
+
// a ?? b: returns b only if a is nullish
|
|
2974
|
+
'??': (a, b) => {
|
|
2975
|
+
const va = emit(a), vb = emit(b)
|
|
2976
|
+
const t = temp()
|
|
2977
|
+
const numA = isNumArm(va, a), numB = isNumArm(vb, b)
|
|
2978
|
+
// Both arms can surface as the (untyped) result — `a` when non-nullish (a NaN is not
|
|
2979
|
+
// nullish, so it IS returned), `b` otherwise. Canon a lone-numeric arm; `a` before the
|
|
2980
|
+
// tee so `local.get $t` is canonical. The cond is isNullish, robust to non-canon NaN.
|
|
2981
|
+
return typed(['if', ['result', 'f64'],
|
|
2982
|
+
['i32.eqz', isNullish(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)])],
|
|
2983
|
+
['then', ['local.get', `$${t}`]],
|
|
2984
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2985
|
+
},
|
|
2986
|
+
|
|
2987
|
+
'void': a => {
|
|
2988
|
+
const v = emit(a)
|
|
2989
|
+
const dropAndUndef = (instr) => block64(instr, 'drop', undefExpr())
|
|
2990
|
+
if (v == null) return undefExpr()
|
|
2991
|
+
const op = Array.isArray(v) ? v[0] : null
|
|
2992
|
+
const wasmVoid = op === 'local.set' || (typeof op === 'string' && op.endsWith('.store'))
|
|
2993
|
+
|| op === 'memory.copy' || op === 'global.set'
|
|
2994
|
+
if (wasmVoid)
|
|
2995
|
+
return block64(v, undefExpr())
|
|
2996
|
+
if (v.type && v.type !== 'void')
|
|
2997
|
+
return dropAndUndef(v)
|
|
2998
|
+
return block64(...flat(v), undefExpr())
|
|
2999
|
+
},
|
|
3000
|
+
|
|
3001
|
+
'(': a => emit(a),
|
|
3002
|
+
|
|
3003
|
+
// === Bitwise (i32 for numbers, i64 for BigInt) ===
|
|
3004
|
+
|
|
3005
|
+
// Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
|
|
3006
|
+
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
3007
|
+
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
3008
|
+
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
3009
|
+
// `~~x` is the idiomatic int32 truncation: the two xor-with-(-1) cancel, leaving
|
|
3010
|
+
// a single toI32 (whose NaN/Infinity guard runs once, unchanged). Fold it here so
|
|
3011
|
+
// DSP/bytebeat `~~` doesn't emit a dead double-xor watr won't remove.
|
|
3012
|
+
'~': a => {
|
|
3013
|
+
if (Array.isArray(a) && a[0] === '~') {
|
|
3014
|
+
const inner = a[1]
|
|
3015
|
+
// ~~x === x for BigInt; the int32-truncation fold below is number-only.
|
|
3016
|
+
if (valTypeOf(inner) === VAL.BIGINT) return emit(inner)
|
|
3017
|
+
const iv = emit(inner)
|
|
3018
|
+
return isLit(iv) ? emitNum(~~litVal(iv)) : typed(toI32(isI32Num(iv) ? iv : toNumF64(inner, iv)), 'i32')
|
|
3019
|
+
}
|
|
3020
|
+
// BigInt complement is the i64 `x ^ -1` (all bits flipped), like emitNeg's i64.sub.
|
|
3021
|
+
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.xor', asI64(emit(a)), ['i64.const', -1]])
|
|
3022
|
+
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')
|
|
3023
|
+
},
|
|
3024
|
+
...Object.fromEntries([
|
|
3025
|
+
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
3026
|
+
].map(([op, fn]) => [op, (a, b) => {
|
|
3027
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3028
|
+
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
3029
|
+
if (op === '|') { // `(x / y) | 0` integer-division idiom → i32.div_s
|
|
3030
|
+
const divN = intLiteralValue(b) === 0 ? a : intLiteralValue(a) === 0 ? b : null
|
|
3031
|
+
if (Array.isArray(divN) && divN[0] === '/') { const r = tryIntDivTrunc(divN[1], divN[2]); if (r) return r }
|
|
3032
|
+
}
|
|
3033
|
+
const va = emit(a), vb = emit(b)
|
|
3034
|
+
if (isLit(va) && isLit(vb)) {
|
|
3035
|
+
const la = litVal(va), lb = litVal(vb)
|
|
3036
|
+
if (op === '&') return emitNum(la & lb); if (op === '|') return emitNum(la | lb)
|
|
3037
|
+
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
3038
|
+
if (op === '>>') return emitNum(la >> lb)
|
|
3039
|
+
}
|
|
3040
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
3041
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
3042
|
+
return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
|
|
3043
|
+
}])),
|
|
3044
|
+
'>>>': (a, b) => {
|
|
3045
|
+
const va = emit(a), vb = emit(b)
|
|
3046
|
+
if (isLit(va) && isLit(vb)) {
|
|
3047
|
+
const r = litVal(va) >>> litVal(vb) // JS uint32 result ∈ [0, 2^32)
|
|
3048
|
+
// ≥ 2^31 doesn't fit signed i32: materialize the wrapped bits as an i32 const
|
|
3049
|
+
// tagged `.unsigned` so `asF64` lifts via `convert_i32_u`. Emitting `f64.const r`
|
|
3050
|
+
// here (the old foldConst path) would `trunc_sat_f64_s`-saturate to INT32_MAX
|
|
3051
|
+
// when the enclosing function narrows to an i32 result. Values < 2^31 fold to a
|
|
3052
|
+
// plain i32 const (signed == unsigned, stays foldable downstream).
|
|
3053
|
+
if (r >= 0x80000000) { const node = typed(['i32.const', r | 0], 'i32'); node.unsigned = true; return node }
|
|
3054
|
+
return emitNum(r)
|
|
3055
|
+
}
|
|
3056
|
+
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
3057
|
+
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
3058
|
+
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
3059
|
+
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
3060
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
3061
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
3062
|
+
const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
|
|
3063
|
+
node.unsigned = true
|
|
3064
|
+
return node
|
|
3065
|
+
},
|
|
3066
|
+
|
|
3067
|
+
// === Control flow ===
|
|
3068
|
+
|
|
3069
|
+
'if': (cond, then, els) => {
|
|
3070
|
+
// Dead branch elimination: constant condition → emit only the live branch
|
|
3071
|
+
const ce = emit(cond)
|
|
3072
|
+
if (isLit(ce)) {
|
|
3073
|
+
const v = litVal(ce), truthy = v !== 0 && v === v
|
|
3074
|
+
if (truthy) return emitVoid(then)
|
|
3075
|
+
if (els != null) return emitVoid(els)
|
|
3076
|
+
return null
|
|
3077
|
+
}
|
|
3078
|
+
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
3079
|
+
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
3080
|
+
const thenRefs = extractRefinements(cond, new Map(), true)
|
|
3081
|
+
const elseRefs = extractRefinements(cond, new Map(), false)
|
|
3082
|
+
const thenBody = withRefinements(thenRefs, then, () => emitVoid(then))
|
|
3083
|
+
if (els != null) {
|
|
3084
|
+
const elseBody = withRefinements(elseRefs, els, () => emitVoid(els))
|
|
3085
|
+
return ['if', c, ['then', ...thenBody], ['else', ...elseBody]]
|
|
3086
|
+
}
|
|
3087
|
+
return ['if', c, ['then', ...thenBody]]
|
|
3088
|
+
},
|
|
3089
|
+
|
|
3090
|
+
'for': (init, cond, step, body) => {
|
|
3091
|
+
if (body === undefined) return err('for-in/for-of not supported')
|
|
3092
|
+
// An enclosing labeled statement (`outer: for …`) hands its label down so `continue outer`
|
|
3093
|
+
// can target this loop's continue point. The immediately-enclosed loop consumes it.
|
|
3094
|
+
const myLabel = ctx.func.pendingLabel; ctx.func.pendingLabel = null
|
|
3095
|
+
const labeledContinue = myLabel != null && hasLabeledContinueTo(body, myLabel)
|
|
3096
|
+
// Don't unroll a loop that is the target of a `continue <label>` — unrolling would lose the
|
|
3097
|
+
// continue edge. (Plain loops with no labeled-continue still unroll.)
|
|
3098
|
+
if (!labeledContinue && (!ctx.transform.optimize || ctx.transform.optimize.smallConstForUnroll !== false)) {
|
|
3099
|
+
const unrolled = unrollSmallConstFor(init, cond, step, body)
|
|
3100
|
+
if (unrolled) return unrolled
|
|
3101
|
+
}
|
|
3102
|
+
// for-in over a static schema → unroll with key-literal substitution (folds
|
|
3103
|
+
// o[k] to schema slots). Recognized via the for-in-exclusive __keys_ro intrinsic.
|
|
3104
|
+
if (!labeledContinue && (!ctx.transform.optimize || ctx.transform.optimize.forInUnroll !== false)) {
|
|
3105
|
+
const fu = unrollForIn(init, cond, step, body)
|
|
3106
|
+
if (fu) return fu
|
|
3107
|
+
}
|
|
3108
|
+
// Lift constant array/object literals out of the loop (allocate once, not per
|
|
3109
|
+
// iteration) when they are read-only + non-escaping inside it. Strip them from the
|
|
3110
|
+
// body up front so freshBoxed / continue analysis see the reduced body.
|
|
3111
|
+
let preLoopLits = []
|
|
3112
|
+
if (!ctx.transform.optimize || ctx.transform.optimize.hoistConstLit !== false) {
|
|
3113
|
+
const ex = extractHoistableLiterals(body)
|
|
3114
|
+
if (ex) { preLoopLits = ex.hoisted; body = ex.body }
|
|
3115
|
+
}
|
|
3116
|
+
const id = ctx.func.uniq++
|
|
3117
|
+
const brk = `$brk${id}`, loop = `$loop${id}`
|
|
3118
|
+
// The cont wrapper is only needed if the body has a `continue` AND there is a step
|
|
3119
|
+
// expression — `continue` must jump to before the step. Without a step, `continue`
|
|
3120
|
+
// can target the loop label directly, saving a redundant `block`.
|
|
3121
|
+
const needsCont = step && (hasOwnContinue(body) || labeledContinue)
|
|
3122
|
+
const cont = needsCont ? `$cont${id}` : loop
|
|
3123
|
+
ctx.func.stack.push({ brk, loop: cont })
|
|
3124
|
+
const frame = ctx.func.stack[ctx.func.stack.length - 1]
|
|
3125
|
+
if (myLabel != null) frame.contLabel = myLabel // so `continue <myLabel>` targets this loop's step/test
|
|
3126
|
+
// Per-iteration fresh cells for boxed locals declared in the body — allocated
|
|
3127
|
+
// at body entry so a closure declared before its binding captures the right
|
|
3128
|
+
// cell (sets frame.loopFresh; emitDecl then stores rather than re-allocates).
|
|
3129
|
+
const freshBoxed = emitLoopFreshBoxed(body, frame)
|
|
3130
|
+
const result = []
|
|
3131
|
+
if (init != null) result.push(...emitVoid(init))
|
|
3132
|
+
for (const lit of preLoopLits) result.push(...emitVoid(lit)) // allocate hoisted literals once
|
|
3133
|
+
// Hoist a loop-invariant immutable-length bound out of the condition. A typed
|
|
3134
|
+
// array's `.length` is fixed, so `i < arr.length` otherwise reloads the header
|
|
3135
|
+
// (`i32.load (base-8) >> 2`) every iteration for nothing (V8's JIT hoists it).
|
|
3136
|
+
// Compute it once into a temp when `arr` is a typed-array var not reassigned in
|
|
3137
|
+
// the body. Only the simple top-level comparison forms — anything fancier just
|
|
3138
|
+
// keeps the per-iteration eval (correct, only misses the speedup).
|
|
3139
|
+
let condForLoop = cond
|
|
3140
|
+
if (cond && Array.isArray(cond) && HOIST_CMP.has(cond[0])) {
|
|
3141
|
+
const side = immutableLenBound(cond[2], body) ? 2 : immutableLenBound(cond[1], body) ? 1 : 0
|
|
3142
|
+
if (side) {
|
|
3143
|
+
const lt = tempI32('len')
|
|
3144
|
+
result.push(['local.set', `$${lt}`, asI32(emit(cond[side]))])
|
|
3145
|
+
condForLoop = cond.slice(); condForLoop[side] = lt
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
const loopBody = []
|
|
3149
|
+
if (condForLoop) loopBody.push(['br_if', brk, ['i32.eqz', toBool(condForLoop)]])
|
|
3150
|
+
loopBody.push(...freshBoxed)
|
|
3151
|
+
if (needsCont) loopBody.push(['block', cont, ...emitVoid(body)])
|
|
3152
|
+
else loopBody.push(...emitVoid(body))
|
|
3153
|
+
if (step) loopBody.push(...emitVoid(step))
|
|
3154
|
+
loopBody.push(['br', loop])
|
|
3155
|
+
result.push(['block', brk, ['loop', loop, ...loopBody]])
|
|
3156
|
+
ctx.func.stack.pop()
|
|
3157
|
+
return result.length === 1 ? result[0] : result
|
|
3158
|
+
},
|
|
3159
|
+
|
|
3160
|
+
'switch': (discriminant, ...cases) => {
|
|
3161
|
+
const disc = `${T}disc${ctx.func.uniq++}`
|
|
3162
|
+
ctx.func.locals.set(disc, 'f64')
|
|
3163
|
+
|
|
3164
|
+
const result = [['local.set', `$${disc}`, asF64(emit(discriminant))]]
|
|
3165
|
+
|
|
3166
|
+
for (const c of cases) {
|
|
3167
|
+
if (c[0] === 'case') {
|
|
3168
|
+
const [, test, body] = c
|
|
3169
|
+
const skip = `$skip${ctx.func.uniq++}`
|
|
3170
|
+
// Block: skip if discriminant != test, otherwise execute body
|
|
3171
|
+
result.push(['block', skip,
|
|
3172
|
+
['br_if', skip, typed(['f64.ne', typed(['local.get', `$${disc}`], 'f64'), asF64(emit(test))], 'i32')],
|
|
3173
|
+
...emitVoid(body)])
|
|
3174
|
+
} else if (c[0] === 'default') {
|
|
3175
|
+
result.push(...emitVoid(c[1]))
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
return result
|
|
3180
|
+
},
|
|
3181
|
+
|
|
3182
|
+
'while': (cond, body) => emitter['for'](null, cond, null, body),
|
|
3183
|
+
'label': (name, body) => {
|
|
3184
|
+
const brk = `$label${ctx.func.uniq++}`
|
|
3185
|
+
ctx.func.stack.push({ label: name, brk })
|
|
3186
|
+
// Hand the label to the immediately-enclosed loop so `continue name` can target it.
|
|
3187
|
+
ctx.func.pendingLabel = name
|
|
3188
|
+
const result = ['block', brk, ...emitVoid(body)]
|
|
3189
|
+
ctx.func.pendingLabel = null // clear if the body wasn't a loop (nothing consumed it)
|
|
3190
|
+
ctx.func.stack.pop()
|
|
3191
|
+
return result
|
|
3192
|
+
},
|
|
3193
|
+
'break': (label) => {
|
|
3194
|
+
const target = label == null
|
|
3195
|
+
? loopTop().brk
|
|
3196
|
+
: ctx.func.stack.findLast(frame => frame.label === label)?.brk
|
|
3197
|
+
if (!target) err(`break label '${label}' is not in scope`)
|
|
3198
|
+
return [...emitFinalizers(), ['br', target]]
|
|
3199
|
+
},
|
|
3200
|
+
'continue': (label) => {
|
|
3201
|
+
if (label == null) return [...emitFinalizers(), ['br', loopTop().loop]]
|
|
3202
|
+
// Labeled continue: target the continue point of the loop that adopted this label.
|
|
3203
|
+
const frame = ctx.func.stack.findLast(f => f.contLabel === label)
|
|
3204
|
+
if (!frame) err(`continue label '${label}' is not in scope`)
|
|
3205
|
+
return [...emitFinalizers(), ['br', frame.loop]]
|
|
3206
|
+
},
|
|
3207
|
+
|
|
3208
|
+
// === Call ===
|
|
3209
|
+
|
|
3210
|
+
// Arrow as value → closure
|
|
3211
|
+
'=>': (rawParams, body) => {
|
|
3212
|
+
if (!ctx.closure.make) err('Closures require fn module (auto-included)')
|
|
3213
|
+
|
|
3214
|
+
const raw = extractParams(rawParams)
|
|
3215
|
+
const params = [], defaults = {}
|
|
3216
|
+
let restParam = null, bodyPrefix = []
|
|
3217
|
+
for (const r of raw) {
|
|
3218
|
+
const c = classifyParam(r)
|
|
3219
|
+
if (c.kind === 'rest') { restParam = c.name; params.push(c.name) }
|
|
3220
|
+
else if (c.kind === 'plain') params.push(c.name)
|
|
3221
|
+
else if (c.kind === 'default') { params.push(c.name); defaults[c.name] = c.defValue }
|
|
3222
|
+
else {
|
|
3223
|
+
const tmp = `${T}p${ctx.func.uniq++}`
|
|
3224
|
+
params.push(tmp)
|
|
3225
|
+
if (c.kind === 'destruct-default') defaults[tmp] = c.defValue
|
|
3226
|
+
bodyPrefix.push(['let', ['=', c.pattern, tmp]])
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// Prepend destructuring to body (if any destructured params)
|
|
3231
|
+
if (bodyPrefix.length) {
|
|
3232
|
+
const origBody = body
|
|
3233
|
+
if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
|
|
3234
|
+
body = ['{}', [';', ...bodyPrefix, ...body[1].slice(1)]]
|
|
3235
|
+
else if (Array.isArray(body) && body[0] === '{}')
|
|
3236
|
+
body = ['{}', [';', ...bodyPrefix, body[1]]]
|
|
3237
|
+
else body = ['{}', [';', ...bodyPrefix, ['return', body]]]
|
|
3238
|
+
if (origBody && origBody._nonEscaping) body._nonEscaping = origBody._nonEscaping
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// Find free variables in body that aren't params → captures
|
|
3242
|
+
const paramSet = new Set(params)
|
|
3243
|
+
const captures = []
|
|
3244
|
+
findFreeVars(body, paramSet, captures)
|
|
3245
|
+
for (const def of Object.values(defaults)) findFreeVars(def, paramSet, captures)
|
|
3246
|
+
|
|
3247
|
+
// Pass closure info including rest param and defaults
|
|
3248
|
+
const closureInfo = { params, body, captures, restParam }
|
|
3249
|
+
if (Object.keys(defaults).length) closureInfo.defaults = defaults
|
|
3250
|
+
return ctx.closure.make(closureInfo)
|
|
3251
|
+
},
|
|
3252
|
+
|
|
3253
|
+
// Linear callee-kind dispatcher. Each strategy below is its own named function
|
|
3254
|
+
// (extracted to module scope above); this body is just the routing table.
|
|
3255
|
+
'()': (callee, callArgs) => {
|
|
3256
|
+
const argList = commaList(callArgs)
|
|
3257
|
+
const parsed = parseCallArgs(argList)
|
|
3258
|
+
|
|
3259
|
+
// Closure devirtualization: a module-global callee proven (by plan.js) to hold
|
|
3260
|
+
// one statically-known function rewrites to that function, so the
|
|
3261
|
+
// known-top-level-function branch emits a direct `call`, dropping the
|
|
3262
|
+
// indirect/trampoline path.
|
|
3263
|
+
if (typeof callee === 'string' && ctx.func.globalDevirt?.has(callee))
|
|
3264
|
+
callee = ctx.func.globalDevirt.get(callee)
|
|
3265
|
+
|
|
3266
|
+
if (Array.isArray(callee) && callee[0] === '.') return emitMethodCall(callee, parsed, callArgs)
|
|
3267
|
+
|
|
3268
|
+
if (typeof callee === 'string' && ctx.core.emit[callee] && !isBoundName(callee) && !isUserFunc(callee))
|
|
3269
|
+
return emitBuiltinCall(callee, parsed)
|
|
3270
|
+
|
|
3271
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee) && !isBoundName(callee))
|
|
3272
|
+
return emitDirectFunctionCall(callee, parsed, callArgs)
|
|
3273
|
+
|
|
3274
|
+
if (typeof callee === 'string' && !parsed.hasSpread && ctx.func.directClosures?.has(callee)) {
|
|
3275
|
+
const direct = tryDirectClosureCall(callee, parsed)
|
|
3276
|
+
if (direct) return direct
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
if (ctx.closure.call) return emitGenericClosureCall(callee, parsed)
|
|
3280
|
+
|
|
3281
|
+
return emitUnknownCalleeCall(callee, argList)
|
|
3282
|
+
},
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// === Emit dispatch ===
|
|
3286
|
+
|
|
3287
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
|
|
3288
|
+
// Per ECMAScript, an optional access short-circuits the entire continuation, not just
|
|
3289
|
+
// its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
|
|
3290
|
+
// nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
|
|
3291
|
+
//
|
|
3292
|
+
// At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
|
|
3293
|
+
// the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
|
|
3294
|
+
// with that optional replaced by a regular access. The single guard short-circuits the
|
|
3295
|
+
// whole continuation. Nested optionals further inside the chain are left intact and
|
|
3296
|
+
// handle their own short-circuiting on recursion.
|
|
3297
|
+
function liftOptionalChain(node) {
|
|
3298
|
+
const path = []
|
|
3299
|
+
let cur = node
|
|
3300
|
+
while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
|
|
3301
|
+
cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
|
|
3302
|
+
path.push(cur)
|
|
3303
|
+
cur = cur[1]
|
|
3304
|
+
}
|
|
3305
|
+
// Find the deepest optional with continuation outside it. optIdx === 0 means the
|
|
3306
|
+
// chain root itself is optional with no continuation — handled by the regular
|
|
3307
|
+
// `?.` / `?.[]` / `?.()` emitters.
|
|
3308
|
+
let optIdx = -1
|
|
3309
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
3310
|
+
if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
|
|
3311
|
+
optIdx = i
|
|
3312
|
+
break
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
if (optIdx <= 0) return null
|
|
3316
|
+
const opt = path[optIdx]
|
|
3317
|
+
return withNullGuard(asF64(emit(opt[1])), t => {
|
|
3318
|
+
let rebuilt = opt[0] === '?.' ? ['.', t, opt[2]]
|
|
3319
|
+
: opt[0] === '?.[]' ? ['[]', t, opt[2]]
|
|
3320
|
+
: ['()', t, ...opt.slice(2)]
|
|
3321
|
+
for (let i = optIdx - 1; i >= 0; i--) rebuilt = [path[i][0], rebuilt, ...path[i].slice(2)]
|
|
3322
|
+
return asF64(emit(rebuilt))
|
|
3323
|
+
}, 'oc')
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
* Emit single AST node to typed WASM IR.
|
|
3328
|
+
* Every returned node has .type = 'i32' | 'f64'.
|
|
3329
|
+
* @param {import('./prepare.js').ASTNode} node
|
|
3330
|
+
* @returns {Array} typed WASM S-expression
|
|
3331
|
+
*/
|
|
3332
|
+
export function emit(node, expect) {
|
|
3333
|
+
ctx.func._expect = expect || null
|
|
3334
|
+
if (Array.isArray(node)) {
|
|
3335
|
+
ctx.error.node = node
|
|
3336
|
+
if (node.loc != null) ctx.error.loc = node.loc
|
|
3337
|
+
}
|
|
3338
|
+
if (node == null) return null
|
|
3339
|
+
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
3340
|
+
// AST), but their working representation is the plain number 0/1 — identical
|
|
3341
|
+
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|
|
3342
|
+
if (node === true) return emitNum(1)
|
|
3343
|
+
if (node === false) return emitNum(0)
|
|
3344
|
+
if (typeof node === 'symbol') // JZ_NULL / JZ_UNDEF sentinels → null / undefined NaN
|
|
3345
|
+
return node === JZ_UNDEF ? undefExpr() : nullExpr()
|
|
3346
|
+
if (typeof node === 'bigint') {
|
|
3347
|
+
// Truncate to 64 bits — `BigInt.asUintN(64, …)` semantics, same as the
|
|
3348
|
+
// explicit mask `node & 0xFFFFFFFFFFFFFFFFn`. Decimal form (vs. the prior
|
|
3349
|
+
// unsigned-hex dance) is enough now that watr's optimize.js getConst
|
|
3350
|
+
// handles signed strings correctly (4.6.8 W5 fix).
|
|
3351
|
+
return typed(['f64.reinterpret_i64', ['i64.const', BigInt.asUintN(64, node).toString()]], 'f64')
|
|
3352
|
+
}
|
|
3353
|
+
if (typeof node === 'number') return emitNum(node)
|
|
3354
|
+
if (typeof node === 'string') {
|
|
3355
|
+
// Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
|
|
3356
|
+
if (ctx.func.boxed?.has(node) || isBoundName(node) || isGlobal(node) || repOf(node)?.intConst != null)
|
|
3357
|
+
return readVar(node)
|
|
3358
|
+
// Top-level function used as value → wrap as closure pointer for call_indirect
|
|
3359
|
+
if (ctx.func.names.has(node) && !isBoundName(node) && ctx.closure.table) {
|
|
3360
|
+
// Trampoline signature: uniform closure ABI (env f64, argc i32, a0..a{MAX-1} f64) → f64.
|
|
3361
|
+
// Forwards the first N inline slots to $func where N = func's fixed param count.
|
|
3362
|
+
const func = ctx.func.map.get(node)
|
|
3363
|
+
const sigParams = func?.sig.params || []
|
|
3364
|
+
if (sigParams.length > MAX_CLOSURE_ARITY) err(`Function ${node} used as closure value has ${sigParams.length} params, exceeds MAX_CLOSURE_ARITY=${MAX_CLOSURE_ARITY}`)
|
|
3365
|
+
const trampolineName = `${T}tramp_${node}`
|
|
3366
|
+
if (!ctx.core.stdlib[trampolineName]) {
|
|
3367
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
3368
|
+
const paramDecls = ['(param $__env f64)', '(param $__argc i32)']
|
|
3369
|
+
for (let i = 0; i < W; i++) paramDecls.push(`(param $__a${i} f64)`)
|
|
3370
|
+
// A rest param (always last) must be packed into a fresh array from the
|
|
3371
|
+
// overflow inline slots — the direct-call path does this via
|
|
3372
|
+
// buildArrayWithSpreads, and `=>` closures via emitClosureBody. Without
|
|
3373
|
+
// it here an indirect caller's single array arg arrives AS the rest array
|
|
3374
|
+
// (spread one level) instead of `[arg]`. len = clamp(argc-restIdx, 0, restSlots).
|
|
3375
|
+
const restIdx = func?.rest ? sigParams.length - 1 : -1
|
|
3376
|
+
let restLocals = '', restPrelude = ''
|
|
3377
|
+
if (restIdx >= 0) {
|
|
3378
|
+
const restSlots = W - restIdx
|
|
3379
|
+
const stores = []
|
|
3380
|
+
for (let i = 0; i < restSlots; i++)
|
|
3381
|
+
stores.push(`(if (i32.gt_s (local.get $__rlen) (i32.const ${i})) (then (f64.store (i32.add (local.get $__roff) (i32.const ${i * 8})) (local.get $__a${restIdx + i}))))`)
|
|
3382
|
+
restLocals = '(local $__rlen i32) (local $__roff i32) '
|
|
3383
|
+
restPrelude =
|
|
3384
|
+
`(local.set $__rlen (select (i32.sub (local.get $__argc) (i32.const ${restIdx})) (i32.const 0) (i32.gt_s (local.get $__argc) (i32.const ${restIdx})))) ` +
|
|
3385
|
+
`(if (i32.gt_s (local.get $__rlen) (i32.const ${restSlots})) (then (local.set $__rlen (i32.const ${restSlots})))) ` +
|
|
3386
|
+
`(local.set $__roff (call $__alloc_hdr (local.get $__rlen) (local.get $__rlen))) ` +
|
|
3387
|
+
stores.join(' ') + ' '
|
|
3388
|
+
}
|
|
3389
|
+
// Forward fixed slots (i32 via trunc_sat); the rest slot → packed array ptr.
|
|
3390
|
+
const fwd = sigParams.map((p, i) =>
|
|
3391
|
+
i === restIdx
|
|
3392
|
+
? `(call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $__roff))`
|
|
3393
|
+
: p.type === 'i32'
|
|
3394
|
+
? `(i32.trunc_sat_f64_s (local.get $__a${i}))`
|
|
3395
|
+
: `(local.get $__a${i})`).join(' ')
|
|
3396
|
+
if ((func?.sig.results.length || 1) > 1) {
|
|
3397
|
+
const n = func.sig.results.length
|
|
3398
|
+
const arr = `${T}retarr`
|
|
3399
|
+
const temps = Array.from({ length: n }, (_, i) => `${T}ret${i}`)
|
|
3400
|
+
const tempLocals = temps.map(name => `(local $${name} f64)`).join(' ')
|
|
3401
|
+
const stores = temps.map((name, i) =>
|
|
3402
|
+
`(f64.store (i32.add (local.get $${arr}) (i32.const ${i * 8})) (local.get $${name}))`
|
|
3403
|
+
).join(' ')
|
|
3404
|
+
const capture = temps.slice().reverse().map(name => `(local.set $${name})`).join(' ')
|
|
3405
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (local $${arr} i32) ${tempLocals} ${restLocals}${restPrelude}(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})))`
|
|
3406
|
+
inc(trampolineName, '__alloc', '__mkptr', ...(restIdx >= 0 ? ['__alloc_hdr'] : []))
|
|
3407
|
+
} else {
|
|
3408
|
+
// Rebox the inner result into the uniform closure ABI (always f64).
|
|
3409
|
+
const resType = func?.sig.results[0]
|
|
3410
|
+
const callExpr = `(call $${node} ${fwd})`
|
|
3411
|
+
// A pointer-returning func carries its result as the raw i32 offset
|
|
3412
|
+
// (sig.ptrKind names the heap kind). Rebox it as a NaN-boxed pointer
|
|
3413
|
+
// with its tag — same as the boundary wrapper (synthesizeBoundaryWrappers).
|
|
3414
|
+
// Numeric `f64.convert_i32_s` here would turn the offset into a plain
|
|
3415
|
+
// number, silently losing the pointer (a Map came back as e.g. 480360.0,
|
|
3416
|
+
// so a caller's `for…of`/`.size` saw a number and read nothing).
|
|
3417
|
+
const ptrResult = func?.sig.ptrKind != null
|
|
3418
|
+
const wrapped = ptrResult
|
|
3419
|
+
? `(call $__mkptr (i32.const ${valKindToPtr(func.sig.ptrKind)}) (i32.const ${func.sig.ptrAux ?? 0}) ${callExpr})`
|
|
3420
|
+
: resType === 'i32'
|
|
3421
|
+
? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
|
|
3422
|
+
: resType === 'i64'
|
|
3423
|
+
? `(f64.reinterpret_i64 ${callExpr})`
|
|
3424
|
+
: callExpr
|
|
3425
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${restLocals}${restPrelude}${wrapped})`
|
|
3426
|
+
inc(trampolineName, ...(ptrResult ? ['__mkptr'] : []), ...(restIdx >= 0 ? ['__alloc_hdr', '__mkptr'] : []))
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
let idx = ctx.closure.table.indexOf(trampolineName)
|
|
3430
|
+
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
|
|
3431
|
+
const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
3432
|
+
ir.closureFuncIdx = idx
|
|
3433
|
+
return ir
|
|
3434
|
+
}
|
|
3435
|
+
// Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision.
|
|
3436
|
+
// `handler.length` distinguishes the two flavors of entry: arity-0 handlers
|
|
3437
|
+
// are constants (e.g. `math.PI` → emits `f64.const PI`) and can be invoked
|
|
3438
|
+
// directly here. Arity-≥1 handlers expect the surrounding call node, so
|
|
3439
|
+
// bare-name use of them is a first-class-value reference — wrap as a closure.
|
|
3440
|
+
if (node.includes('.') && ctx.core.emit[node]) {
|
|
3441
|
+
const handler = ctx.core.emit[node]
|
|
3442
|
+
const isCallable = handler.length > 0
|
|
3443
|
+
return isCallable ? builtinFunctionValue(node) : handler()
|
|
3444
|
+
}
|
|
3445
|
+
// Auto-import known host globals (WebAssembly, globalThis, etc.). Emit only
|
|
3446
|
+
// records the usage; the `(import "env" … (global … i64))` node is drained
|
|
3447
|
+
// into ctx.module.imports at assembly (compile/index.js), the same way
|
|
3448
|
+
// ctx.core.jsstring is — emit does not own ctx.scope / ctx.module sections.
|
|
3449
|
+
// Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed external-ref
|
|
3450
|
+
// payload across the wasm↔JS global boundary (same hazard as env.print —
|
|
3451
|
+
// see module/console.js header). asF64() reinterprets to f64 at each read.
|
|
3452
|
+
if (HOST_GLOBALS.has(node) && !isBoundName(node) && !isGlobal(node)) {
|
|
3453
|
+
if (ctx.transform.host === 'wasi') err(`host:'wasi': reference to host global \`${node}\` requires an env import. Remove the reference or use host:'js'.`)
|
|
3454
|
+
ctx.features.external = true
|
|
3455
|
+
ctx.core.hostGlobals.add(node)
|
|
3456
|
+
return typed(['global.get', `$${node}`], 'i64')
|
|
3457
|
+
}
|
|
3458
|
+
const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
|
|
3459
|
+
return typed(['local.get', `$${node}`], t)
|
|
3460
|
+
}
|
|
3461
|
+
if (!Array.isArray(node)) return typed(['f64.const', 0], 'f64')
|
|
3462
|
+
|
|
3463
|
+
const [op, ...args] = node
|
|
3464
|
+
// WASM IR passthrough: internally-generated IR nodes (from statement flattening) pass through
|
|
3465
|
+
if (typeof op === 'string' && !ctx.core.emit[op] && (op.includes('.') || WASM_OPS.has(op))) return node
|
|
3466
|
+
|
|
3467
|
+
// Self-describing bigint literal (`normalizeBigints`, used at the host→kernel AST
|
|
3468
|
+
// boundary). args[0] is the unsigned-64 decimal computed host-side, passed straight
|
|
3469
|
+
// to i64.const — no in-kernel parse, byte-identical to the raw-primitive path above.
|
|
3470
|
+
if (op === 'bigint') return typed(['f64.reinterpret_i64', ['i64.const', args[0]]], 'f64')
|
|
3471
|
+
|
|
3472
|
+
// Self-describing NaN literal — same reason bigints are self-describing: a raw NaN
|
|
3473
|
+
// number is NaN-boxing-ambiguous and degrades to 0 across the self-host kernel's
|
|
3474
|
+
// value/marshalling boundary. The `NaN` global resolves to this (prepare) instead
|
|
3475
|
+
// of a `[, NaN]` literal; watr emits the canonical quiet NaN. (Infinity is a normal
|
|
3476
|
+
// f64 and survives, so it stays a plain literal.)
|
|
3477
|
+
if (op === 'nan') return typed(['f64.const', 'nan'], 'f64')
|
|
3478
|
+
|
|
3479
|
+
// Self-describing boolean literal (`normalizeBigints` at the host→kernel boundary).
|
|
3480
|
+
// A raw `true`/`false` in the marshalled AST coerces to the number 1/0 and loses its
|
|
3481
|
+
// VAL.BOOL kind, so the kernel returns a plain f64 instead of a NaN-boxed boolean.
|
|
3482
|
+
// args[0] is 1/0 (prepare may wrap it as a `[, 1]` literal node) — emit it as that
|
|
3483
|
+
// working rep; the BOOL boxing happens at the boundary via valTypeOf('bool')=VAL.BOOL.
|
|
3484
|
+
if (op === 'bool') return emit(args[0])
|
|
3485
|
+
|
|
3486
|
+
// Literal node [, value] — handle null/undefined values
|
|
3487
|
+
if (op == null && args.length === 1) {
|
|
3488
|
+
const v = args[0]
|
|
3489
|
+
return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
|
|
3493
|
+
// Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
|
|
3494
|
+
// with the optional already replaced by a non-optional access on a guarded temp.
|
|
3495
|
+
if (op === '.' || op === '[]' || op === '()') {
|
|
3496
|
+
const lifted = liftOptionalChain(node)
|
|
3497
|
+
if (lifted) return lifted
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// `let`/`const` dispatch directly to the imported emitDecl rather than through the
|
|
3501
|
+
// ctx.core.emit table reference: under self-host the table reference is a closure value,
|
|
3502
|
+
// and a runtime spread of >8 args into a closure call silently drops arguments — so a
|
|
3503
|
+
// `let` with >8 expression-init declarators (e.g. an SROA prologue loading 16 typed-array
|
|
3504
|
+
// slots) lost everything past the 8th. A direct call to the module-local binding compiles
|
|
3505
|
+
// as a real direct call, which marshals all args.
|
|
3506
|
+
if (op === 'let' || op === 'const') return emitDecl(...args)
|
|
3507
|
+
const handler = ctx.core.emit[op]
|
|
3508
|
+
if (!handler) err(`Unknown op: ${op}`)
|
|
3509
|
+
const ir = handler(...args)
|
|
3510
|
+
if (ir && ir.type === 'f64' && valTypeOf(node) === VAL.NUMBER) ir.valKind = VAL.NUMBER
|
|
3511
|
+
return ir
|
|
3512
|
+
}
|