jz 0.4.0 → 0.5.1
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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/emit.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AST → WASM IR emission.
|
|
3
3
|
*
|
|
4
4
|
* # Stage contract
|
|
5
|
-
* IN: prepared AST node + ctx state (func.locals, func.
|
|
5
|
+
* IN: prepared AST node + ctx state (func.locals, func.localReps, types.typedElem, etc.)
|
|
6
6
|
* OUT: IR node (array) with `.type` ('i32' | 'f64' | 'void'). For statements, a flat
|
|
7
7
|
* list of WASM instructions (no type tag).
|
|
8
8
|
* NO-MUTATE: emit does not rewrite the AST. Side effects go to ctx.runtime.*,
|
|
@@ -23,32 +23,50 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { ctx, err, inc, PTR } from './ctx.js'
|
|
26
|
-
import { T, VAL, nonNegIntLiteral, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
|
|
27
26
|
import {
|
|
27
|
+
T, VAL, nonNegIntLiteral, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey,
|
|
28
|
+
// AST predicates (formerly in src/ast.js)
|
|
28
29
|
isReassigned, hasOwnContinue, hasOwnBreakOrContinue, containsNestedClosure,
|
|
29
30
|
containsNestedLoop, nestedSmallLoopBudget, containsDeclOf, cloneWithSubst,
|
|
30
31
|
containsKnownTypedArrayIndex, smallConstForTripCount, isTerminator,
|
|
32
|
+
inBoundsCharCodeAt,
|
|
31
33
|
MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
|
|
32
|
-
} from './
|
|
34
|
+
} from './analyze.js'
|
|
33
35
|
import {
|
|
34
36
|
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
35
37
|
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
36
38
|
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
37
39
|
mkPtrIR, ptrOffsetIR, ptrTypeIR,
|
|
38
|
-
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64,
|
|
40
|
+
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64,
|
|
39
41
|
truthyIR, toBoolFromEmitted, isPostfix,
|
|
40
42
|
isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
|
|
41
43
|
temp, tempI32, tempI64, allocPtr,
|
|
42
|
-
boxedAddr, readVar, writeVar, isNullish,
|
|
44
|
+
boxedAddr, readVar, writeVar, isNullish, isBoolAtom,
|
|
43
45
|
isLiteralStr, resolveValType, isFuncRef,
|
|
44
46
|
multiCount, loopTop, flat,
|
|
45
|
-
reconstructArgsWithSpreads,
|
|
47
|
+
reconstructArgsWithSpreads, tcoTailRewrite,
|
|
46
48
|
} from './ir.js'
|
|
49
|
+
import { typeofPredicate } from './infer.js'
|
|
47
50
|
|
|
48
51
|
// Current emission "expect" mode ('void' or null); set by emit(), read by compound-assignment emitters
|
|
49
52
|
// to decide whether to emit a value-returning or side-effect-only form.
|
|
50
53
|
let _expect = null
|
|
51
54
|
|
|
55
|
+
// A genuine i32 *number* — safe for the i32 fast path in arithmetic/bitwise
|
|
56
|
+
// operators. An unboxed pointer (object/array/string/closure local kept as a
|
|
57
|
+
// raw i32 handle) is *also* i32-typed but carries `.ptrKind`; treating it as a
|
|
58
|
+
// number would compute on raw pointer bits. A ptrKind-carrying operand must
|
|
59
|
+
// instead route through ToNumber (`toNumF64`), which performs ToPrimitive.
|
|
60
|
+
const isI32Num = (v) => v.type === 'i32' && v.ptrKind == null
|
|
61
|
+
|
|
62
|
+
// An operand whose uint32 value can be *observed as a JS number* — a `>>>`
|
|
63
|
+
// result, an `unsignedResult` call, or an unsigned i32.const. Its magnitude can
|
|
64
|
+
// exceed signed-i32 range, so wrapping i32 arithmetic would corrupt it; widen to
|
|
65
|
+
// f64 instead. A `.wrapSafe` operand is also unsigned but is a `narrowUint32`
|
|
66
|
+
// accumulator read proven to be re-truncated by a `>>>` (ToUint32) sink at every
|
|
67
|
+
// use — wrapping is exactly its intended semantics, so it stays on the i32 path.
|
|
68
|
+
const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
|
|
69
|
+
|
|
52
70
|
const FIRST_CLASS_UNARY_MATH = {
|
|
53
71
|
'math.abs': 'f64.abs',
|
|
54
72
|
'math.sqrt': 'f64.sqrt',
|
|
@@ -80,14 +98,30 @@ function builtinFunctionValue(name) {
|
|
|
80
98
|
const emitNeg = (a) => {
|
|
81
99
|
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
82
100
|
const v = emit(a)
|
|
83
|
-
return isLit(v) ? emitNum(-litVal(v)) : v
|
|
101
|
+
return isLit(v) ? emitNum(-litVal(v)) : isI32Num(v)
|
|
84
102
|
? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32')
|
|
85
103
|
: typed(['f64.neg', toNumF64(a, v)], 'f64')
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/** Try constant-folding binary arith: returns emitNum(result) or null. */
|
|
107
|
+
// `.unsigned` literals carry a uint32 value whose i32 `litVal` is its *signed* bit
|
|
108
|
+
// pattern (e.g. `-1` for 4294967295), so folding them through `fn` numerically would
|
|
109
|
+
// be wrong. Bail to the runtime path — the arithmetic handlers widen unsigned operands
|
|
110
|
+
// to f64 (convert_i32_u), reproducing the JS-spec result.
|
|
89
111
|
const foldConst = (va, vb, fn, guard) =>
|
|
90
|
-
isLit(va) && isLit(vb) && (!guard || guard(litVal(vb)))
|
|
112
|
+
isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned && (!guard || guard(litVal(vb)))
|
|
113
|
+
? emitNum(fn(litVal(va), litVal(vb))) : null
|
|
114
|
+
|
|
115
|
+
// JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
|
|
116
|
+
// Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
|
|
117
|
+
// exact product stays f64-exact, i.e. |product| <= 2^53. Two i32 operands can
|
|
118
|
+
// reach 2^62, so `i32.mul` is sound only when one side is a literal small
|
|
119
|
+
// enough that, against the full i32 range (2^31) of the other, the product
|
|
120
|
+
// holds within 2^53 — i.e. |literal| <= 2^22. Keeps index arithmetic (`i*4`,
|
|
121
|
+
// `row*16`) on `i32.mul` while routing hash-mix-scale products to `f64.mul`.
|
|
122
|
+
const mulFitsI32 = (va, vb) =>
|
|
123
|
+
(isLit(va) && Math.abs(litVal(va)) <= 0x400000) ||
|
|
124
|
+
(isLit(vb) && Math.abs(litVal(vb)) <= 0x400000)
|
|
91
125
|
|
|
92
126
|
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
93
127
|
export function emitTypeofCmp(a, b, cmpOp) {
|
|
@@ -102,6 +136,10 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
102
136
|
const eq = cmpOp === 'eq'
|
|
103
137
|
|
|
104
138
|
if (code === -1) {
|
|
139
|
+
// A VAL.BOOL value rides in the f64 0/1 carrier, so it would pass the
|
|
140
|
+
// `v===v` number test — but it is typeof "boolean", not "number".
|
|
141
|
+
if (resolveValType(typeofExpr, valTypeOf, lookupValType) === VAL.BOOL)
|
|
142
|
+
return typed(['i32.const', eq ? 0 : 1], 'i32')
|
|
105
143
|
return typed(eq
|
|
106
144
|
? ['f64.eq', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
107
145
|
: ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
|
|
@@ -118,7 +156,12 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
118
156
|
return typed(eq ? check : ['i32.eqz', check], 'i32')
|
|
119
157
|
}
|
|
120
158
|
if (code === -4) {
|
|
121
|
-
|
|
159
|
+
// boolean. Statically VAL.BOOL → true; any other known type → false.
|
|
160
|
+
const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
|
|
161
|
+
if (vt) return typed(['i32.const', (vt === VAL.BOOL) === eq ? 1 : 0], 'i32')
|
|
162
|
+
// Unknown static type: boolean iff the carrier is a boxed-boolean atom.
|
|
163
|
+
const isBool = isBoolAtom(['local.tee', `$${t}`, va])
|
|
164
|
+
return typed(eq ? isBool : ['i32.eqz', isBool], 'i32')
|
|
122
165
|
}
|
|
123
166
|
if (code === -5) {
|
|
124
167
|
inc('__ptr_type')
|
|
@@ -157,9 +200,22 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
157
200
|
return null
|
|
158
201
|
}
|
|
159
202
|
|
|
203
|
+
/** Stringify a VAL.BOOL operand to "true"/"false" (f64 string pointer). The
|
|
204
|
+
* boolean rides the cheap 0/1 carrier, so we runtime-select between the two
|
|
205
|
+
* interned literals; a constant operand folds to a single literal downstream. */
|
|
206
|
+
export const emitBoolStr = (node) =>
|
|
207
|
+
typed(['select', asF64(emit(['str', 'true'])), asF64(emit(['str', 'false'])), truthyIR(emit(node))], 'f64')
|
|
208
|
+
|
|
160
209
|
const CMP_SET = new Set(['>', '<', '>=', '<=', '==', '!=', '!'])
|
|
161
210
|
const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
|
|
162
211
|
|
|
212
|
+
// Map/Set methods whose generic (`.${method}`) emitter assumes a collection
|
|
213
|
+
// receiver and dereferences a key/value argument. Every one needs ≥1 argument
|
|
214
|
+
// (`.get(k)` / `.has(v)` / `.add(v)` / `.delete(v)` / `.set(k[,v])`), so a
|
|
215
|
+
// zero-arg call on a not-proven-collection receiver cannot be the collection
|
|
216
|
+
// op — it is a user/closure method and must not reach the collection emitter.
|
|
217
|
+
const COLLECTION_METHODS = new Set(['get', 'set', 'has', 'add', 'delete'])
|
|
218
|
+
|
|
163
219
|
// Pointer kinds for which JS `==` / `!=` is pure reference equality — i.e. i64 bit
|
|
164
220
|
// compare of the NaN-box is equivalent to __eq. Excludes STRING (content compare for
|
|
165
221
|
// heap strings) and BIGINT (content compare).
|
|
@@ -174,6 +230,23 @@ function stringLiteral(node) {
|
|
|
174
230
|
return null
|
|
175
231
|
}
|
|
176
232
|
|
|
233
|
+
// Index expressions where peepholing `s[k] === 'X'` to char-byte compare is
|
|
234
|
+
// semantics-preserving: must produce a non-negative *integer* at run time so
|
|
235
|
+
// `__str_byteLen u> k` bounds-checks the same range JS would. Out-of-range
|
|
236
|
+
// (negative or ≥ len) falls into the `else 0` arm — matches `undefined === 'X'`.
|
|
237
|
+
function intIndexIR(key) {
|
|
238
|
+
const lit = nonNegIntLiteral(key)
|
|
239
|
+
if (lit != null) return ['i32.const', lit]
|
|
240
|
+
// intCertain name: forward-prop says every defining RHS is integer-shaped.
|
|
241
|
+
// Captures loop variables (`for(let i=0;;i++)`), `let k = j + 1`, etc.
|
|
242
|
+
if (typeof key === 'string' && repOf(key)?.intCertain) return asI32(emit(key))
|
|
243
|
+
// intCertain schema slot read `o.x`: every observed write is integer-shaped,
|
|
244
|
+
// so the loaded f64 represents an int — fold into the byte-compare fast path.
|
|
245
|
+
if (Array.isArray(key) && key[0] === '.' && typeof key[1] === 'string' && typeof key[2] === 'string' &&
|
|
246
|
+
ctx.schema.slotIntCertainAt?.(key[1], key[2]) === true) return asI32(emit(key))
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
178
251
|
const leftLit = stringLiteral(a)
|
|
179
252
|
const rightLit = stringLiteral(b)
|
|
@@ -188,8 +261,8 @@ function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
|
188
261
|
if ([...lit].some(c => c.charCodeAt(0) > 0x7F)) return null
|
|
189
262
|
|
|
190
263
|
const [, obj, key] = indexed
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
264
|
+
const idxIR = intIndexIR(key)
|
|
265
|
+
if (idxIR == null) return null
|
|
193
266
|
|
|
194
267
|
const vt = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
|
|
195
268
|
if (vt && vt !== VAL.STRING) return null
|
|
@@ -202,30 +275,102 @@ function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
|
202
275
|
// Single-char literal: compare byte directly, skipping __str_idx allocation.
|
|
203
276
|
if (lit.length !== 1 || !ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
|
|
204
277
|
|
|
205
|
-
|
|
278
|
+
// Stash the index in a local when it isn't a constant — bounds + load both reference it.
|
|
279
|
+
const isConstIdx = Array.isArray(idxIR) && idxIR[0] === 'i32.const'
|
|
280
|
+
let idxRefIR = idxIR, idxBindIR = null
|
|
281
|
+
if (!isConstIdx) {
|
|
282
|
+
const idxTmp = tempI32('si')
|
|
283
|
+
idxBindIR = ['local.set', `$${idxTmp}`, idxIR]
|
|
284
|
+
idxRefIR = ['local.get', `$${idxTmp}`]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const ptr = temp('sc')
|
|
206
288
|
inc('__str_byteLen', '__char_at')
|
|
207
289
|
const charEq = ['if', ['result', 'i32'],
|
|
208
|
-
['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]],
|
|
209
|
-
['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]],
|
|
290
|
+
['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], idxRefIR],
|
|
291
|
+
['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR], ['i32.const', lit.charCodeAt(0)]]],
|
|
210
292
|
['else', ['i32.const', 0]]]
|
|
211
293
|
|
|
294
|
+
const prelude = idxBindIR ? [['local.set', `$${ptr}`, asF64(emit(obj))], idxBindIR] : [['local.set', `$${ptr}`, asF64(emit(obj))]]
|
|
295
|
+
|
|
212
296
|
if (vt === VAL.STRING) {
|
|
213
|
-
return typed(['block', ['result', 'i32'],
|
|
214
|
-
['local.set', `$${ptr}`, asF64(emit(obj))],
|
|
215
|
-
finish(charEq)], 'i32')
|
|
297
|
+
return typed(['block', ['result', 'i32'], ...prelude, finish(charEq)], 'i32')
|
|
216
298
|
}
|
|
217
299
|
|
|
218
300
|
inc('__ptr_type', '__typed_idx', '__eq')
|
|
219
301
|
const genericEq = ['call', '$__eq',
|
|
220
|
-
['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]],
|
|
302
|
+
['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxRefIR]],
|
|
221
303
|
asI64(emit(['str', lit]))]
|
|
222
304
|
const cmp = ['if', ['result', 'i32'],
|
|
223
305
|
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], ['i32.const', PTR.STRING]],
|
|
224
306
|
['then', charEq],
|
|
225
307
|
['else', genericEq]]
|
|
308
|
+
return typed(['block', ['result', 'i32'], ...prelude, finish(cmp)], 'i32')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// `<str>.{substr,substring,slice}(...) === <other>` whose substring is consumed
|
|
312
|
+
// only by the equality: materialising it (an __alloc + byte copy) is pure waste.
|
|
313
|
+
// Fuse to __str_{substring,slice}_eq, which clamp the range like the method then
|
|
314
|
+
// byte-compare it against `other` in place. Sibling to emitSingleCharIndexCmp,
|
|
315
|
+
// tried at the same `==`/`!=` sites. Motivating hot path: the parser keyword
|
|
316
|
+
// scan, `cur.substr(i,l) === keyword`.
|
|
317
|
+
function emitSubstringEqCmp(a, b, negate = false) {
|
|
318
|
+
// Post-prepare a multi-arg call keeps its args as one comma list; a single
|
|
319
|
+
// arg sits bare. Normalise either (and a flat tail, defensively) to a list.
|
|
320
|
+
const callInfo = node => {
|
|
321
|
+
if (!Array.isArray(node) || node[0] !== '()') return null
|
|
322
|
+
const callee = node[1]
|
|
323
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return null
|
|
324
|
+
const method = callee[2]
|
|
325
|
+
if (method !== 'substr' && method !== 'substring' && method !== 'slice') return null
|
|
326
|
+
let args = node.slice(2)
|
|
327
|
+
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') args = args[0].slice(1)
|
|
328
|
+
while (args.length && args[args.length - 1] == null) args = args.slice(0, -1)
|
|
329
|
+
return { recv: callee[1], method, args }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let info = callInfo(a), other = b, callIsLeft = true
|
|
333
|
+
if (!info) { info = callInfo(b); other = a; callIsLeft = false }
|
|
334
|
+
if (!info) return null
|
|
335
|
+
const { recv, method, args } = info
|
|
336
|
+
if (args.length > 2) return null
|
|
337
|
+
if (!ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
|
|
338
|
+
|
|
339
|
+
// The receiver must be a string. `substr`/`substring` name string-only methods,
|
|
340
|
+
// so an unknown receiver is safe — the normal `.substr`/`.substring` emitter
|
|
341
|
+
// assumes a string too. `slice` is also Array.prototype.slice — require a
|
|
342
|
+
// statically-known STRING there. A known non-string receiver bails always.
|
|
343
|
+
const vt = resolveValType(recv, valTypeOf, lookupValType)
|
|
344
|
+
if (vt && vt !== VAL.STRING) return null
|
|
345
|
+
if (method === 'slice' && vt !== VAL.STRING) return null
|
|
346
|
+
|
|
347
|
+
const helper = method === 'slice' ? '__str_slice_eq' : '__str_substring_eq'
|
|
348
|
+
inc(helper)
|
|
349
|
+
|
|
350
|
+
// Absent end → byteLen: pass i32 max — every clamp arm floors it to the length.
|
|
351
|
+
const TO_END = ['i32.const', 0x7FFFFFFF]
|
|
352
|
+
let startIR, endIR
|
|
353
|
+
if (method === 'substr' && args[1] != null) {
|
|
354
|
+
// substr's 2nd arg is a length: end = start + length, so start reads twice.
|
|
355
|
+
const s = tempI32('subS')
|
|
356
|
+
startIR = ['local.tee', `$${s}`, args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))]
|
|
357
|
+
endIR = ['i32.add', ['local.get', `$${s}`], asI32(emit(args[1]))]
|
|
358
|
+
} else {
|
|
359
|
+
startIR = args[0] == null ? ['i32.const', 0] : asI32(emit(args[0]))
|
|
360
|
+
endIR = args[1] == null ? TO_END : asI32(emit(args[1]))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const finish = expr => negate ? ['i32.eqz', expr] : expr
|
|
364
|
+
|
|
365
|
+
if (callIsLeft)
|
|
366
|
+
return typed(finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR, asI64(emit(other))]), 'i32')
|
|
367
|
+
|
|
368
|
+
// `other` is the source-left operand — evaluate it first to preserve order.
|
|
369
|
+
const o = temp('subO')
|
|
226
370
|
return typed(['block', ['result', 'i32'],
|
|
227
|
-
['local.set', `$${
|
|
228
|
-
finish(
|
|
371
|
+
['local.set', `$${o}`, asF64(emit(other))],
|
|
372
|
+
finish(['call', `$${helper}`, asI64(emit(recv)), startIR, endIR,
|
|
373
|
+
['i64.reinterpret_f64', ['local.get', `$${o}`]]])], 'i32')
|
|
229
374
|
}
|
|
230
375
|
|
|
231
376
|
// === Flow-sensitive type refinement ===
|
|
@@ -235,7 +380,7 @@ const TYPEOF_CODE_TO_VAL = { [-1]: VAL.NUMBER, [-2]: VAL.STRING, [-6]: VAL.CLOSU
|
|
|
235
380
|
|
|
236
381
|
/** Extract refinements from a boolean condition AST.
|
|
237
382
|
* `sense`: true = refine for then-branch, false = refine for else-branch (i.e. cond inverted).
|
|
238
|
-
* Returns a Map<name, VAL>. Walks && / || / ! accordingly. */
|
|
383
|
+
* Returns a Map<name, {val?: VAL, notString?: true}>. Walks && / || / ! accordingly. */
|
|
239
384
|
function extractRefinements(cond, out, sense = true) {
|
|
240
385
|
if (!Array.isArray(cond)) return out
|
|
241
386
|
const op = cond[0]
|
|
@@ -247,29 +392,51 @@ function extractRefinements(cond, out, sense = true) {
|
|
|
247
392
|
if (op === '||' && !sense) { extractRefinements(cond[1], out, false); extractRefinements(cond[2], out, false); return out }
|
|
248
393
|
// typeof x == 'number' | 'string' | 'function' — sense must be positive for "==", negative for "!="
|
|
249
394
|
if ((op === '==' || op === '===' || op === '!=' || op === '!==')) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
395
|
+
const tp = typeofPredicate(cond)
|
|
396
|
+
if (tp) {
|
|
397
|
+
const wantPositive = tp.eq ? sense : !sense
|
|
398
|
+
if (wantPositive) {
|
|
399
|
+
const val = TYPEOF_CODE_TO_VAL[tp.code]
|
|
400
|
+
if (val) mergeRefinement(out, tp.name, { val })
|
|
401
|
+
} else if (tp.code === 'string' || tp.code === -2) {
|
|
402
|
+
// Negative branch of typeof-string guard (e.g. post `if (typeof x === 'string') return`)
|
|
403
|
+
// proves the binding is not a primitive string in the suffix scope — feeds B4's
|
|
404
|
+
// length / subscript dispatch elision the same way write-shape evidence does.
|
|
405
|
+
mergeRefinement(out, tp.name, { notString: true })
|
|
406
|
+
}
|
|
259
407
|
}
|
|
260
408
|
return out
|
|
261
409
|
}
|
|
262
|
-
//
|
|
263
|
-
// Callee may be the flattened string 'Array.isArray' or the raw ['.', 'Array',
|
|
410
|
+
// Type-predicate calls under positive sense — refine by the asserted VAL.
|
|
411
|
+
// Callee may be the flattened string 'Array.isArray' or the raw ['.', 'Array',
|
|
412
|
+
// 'isArray'] pair; __is_map / __is_set / __is_typed come from jzify's
|
|
413
|
+
// instanceof lowering as a bare string callee.
|
|
264
414
|
if (op === '()' && sense && typeof cond[2] === 'string') {
|
|
265
415
|
const callee = cond[1]
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
if (isArr) { out.set(cond[2], VAL.ARRAY); return out }
|
|
416
|
+
const val = predicateRefinement(callee)
|
|
417
|
+
if (val != null) { mergeRefinement(out, cond[2], { val }); return out }
|
|
269
418
|
}
|
|
270
419
|
return out
|
|
271
420
|
}
|
|
272
421
|
|
|
422
|
+
/** Map a call-callee shape to the VAL kind it asserts under positive sense, or null. */
|
|
423
|
+
function predicateRefinement(callee) {
|
|
424
|
+
if (callee === 'Array.isArray') return VAL.ARRAY
|
|
425
|
+
if (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Array' && callee[2] === 'isArray')
|
|
426
|
+
return VAL.ARRAY
|
|
427
|
+
if (callee === '__is_map') return VAL.MAP
|
|
428
|
+
if (callee === '__is_set') return VAL.SET
|
|
429
|
+
if (callee === '__is_typed') return VAL.TYPED
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Merge a refinement fact into the per-name slot. Later facts override; non-overlapping
|
|
434
|
+
* fields union. Keeps the call-side simple (always assign through this). */
|
|
435
|
+
function mergeRefinement(out, name, fact) {
|
|
436
|
+
const cur = out.get(name)
|
|
437
|
+
out.set(name, cur ? { ...cur, ...fact } : fact)
|
|
438
|
+
}
|
|
439
|
+
|
|
273
440
|
function unrollSmallConstFor(init, cond, step, body) {
|
|
274
441
|
const end = smallConstForTripCount(init, cond, step)
|
|
275
442
|
if (end == null) return null
|
|
@@ -294,19 +461,31 @@ function canThrow(body, seen = new Set()) {
|
|
|
294
461
|
if (op === '=>') return false
|
|
295
462
|
if (op === '()') {
|
|
296
463
|
const callee = body[1]
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
464
|
+
// A call can throw unless we can see the whole callee and prove it can't:
|
|
465
|
+
// only direct calls into a resolvable, non-raw function body are traceable.
|
|
466
|
+
// Indirect/method/builtin calls (callee not a plain name, or a name we can't
|
|
467
|
+
// resolve) are conservatively throwing — a user `try` must wrap them.
|
|
468
|
+
if (typeof callee !== 'string') return true
|
|
469
|
+
const bodyName = ctx.func.directClosures?.get(callee)
|
|
470
|
+
const f = ctx.func.map?.get(bodyName || callee)
|
|
471
|
+
if (!f?.body || f.raw) return true
|
|
472
|
+
if (!seen.has(f.name)) {
|
|
473
|
+
seen.add(f.name)
|
|
474
|
+
if (canThrow(f.body, seen)) return true
|
|
304
475
|
}
|
|
305
476
|
}
|
|
306
477
|
for (let i = 1; i < body.length; i++) if (canThrow(body[i], seen)) return true
|
|
307
478
|
return false
|
|
308
479
|
}
|
|
309
480
|
|
|
481
|
+
const isBoundName = name =>
|
|
482
|
+
ctx.func.locals?.has(name) || ctx.func.current?.params?.some(p => p.name === name)
|
|
483
|
+
|
|
484
|
+
// A source-defined function (carries a body) — as opposed to an imported name,
|
|
485
|
+
// which `ctx.func.names` also holds but which has no body and may legitimately
|
|
486
|
+
// share a name with a built-in emitter (e.g. an imported `parseInt`).
|
|
487
|
+
const isUserFunc = name => !!ctx.func.map.get(name)?.body
|
|
488
|
+
|
|
310
489
|
/** Emit pending `finally` cleanups for an abrupt control-flow exit.
|
|
311
490
|
* Inner cleanups run before outer cleanups. While emitting each cleanup, remove
|
|
312
491
|
* it from the active stack so `return` inside `finally` does not re-enter it. */
|
|
@@ -423,6 +602,19 @@ export function emitDecl(...inits) {
|
|
|
423
602
|
const [, name, init] = i
|
|
424
603
|
if (typeof name !== 'string' || init == null) continue
|
|
425
604
|
|
|
605
|
+
// SRoA flat object: `let o = {a:1, b:2}` — dissolve fields into `o#i`
|
|
606
|
+
// locals, no heap alloc. Each field local ← asF64(value). Reads/writes are
|
|
607
|
+
// rewritten by the `.`/`[]` flat hooks. See scanFlatObjects (analyze.js).
|
|
608
|
+
// Monotonic-extension fields (`o.newProp = …`) carry no literal value —
|
|
609
|
+
// they init to undefined so a read before the write matches JS.
|
|
610
|
+
const flatDecl = ctx.func.flatObjects?.get(name)
|
|
611
|
+
if (flatDecl && Array.isArray(init) && init[0] === '{}') {
|
|
612
|
+
for (let j = 0; j < flatDecl.names.length; j++)
|
|
613
|
+
result.push(['local.set', `$${name}#${j}`,
|
|
614
|
+
flatDecl.values[j] === undefined ? undefExpr() : asF64(emit(flatDecl.values[j]))])
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
|
|
426
618
|
// Multi-value ephemeral destructuring — skip heap alloc when temp is
|
|
427
619
|
// assigned from a multi-value call then immediately destructured element-by-element.
|
|
428
620
|
if (name.startsWith(T) && Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string'
|
|
@@ -457,9 +649,28 @@ export function emitDecl(...inits) {
|
|
|
457
649
|
}
|
|
458
650
|
}
|
|
459
651
|
}
|
|
652
|
+
// No-copy slice view: `let t = s.slice(...)` whose result scanSliceViews
|
|
653
|
+
// proved never escapes — lower the initializer to a SLICE_BIT view instead
|
|
654
|
+
// of a copying slice. Everything downstream treats `t` as an ordinary
|
|
655
|
+
// string. Gated here (not in the analysis) on a statically-known STRING
|
|
656
|
+
// receiver — param types are settled only by emit time — and on plain-local
|
|
657
|
+
// carriers (boxed/global escape); any miss falls back to the copying slice.
|
|
658
|
+
let viewInit = null
|
|
659
|
+
if (ctx.func.sliceViews?.has(name) && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
660
|
+
&& Array.isArray(init) && init[0] === '()'
|
|
661
|
+
&& Array.isArray(init[1]) && init[1][0] === '.' && init[1][2] === 'slice') {
|
|
662
|
+
const recv = init[1][1]
|
|
663
|
+
const recvVt = typeof recv === 'string' ? keyValType(recv) : valTypeOf(recv)
|
|
664
|
+
if (recvVt === VAL.STRING) {
|
|
665
|
+
const raw = init[2]
|
|
666
|
+
const sa = raw == null ? [] : Array.isArray(raw) && raw[0] === ',' ? raw.slice(1) : [raw]
|
|
667
|
+
viewInit = ctx.core.emit['.string:slice#view'](recv, sa[0], sa[1])
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
460
671
|
const isObjLit = Array.isArray(init) && init[0] === '{}'
|
|
461
672
|
if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
|
|
462
|
-
const val = emit(init)
|
|
673
|
+
const val = viewInit || emit(init)
|
|
463
674
|
if (isObjLit) ctx.schema.targetStack.pop()
|
|
464
675
|
// Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
|
|
465
676
|
// Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
|
|
@@ -492,10 +703,12 @@ export function emitDecl(...inits) {
|
|
|
492
703
|
}
|
|
493
704
|
const localType = ctx.func.locals.get(name) || 'f64'
|
|
494
705
|
let ptrKind = repOf(name)?.ptrKind
|
|
706
|
+
// Emit-time rep mutation (lifecycle: analysis → emit transition).
|
|
495
707
|
// Inherit ptrKind from a pointer-ABI RHS: destructure temps (`__d0 = v`) and other
|
|
496
708
|
// fresh let-bindings whose init is already an unboxed pointer. Without this, readVar
|
|
497
709
|
// returns an untyped i32 local.get and later `asF64` emits a numeric convert instead
|
|
498
|
-
// of a ptr-rebox. Safe because emitDecl runs once per let/const binding
|
|
710
|
+
// of a ptr-rebox. Safe because emitDecl runs once per let/const binding — no prior
|
|
711
|
+
// emit-time read could have observed the unset rep.
|
|
499
712
|
if (ptrKind == null && val.ptrKind != null && localType === 'i32' && !ctx.func.boxed?.has(name)) {
|
|
500
713
|
updateRep(name, { ptrKind: val.ptrKind })
|
|
501
714
|
ptrKind = val.ptrKind
|
|
@@ -515,12 +728,13 @@ export function emitDecl(...inits) {
|
|
|
515
728
|
// Unboxed pointer local — extract i32 offset from NaN-boxed f64 via reinterpret, not numeric trunc.
|
|
516
729
|
// CLOSURE init carries funcIdx in val.closureFuncIdx; persist it on the rep so a later
|
|
517
730
|
// asF64 (escape: store, return, indirect-call rebox) reconstructs the correct table slot.
|
|
731
|
+
// Emit-time mutation — analyzeValTypes never sees closureFuncIdx.
|
|
518
732
|
if (ptrKind === VAL.CLOSURE && val.closureFuncIdx != null && repOf(name)?.ptrAux == null)
|
|
519
733
|
updateRep(name, { ptrAux: val.closureFuncIdx })
|
|
520
734
|
coerced = val.ptrKind === ptrKind ? val
|
|
521
735
|
: typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
|
|
522
736
|
} else {
|
|
523
|
-
coerced = localType === 'f64' ? asF64(val) :
|
|
737
|
+
coerced = localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
|
|
524
738
|
}
|
|
525
739
|
if (!(isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length))
|
|
526
740
|
result.push(['local.set', `$${name}`, coerced])
|
|
@@ -547,6 +761,56 @@ export function emitDecl(...inits) {
|
|
|
547
761
|
return result.length === 0 ? null : result.length === 1 ? result[0] : result
|
|
548
762
|
}
|
|
549
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Copy a spread source's elements into a destination array.
|
|
766
|
+
*
|
|
767
|
+
* `dest` is the destination data-base i32 local; `posLocal` the element index to
|
|
768
|
+
* start writing at — advanced by the source length on exit. An ARRAY source is a
|
|
769
|
+
* contiguous block of f64 NaN-boxes, so it copies with a single `memory.copy`; a
|
|
770
|
+
* string/typed source needs a per-element decode. The source's *type* is
|
|
771
|
+
* loop-invariant — it cannot change while the spread runs — so when it is not
|
|
772
|
+
* statically known it is resolved exactly once (one `__ptr_type`) and branched,
|
|
773
|
+
* never re-checked per element. Returns a list of IR instructions.
|
|
774
|
+
*/
|
|
775
|
+
function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
|
|
776
|
+
const srcI64 = () => ['i64.reinterpret_f64', ['local.get', `$${srcLocal}`]]
|
|
777
|
+
const destAddr = idx => ['i32.add', ['local.get', `$${dest}`], ['i32.shl', idx, ['i32.const', 3]]]
|
|
778
|
+
const arrCopy = () => (inc('__ptr_offset'),
|
|
779
|
+
['memory.copy', destAddr(['local.get', `$${posLocal}`]),
|
|
780
|
+
['call', '$__ptr_offset', srcI64()],
|
|
781
|
+
['i32.shl', ['local.get', `$${srcLenLocal}`], ['i32.const', 3]]])
|
|
782
|
+
const scalarLoop = () => {
|
|
783
|
+
const sidx = `${T}sidx${ctx.func.uniq++}`
|
|
784
|
+
ctx.func.locals.set(sidx, 'i32')
|
|
785
|
+
const loopId = ctx.func.uniq++
|
|
786
|
+
const elem = ctx.module.modules['string']
|
|
787
|
+
? ['if', ['result', 'f64'],
|
|
788
|
+
['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
|
|
789
|
+
['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
|
|
790
|
+
['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]]
|
|
791
|
+
: (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
|
|
792
|
+
// Reset the counter on each entry — WASM zeroes locals once at function
|
|
793
|
+
// entry, but this loop re-executes when the spread sits inside a JS loop;
|
|
794
|
+
// a stale `sidx` (= prior srcLen) would skip the copy entirely.
|
|
795
|
+
return ['block', `$break${loopId}`,
|
|
796
|
+
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
797
|
+
['loop', `$loop${loopId}`,
|
|
798
|
+
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
|
|
799
|
+
['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
|
|
800
|
+
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
801
|
+
['br', `$loop${loopId}`]]]
|
|
802
|
+
}
|
|
803
|
+
const advance = ['local.set', `$${posLocal}`,
|
|
804
|
+
['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
|
|
805
|
+
if (staticVT === VAL.ARRAY) return [arrCopy(), advance]
|
|
806
|
+
if (staticVT === VAL.STRING || staticVT === VAL.TYPED) return [scalarLoop(), advance]
|
|
807
|
+
inc('__ptr_type')
|
|
808
|
+
return [['if',
|
|
809
|
+
['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.ARRAY]],
|
|
810
|
+
['then', arrCopy()],
|
|
811
|
+
['else', scalarLoop()]], advance]
|
|
812
|
+
}
|
|
813
|
+
|
|
550
814
|
/**
|
|
551
815
|
* Build an array from items, handling ['__spread', expr] markers.
|
|
552
816
|
* Split into sections (normal arrays and spreads), then copy all into result.
|
|
@@ -581,9 +845,12 @@ export function buildArrayWithSpreads(items) {
|
|
|
581
845
|
sections.push({ type: 'array', items: currentArray })
|
|
582
846
|
}
|
|
583
847
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
848
|
+
// A single all-normal section is a plain literal — defer to the `[` emitter.
|
|
849
|
+
// A single *spread* section is NOT shortcut to `emit(sec.expr)`: that would
|
|
850
|
+
// alias the source, but `[...x]` must yield a fresh array. It falls through
|
|
851
|
+
// to the alloc + emitSpreadCopy path below, which copies.
|
|
852
|
+
if (sections.length === 1 && sections[0].type === 'array') {
|
|
853
|
+
return emit(['[', ...sections[0].items])
|
|
587
854
|
}
|
|
588
855
|
|
|
589
856
|
const len = tempI32('len')
|
|
@@ -591,35 +858,39 @@ export function buildArrayWithSpreads(items) {
|
|
|
591
858
|
const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
|
|
592
859
|
const result = out.local
|
|
593
860
|
|
|
594
|
-
const ir = [
|
|
595
|
-
|
|
596
|
-
]
|
|
861
|
+
const ir = []
|
|
862
|
+
inc('__len')
|
|
597
863
|
|
|
598
|
-
|
|
864
|
+
// Pass 1 — evaluate every section IN SOURCE ORDER into temps. JS spread keeps
|
|
865
|
+
// strict left-to-right order: a later spread whose source mutates an earlier
|
|
866
|
+
// element's input must still observe the pre-mutation value. Array items
|
|
867
|
+
// become per-item f64 temps; spreads become a ptr temp + a cached __len.
|
|
599
868
|
for (const sec of sections) {
|
|
600
|
-
if (sec.type === '
|
|
869
|
+
if (sec.type === 'array') {
|
|
870
|
+
sec.itemLocals = []
|
|
871
|
+
for (let i = 0; i < sec.items.length; i++) {
|
|
872
|
+
const it = `${T}ai${ctx.func.uniq++}`
|
|
873
|
+
ctx.func.locals.set(it, 'f64')
|
|
874
|
+
sec.itemLocals.push(it)
|
|
875
|
+
ir.push(['local.set', `$${it}`, asF64(emit(sec.items[i]))])
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
601
878
|
sec.local = `${T}sp${ctx.func.uniq++}`
|
|
602
879
|
ctx.func.locals.set(sec.local, 'f64')
|
|
603
880
|
sec.lenLocal = `${T}spl${ctx.func.uniq++}`
|
|
604
881
|
ctx.func.locals.set(sec.lenLocal, 'i32')
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
sec.baseLocal = `${T}spb${ctx.func.uniq++}`
|
|
609
|
-
ctx.func.locals.set(sec.baseLocal, 'i32')
|
|
610
|
-
}
|
|
882
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
883
|
+
// emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
|
|
884
|
+
sec.val = multiCount(sec.expr) ? undefined : valTypeOf(sec.expr)
|
|
611
885
|
const n = multiCount(sec.expr)
|
|
612
886
|
ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
ir.push(['local.set', `$${sec.lenLocal}`, ['i32.load', ['i32.sub', ['local.get', `$${sec.baseLocal}`], ['i32.const', 8]]]])
|
|
616
|
-
} else {
|
|
617
|
-
// Cache __len once per spread; reused below for total-len sum and inner copy bound.
|
|
618
|
-
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
619
|
-
}
|
|
887
|
+
// Cache __len once per spread; reused below for total-len sum and the copy.
|
|
888
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
620
889
|
}
|
|
621
890
|
}
|
|
622
891
|
|
|
892
|
+
// Pass 2 — total length (array sections statically sized, spreads cached above).
|
|
893
|
+
ir.push(['local.set', `$${len}`, ['i32.const', 0]])
|
|
623
894
|
for (const sec of sections) {
|
|
624
895
|
if (sec.type === 'array') {
|
|
625
896
|
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
|
|
@@ -628,41 +899,20 @@ export function buildArrayWithSpreads(items) {
|
|
|
628
899
|
}
|
|
629
900
|
}
|
|
630
901
|
|
|
902
|
+
// Pass 3 — allocate exact, then store the pre-evaluated temps.
|
|
631
903
|
ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
|
|
632
|
-
|
|
633
904
|
for (const sec of sections) {
|
|
634
905
|
if (sec.type === 'array') {
|
|
635
|
-
for (
|
|
906
|
+
for (const it of sec.itemLocals) {
|
|
636
907
|
ir.push(
|
|
637
908
|
['f64.store',
|
|
638
909
|
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
639
|
-
|
|
910
|
+
['local.get', `$${it}`]],
|
|
640
911
|
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
|
|
641
912
|
)
|
|
642
913
|
}
|
|
643
914
|
} else {
|
|
644
|
-
|
|
645
|
-
ctx.func.locals.set(sidx, 'i32')
|
|
646
|
-
const loopId = ctx.func.uniq++
|
|
647
|
-
const elemLoad = sec.baseLocal
|
|
648
|
-
? ['f64.load', ['i32.add', ['local.get', `$${sec.baseLocal}`], ['i32.shl', ['local.get', `$${sidx}`], ['i32.const', 3]]]]
|
|
649
|
-
: ctx.module.modules['string']
|
|
650
|
-
? ['if', ['result', 'f64'],
|
|
651
|
-
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]], ['i32.const', PTR.STRING]],
|
|
652
|
-
['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])],
|
|
653
|
-
['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])]]
|
|
654
|
-
: (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])
|
|
655
|
-
ir.push(
|
|
656
|
-
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
657
|
-
['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
|
|
658
|
-
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${slen}`]]],
|
|
659
|
-
['f64.store',
|
|
660
|
-
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
661
|
-
elemLoad],
|
|
662
|
-
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
|
|
663
|
-
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
664
|
-
['br', `$loop${loopId}`]]]
|
|
665
|
-
)
|
|
915
|
+
ir.push(...emitSpreadCopy(result, pos, sec.local, sec.lenLocal, sec.val))
|
|
666
916
|
}
|
|
667
917
|
}
|
|
668
918
|
|
|
@@ -698,13 +948,17 @@ export function emitBody(node) {
|
|
|
698
948
|
// Skip names that are reassigned later — refinement would be unsound past the assignment.
|
|
699
949
|
if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
|
|
700
950
|
const refs = extractRefinements(s[1], new Map(), false)
|
|
701
|
-
for (const [name,
|
|
951
|
+
for (const [name, fact] of refs) {
|
|
702
952
|
let reassigned = false
|
|
703
953
|
for (let j = i + 1; j < stmts.length; j++)
|
|
704
954
|
if (isReassigned(stmts[j], name)) { reassigned = true; break }
|
|
705
955
|
if (reassigned) continue
|
|
706
|
-
|
|
707
|
-
|
|
956
|
+
const cur = ctx.func.refinements.get(name)
|
|
957
|
+
accumulated.push([name, cur])
|
|
958
|
+
// Merge so sibling early-returns layering on the same name compose
|
|
959
|
+
// (e.g. `if (typeof x === 'string') return; if (Array.isArray(x)) return;`
|
|
960
|
+
// leaves both `notString: true` and would-be array exclusion stacked).
|
|
961
|
+
ctx.func.refinements.set(name, cur ? { ...cur, ...fact } : fact)
|
|
708
962
|
}
|
|
709
963
|
}
|
|
710
964
|
}
|
|
@@ -716,23 +970,86 @@ export function emitBody(node) {
|
|
|
716
970
|
return out
|
|
717
971
|
}
|
|
718
972
|
|
|
973
|
+
// A VAL.BOOL value rides the cheap 0/1 numeric carrier, and `ToNumber(bool)` is
|
|
974
|
+
// exactly that carrier — so for relational / loose-equality coercion a boolean
|
|
975
|
+
// behaves identically to a number. Normalize it before the type-directed compare
|
|
976
|
+
// dispatch (the BOOL fact still drives typeof / String / boundary boxing; only
|
|
977
|
+
// these arithmetic-shaped operators read it as numeric).
|
|
978
|
+
const numericVal = vt => vt === VAL.BOOL ? VAL.NUMBER : vt
|
|
979
|
+
|
|
980
|
+
// Primitive value-type classes for strict-equality type-mismatch folding. Two
|
|
981
|
+
// operands of different known classes — when at least one is a primitive — can
|
|
982
|
+
// never be `===` (number/boolean/string/bigint don't cross-coerce under `===`).
|
|
983
|
+
// Two *reference* kinds (array vs object, …) fall through to the shared ref-eq
|
|
984
|
+
// path instead, which already resolves distinct pointers to `false`.
|
|
985
|
+
const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Strict `===`/`!==`. Unlike loose `==`, no coercion: a statically-known type
|
|
989
|
+
* mismatch folds to a constant (`true === 1` → false, `"1" === 1` → false). When
|
|
990
|
+
* the types match — or one side is statically unknown — the result is bit-for-bit
|
|
991
|
+
* identical to loose `==` on same-type operands, so we delegate to it.
|
|
992
|
+
*
|
|
993
|
+
* Two carrier-level limitations remain (documented gaps, not regressions):
|
|
994
|
+
* • booleans and numbers share the 0/1 carrier, so `1 === trueDynamic` can only
|
|
995
|
+
* be told apart when the boolean's type is statically known;
|
|
996
|
+
* • jz unifies `null` and `undefined` into one NaN-boxed sentinel, so
|
|
997
|
+
* `null === undefined` is `true` (same as `==`).
|
|
998
|
+
*/
|
|
999
|
+
function emitStrictEq(a, b, negate) {
|
|
1000
|
+
// `typeof x === 'type'` (prepare rewrote the literal to a numeric code) — typeof
|
|
1001
|
+
// always yields a string, so strict and loose agree; reuse the loose lowering.
|
|
1002
|
+
const tc = emitTypeofCmp(a, b, negate ? 'ne' : 'eq'); if (tc) return tc
|
|
1003
|
+
// Known, differing primitive classes can never be strictly equal.
|
|
1004
|
+
const rawA = resolveValType(a, valTypeOf, lookupValType)
|
|
1005
|
+
const rawB = resolveValType(b, valTypeOf, lookupValType)
|
|
1006
|
+
if (rawA && rawB && rawA !== rawB && (STRICT_PRIM.has(rawA) || STRICT_PRIM.has(rawB)))
|
|
1007
|
+
return emitNum(negate ? 1 : 0)
|
|
1008
|
+
// Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
|
|
1009
|
+
return emitter[negate ? '!=' : '=='](a, b)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
719
1012
|
/** Comparison op factory with constant folding. */
|
|
720
1013
|
const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
721
1014
|
const va = emit(a), vb = emit(b)
|
|
722
|
-
|
|
1015
|
+
// Skip the const-fold for `.unsigned` operands: `litVal` is the signed bit pattern
|
|
1016
|
+
// (-1, not 4294967295), so folding the order would be wrong. Fall through to the
|
|
1017
|
+
// f64 widen path below, which converts each operand by its own signedness.
|
|
1018
|
+
if (isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
|
|
723
1019
|
// String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
|
|
724
1020
|
// (always false), so without this the spec-correct `"a" < "b"` returns 0.
|
|
725
1021
|
// Route both-STRING operands through __str_cmp's three-way result, then apply
|
|
726
1022
|
// the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
|
|
727
|
-
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
728
|
-
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1023
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1024
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
729
1025
|
if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
|
|
730
1026
|
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
731
1027
|
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
732
1028
|
}
|
|
733
1029
|
if (vta === VAL.STRING && vtb === VAL.STRING) {
|
|
734
|
-
|
|
735
|
-
|
|
1030
|
+
return typed([`i32.${i32op}`, ctx.abi.string.ops.cmp(asF64(va), asF64(vb), ctx), ['i32.const', 0]], 'i32')
|
|
1031
|
+
}
|
|
1032
|
+
// Exactly one operand is a known string; the other has no static type, so it
|
|
1033
|
+
// may hold a string pointer at runtime (e.g. `c >= '0'` where `c` came from
|
|
1034
|
+
// `s[i]` on an untyped receiver). JS relational compare is lexicographic only
|
|
1035
|
+
// when *both* sides are strings, else it ToNumbers both. The f64 path below
|
|
1036
|
+
// would compare the unknown side's NaN-boxed string bits as a float (NaN ⇒
|
|
1037
|
+
// always false), so dispatch at runtime on the unknown side: string → __str_cmp
|
|
1038
|
+
// three-way; else ToNumber both. Mirrors `+`'s __is_str_key string dispatch.
|
|
1039
|
+
// Gated on a *known-string* counterpart, so numeric loops (`i < n`) never pay
|
|
1040
|
+
// the check — comparing against a string literal signals string intent.
|
|
1041
|
+
if (((vta === VAL.STRING && vtb == null) || (vtb === VAL.STRING && vta == null)) && ctx.abi.string?.ops?.cmp) {
|
|
1042
|
+
const unkIsA = vta == null
|
|
1043
|
+
const ta = temp('cmp'), tb = temp('cmp')
|
|
1044
|
+
inc('__is_str_key')
|
|
1045
|
+
const getA = typed(['local.get', `$${ta}`], 'f64'), getB = typed(['local.get', `$${tb}`], 'f64')
|
|
1046
|
+
const check = ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${unkIsA ? ta : tb}`]]]
|
|
1047
|
+
const strCmp = [`i32.${i32op}`, ctx.abi.string.ops.cmp(getA, getB, ctx), ['i32.const', 0]]
|
|
1048
|
+
const numCmp = [`f64.${f64op}`, toNumF64(a, getA), toNumF64(b, getB)]
|
|
1049
|
+
return typed(['block', ['result', 'i32'],
|
|
1050
|
+
['local.set', `$${ta}`, asF64(va)],
|
|
1051
|
+
['local.set', `$${tb}`, asF64(vb)],
|
|
1052
|
+
['if', ['result', 'i32'], check, ['then', strCmp], ['else', numCmp]]], 'i32')
|
|
736
1053
|
}
|
|
737
1054
|
if (vta === VAL.DATE || vtb === VAL.DATE) {
|
|
738
1055
|
const dateNum = (node, v, vt) => {
|
|
@@ -748,11 +1065,17 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
748
1065
|
return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
|
|
749
1066
|
if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
|
|
750
1067
|
return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1068
|
+
// An `.unsigned` i32 operand ([0, 2^32)) can't share a signed i32 compare with a
|
|
1069
|
+
// possibly-signed one: mixed sign inverts the order (3 < 0xFFFFFFFF unsigned, but
|
|
1070
|
+
// 3 > -1 signed). Widen to f64, where asF64 converts each operand by its own
|
|
1071
|
+
// signedness (convert_i32_u for unsigned, _s otherwise) to its true numeric value.
|
|
1072
|
+
if (!va.unsigned && !vb.unsigned) {
|
|
1073
|
+
const ai = intConstValue(a), bi = intConstValue(b)
|
|
1074
|
+
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
1075
|
+
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
1076
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${i32op}`, va, vb], 'i32')
|
|
1077
|
+
}
|
|
1078
|
+
return typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
756
1079
|
}
|
|
757
1080
|
|
|
758
1081
|
function needsRelationalToNumber(expr, vt) {
|
|
@@ -885,7 +1208,7 @@ export const emitter = {
|
|
|
885
1208
|
},
|
|
886
1209
|
|
|
887
1210
|
'throw': expr => {
|
|
888
|
-
ctx.runtime.throws = true
|
|
1211
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
889
1212
|
const thrown = temp()
|
|
890
1213
|
return typed(['block',
|
|
891
1214
|
['local.set', `$${thrown}`, asF64(emit(expr))],
|
|
@@ -896,7 +1219,7 @@ export const emitter = {
|
|
|
896
1219
|
'catch': (body, errName, handler) => {
|
|
897
1220
|
if (!canThrow(body)) return emitFlat(body)
|
|
898
1221
|
|
|
899
|
-
ctx.runtime.throws = true
|
|
1222
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
900
1223
|
const id = ctx.func.uniq++
|
|
901
1224
|
ctx.func.locals.set(errName, 'f64')
|
|
902
1225
|
const prev = ctx.func.inTry; ctx.func.inTry = true
|
|
@@ -922,7 +1245,7 @@ export const emitter = {
|
|
|
922
1245
|
return [...bodyIR, ...cleanupIR]
|
|
923
1246
|
}
|
|
924
1247
|
|
|
925
|
-
ctx.runtime.throws = true
|
|
1248
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
926
1249
|
const id = ctx.func.uniq++
|
|
927
1250
|
const errLocal = temp('err')
|
|
928
1251
|
const parentStack = ctx.func.finallyStack || []
|
|
@@ -966,19 +1289,20 @@ export const emitter = {
|
|
|
966
1289
|
const rt = ctx.func.current?.results[0] || 'f64'
|
|
967
1290
|
const pk = ctx.func.current?.ptrKind
|
|
968
1291
|
const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1292
|
+
const ty = pk != null ? 'i32' : rt
|
|
1293
|
+
const tcoed = tcoTailRewrite(ir, ty)
|
|
1294
|
+
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
1295
|
+
return typed(tcoed, 'void')
|
|
1296
|
+
}
|
|
972
1297
|
if (finalizers.length > 0) {
|
|
973
|
-
const ty = pk != null ? 'i32' : rt
|
|
974
1298
|
const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
|
|
975
1299
|
return [
|
|
976
|
-
['local.set', `$${name}`,
|
|
1300
|
+
['local.set', `$${name}`, tcoed],
|
|
977
1301
|
...finalizerBlock(),
|
|
978
1302
|
typed(['return', ['local.get', `$${name}`]], 'void'),
|
|
979
1303
|
]
|
|
980
1304
|
}
|
|
981
|
-
return typed(['return',
|
|
1305
|
+
return typed(['return', tcoed], 'void')
|
|
982
1306
|
},
|
|
983
1307
|
|
|
984
1308
|
// === Assignment ===
|
|
@@ -990,7 +1314,15 @@ export const emitter = {
|
|
|
990
1314
|
if (Array.isArray(name) && name[0] === '[]') {
|
|
991
1315
|
const [, arr, idx] = name
|
|
992
1316
|
const keyType = keyValType(idx)
|
|
993
|
-
|
|
1317
|
+
// A provably-numeric index name — an int-certain loop counter or a
|
|
1318
|
+
// NUMBER-typed local — can never be a string key, so the runtime
|
|
1319
|
+
// `__is_str_key` → `__dyn_set` dispatch is dead. Mirrors the index *read*
|
|
1320
|
+
// path (`intIndexIR`), closing the read/write asymmetry on `arr[i] = …`
|
|
1321
|
+
// inside refined-array loops (e.g. watr's recursive AST walkers).
|
|
1322
|
+
const idxNumericName = typeof idx === 'string' &&
|
|
1323
|
+
(repOf(idx)?.intCertain === true || repOf(idx)?.val === VAL.NUMBER)
|
|
1324
|
+
const useRuntimeKeyDispatch = !idxNumericName &&
|
|
1325
|
+
(keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING))
|
|
994
1326
|
const keyExpr = asF64(emit(idx))
|
|
995
1327
|
const valueExpr = asF64(emit(val))
|
|
996
1328
|
const storeArrayValue = (arrExpr, idxNode, persist) => {
|
|
@@ -1031,15 +1363,25 @@ export const emitter = {
|
|
|
1031
1363
|
const litKey = isLiteralStr(idx) ? idx[1]
|
|
1032
1364
|
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
1033
1365
|
: null
|
|
1366
|
+
// SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
|
|
1367
|
+
if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
|
|
1368
|
+
const fo = ctx.func.flatObjects.get(arr)
|
|
1369
|
+
const fi = fo.names.indexOf(litKey)
|
|
1370
|
+
if (fi >= 0) {
|
|
1371
|
+
const t = temp()
|
|
1372
|
+
return typed(['block', ['result', 'f64'],
|
|
1373
|
+
['local.set', `$${t}`, valueExpr],
|
|
1374
|
+
['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
|
|
1375
|
+
['local.get', `$${t}`]], 'f64')
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1034
1378
|
if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
|
|
1035
1379
|
const slot = ctx.schema.find(arr, litKey)
|
|
1036
1380
|
if (slot >= 0) {
|
|
1037
1381
|
const t = temp()
|
|
1038
1382
|
return typed(['block', ['result', 'f64'],
|
|
1039
1383
|
['local.set', `$${t}`, valueExpr],
|
|
1040
|
-
['
|
|
1041
|
-
['i32.add', ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), ['i32.const', slot * 8]],
|
|
1042
|
-
['local.get', `$${t}`]],
|
|
1384
|
+
ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), slot, ['local.get', `$${t}`]),
|
|
1043
1385
|
['local.get', `$${t}`]], 'f64')
|
|
1044
1386
|
}
|
|
1045
1387
|
}
|
|
@@ -1188,6 +1530,18 @@ export const emitter = {
|
|
|
1188
1530
|
// Object property assignment: obj.prop = x
|
|
1189
1531
|
if (Array.isArray(name) && name[0] === '.') {
|
|
1190
1532
|
const [, obj, prop] = name
|
|
1533
|
+
// SRoA flat object: `o.prop = x` → `local.set $o#i` (no heap store).
|
|
1534
|
+
const flatW = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
|
|
1535
|
+
if (flatW) {
|
|
1536
|
+
const fi = flatW.names.indexOf(prop)
|
|
1537
|
+
if (fi >= 0) {
|
|
1538
|
+
const t = temp()
|
|
1539
|
+
return typed(['block', ['result', 'f64'],
|
|
1540
|
+
['local.set', `$${t}`, asF64(emit(val))],
|
|
1541
|
+
['local.set', `$${obj}#${fi}`, ['local.get', `$${t}`]],
|
|
1542
|
+
['local.get', `$${t}`]], 'f64')
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1191
1545
|
// Schema-based object → f64.store at fixed offset.
|
|
1192
1546
|
if (typeof obj === 'string' && ctx.schema.find) {
|
|
1193
1547
|
const idx = ctx.schema.find(obj, prop)
|
|
@@ -1197,7 +1551,7 @@ export const emitter = {
|
|
|
1197
1551
|
if (shadow) inc('__dyn_set')
|
|
1198
1552
|
const stmts = [
|
|
1199
1553
|
['local.set', `$${t}`, vv],
|
|
1200
|
-
|
|
1554
|
+
ctx.abi.object.ops.store(ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), idx, ['local.get', `$${t}`]),
|
|
1201
1555
|
]
|
|
1202
1556
|
if (shadow)
|
|
1203
1557
|
stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
|
|
@@ -1350,16 +1704,21 @@ export const emitter = {
|
|
|
1350
1704
|
inc('__str_append_byte', '__char_at')
|
|
1351
1705
|
return typed(['call', '$__str_append_byte',
|
|
1352
1706
|
asI64(emit(a)),
|
|
1353
|
-
|
|
1707
|
+
ctx.abi.string.ops.charCodeAt(asF64(emit(b[1])), asI32(emit(b[2])), ctx),
|
|
1354
1708
|
], 'f64')
|
|
1355
1709
|
}
|
|
1356
1710
|
}
|
|
1357
|
-
|
|
1358
|
-
return typed(['call', '$__str_concat_raw', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1711
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
1359
1712
|
}
|
|
1360
1713
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1714
|
+
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
1715
|
+
// __str_concat's runtime __to_str cannot invoke a user-defined toString.
|
|
1716
|
+
// A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
|
|
1717
|
+
const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
1718
|
+
: vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
|
|
1719
|
+
const ea = strOperand(vtA, a)
|
|
1720
|
+
const eb = strOperand(vtB, b)
|
|
1721
|
+
return typed(ctx.abi.string.ops.concat(ea, eb, ctx), 'f64')
|
|
1363
1722
|
}
|
|
1364
1723
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
1365
1724
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -1386,10 +1745,15 @@ export const emitter = {
|
|
|
1386
1745
|
}
|
|
1387
1746
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
|
|
1388
1747
|
if (_f) return _f
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1748
|
+
// Neither side is a string here (string paths handled above), but either may
|
|
1749
|
+
// still be null/undefined/pointer — numeric `+` performs ToNumber like `-`/`*`.
|
|
1750
|
+
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1751
|
+
if (isLit(va) && litVal(va) === 0) return toNumF64(b, vb)
|
|
1752
|
+
// An `.unsigned` operand is a uint32 (range [0, 2^32)); JS `+` is a float
|
|
1753
|
+
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
1754
|
+
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
1755
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
1756
|
+
return typed(['f64.add', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1393
1757
|
},
|
|
1394
1758
|
'-': (a, b) => {
|
|
1395
1759
|
if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
@@ -1401,7 +1765,9 @@ export const emitter = {
|
|
|
1401
1765
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
1402
1766
|
if (_f) return _f
|
|
1403
1767
|
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1404
|
-
|
|
1768
|
+
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
1769
|
+
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
1770
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
1405
1771
|
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1406
1772
|
},
|
|
1407
1773
|
'u+': a => {
|
|
@@ -1423,7 +1789,9 @@ export const emitter = {
|
|
|
1423
1789
|
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
1424
1790
|
if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
1425
1791
|
if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
1426
|
-
|
|
1792
|
+
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
1793
|
+
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
1794
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
|
|
1427
1795
|
return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1428
1796
|
},
|
|
1429
1797
|
'/': (a, b) => {
|
|
@@ -1439,7 +1807,12 @@ export const emitter = {
|
|
|
1439
1807
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
1440
1808
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
1441
1809
|
if (_f) return _f
|
|
1442
|
-
|
|
1810
|
+
// ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
|
|
1811
|
+
// The i32.rem_s fast path traps on a zero divisor, so divert a literal-zero divisor.
|
|
1812
|
+
if (isLit(vb) && litVal(vb) === 0) return emitNum(NaN)
|
|
1813
|
+
// `.unsigned` operand: `i32.rem_s` reads the uint32 as a negative signed value
|
|
1814
|
+
// ((2^32-1)%7 → rem_s(-1,7) = -1, not 3). Widen to f64 — see `+` above.
|
|
1815
|
+
if (isI32Num(va) && isI32Num(vb) && !va.unsigned && !vb.unsigned) return typed(['i32.rem_s', va, vb], 'i32')
|
|
1443
1816
|
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
1444
1817
|
},
|
|
1445
1818
|
// === Comparisons (always i32 result) ===
|
|
@@ -1447,6 +1820,8 @@ export const emitter = {
|
|
|
1447
1820
|
'==': (a, b) => {
|
|
1448
1821
|
const charCmp = emitSingleCharIndexCmp(a, b)
|
|
1449
1822
|
if (charCmp) return charCmp
|
|
1823
|
+
const subCmp = emitSubstringEqCmp(a, b)
|
|
1824
|
+
if (subCmp) return subCmp
|
|
1450
1825
|
// JS loose nullish equality: x == null / x == undefined.
|
|
1451
1826
|
// If the non-literal side has a known non-null VAL type, fold to 0.
|
|
1452
1827
|
if (isNullishLit(a)) {
|
|
@@ -1465,8 +1840,8 @@ export const emitter = {
|
|
|
1465
1840
|
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1466
1841
|
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
1467
1842
|
// Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
|
|
1468
|
-
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1469
|
-
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1843
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1844
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1470
1845
|
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
|
|
1471
1846
|
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
|
|
1472
1847
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
@@ -1483,6 +1858,8 @@ export const emitter = {
|
|
|
1483
1858
|
'!=': (a, b) => {
|
|
1484
1859
|
const charCmp = emitSingleCharIndexCmp(a, b, true)
|
|
1485
1860
|
if (charCmp) return charCmp
|
|
1861
|
+
const subCmp = emitSubstringEqCmp(a, b, true)
|
|
1862
|
+
if (subCmp) return subCmp
|
|
1486
1863
|
if (isNullishLit(a)) {
|
|
1487
1864
|
if (valTypeOf(b)) return emitNum(1)
|
|
1488
1865
|
return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
|
|
@@ -1494,8 +1871,8 @@ export const emitter = {
|
|
|
1494
1871
|
const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
|
|
1495
1872
|
const va = emit(a), vb = emit(b)
|
|
1496
1873
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
|
|
1497
|
-
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1498
|
-
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1874
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1875
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1499
1876
|
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
|
|
1500
1877
|
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
|
|
1501
1878
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
@@ -1505,6 +1882,8 @@ export const emitter = {
|
|
|
1505
1882
|
inc('__eq')
|
|
1506
1883
|
return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
|
|
1507
1884
|
},
|
|
1885
|
+
'===': (a, b) => emitStrictEq(a, b, false),
|
|
1886
|
+
'!==': (a, b) => emitStrictEq(a, b, true),
|
|
1508
1887
|
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
1509
1888
|
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
1510
1889
|
'<=': cmpOp('le_s', 'le', (a, b) => a <= b),
|
|
@@ -1519,7 +1898,9 @@ export const emitter = {
|
|
|
1519
1898
|
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
1520
1899
|
// Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
|
|
1521
1900
|
// Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
|
|
1522
|
-
|
|
1901
|
+
// VAL.BOOL rides the 0/1 numeric carrier (not a pointer), so normalize it to
|
|
1902
|
+
// NUMBER and let it fall to the truthy path — `!false` must be `true`.
|
|
1903
|
+
const vt = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1523
1904
|
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
|
|
1524
1905
|
return isNullish(asF64(v))
|
|
1525
1906
|
}
|
|
@@ -1586,11 +1967,23 @@ export const emitter = {
|
|
|
1586
1967
|
|
|
1587
1968
|
'&&': (a, b) => {
|
|
1588
1969
|
const va = emit(a)
|
|
1589
|
-
|
|
1970
|
+
// Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
|
|
1971
|
+
if (isLit(va)) {
|
|
1972
|
+
const v = litVal(va)
|
|
1973
|
+
if (v !== 0 && v === v) {
|
|
1974
|
+
const refs = extractRefinements(a, new Map(), true)
|
|
1975
|
+
return withRefinements(refs, b, () => emit(b))
|
|
1976
|
+
}
|
|
1977
|
+
return va
|
|
1978
|
+
}
|
|
1979
|
+
// a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
|
|
1980
|
+
// (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
|
|
1981
|
+
const rightRefs = extractRefinements(a, new Map(), true)
|
|
1982
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
1590
1983
|
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
1591
1984
|
// skip f64 round-trip and __is_truthy call entirely.
|
|
1592
1985
|
if (va.type === 'i32') {
|
|
1593
|
-
const vb =
|
|
1986
|
+
const vb = emitRight()
|
|
1594
1987
|
const t = tempI32()
|
|
1595
1988
|
if (vb.type === 'i32') {
|
|
1596
1989
|
return typed(['if', ['result', 'i32'],
|
|
@@ -1607,15 +2000,25 @@ export const emitter = {
|
|
|
1607
2000
|
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1608
2001
|
return typed(['if', ['result', 'f64'],
|
|
1609
2002
|
toBoolFromEmitted(teed),
|
|
1610
|
-
['then', asF64(
|
|
2003
|
+
['then', asF64(emitRight())],
|
|
1611
2004
|
['else', ['local.get', `$${t}`]]], 'f64')
|
|
1612
2005
|
},
|
|
1613
2006
|
|
|
1614
2007
|
'||': (a, b) => {
|
|
1615
2008
|
const va = emit(a)
|
|
1616
|
-
|
|
2009
|
+
// Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
|
|
2010
|
+
if (isLit(va)) {
|
|
2011
|
+
const v = litVal(va)
|
|
2012
|
+
if (v !== 0 && v === v) return va
|
|
2013
|
+
const refs = extractRefinements(a, new Map(), false)
|
|
2014
|
+
return withRefinements(refs, b, () => emit(b))
|
|
2015
|
+
}
|
|
2016
|
+
// a is falsy in the right-arm — `x == null || ...` proves x is null/undefined in b;
|
|
2017
|
+
// De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
|
|
2018
|
+
const rightRefs = extractRefinements(a, new Map(), false)
|
|
2019
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
1617
2020
|
if (va.type === 'i32') {
|
|
1618
|
-
const vb =
|
|
2021
|
+
const vb = emitRight()
|
|
1619
2022
|
const t = tempI32()
|
|
1620
2023
|
if (vb.type === 'i32') {
|
|
1621
2024
|
return typed(['if', ['result', 'i32'],
|
|
@@ -1633,7 +2036,7 @@ export const emitter = {
|
|
|
1633
2036
|
return typed(['if', ['result', 'f64'],
|
|
1634
2037
|
toBoolFromEmitted(teed),
|
|
1635
2038
|
['then', ['local.get', `$${t}`]],
|
|
1636
|
-
['else', asF64(
|
|
2039
|
+
['else', asF64(emitRight())]], 'f64')
|
|
1637
2040
|
},
|
|
1638
2041
|
|
|
1639
2042
|
// a ?? b: returns b only if a is nullish
|
|
@@ -1669,7 +2072,16 @@ export const emitter = {
|
|
|
1669
2072
|
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
1670
2073
|
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
1671
2074
|
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
1672
|
-
|
|
2075
|
+
// `~~x` is the idiomatic int32 truncation: the two xor-with-(-1) cancel, leaving
|
|
2076
|
+
// a single toI32 (whose NaN/Infinity guard runs once, unchanged). Fold it here so
|
|
2077
|
+
// DSP/bytebeat `~~` doesn't emit a dead double-xor watr won't remove.
|
|
2078
|
+
'~': a => {
|
|
2079
|
+
if (Array.isArray(a) && a[0] === '~') {
|
|
2080
|
+
const inner = a[1], iv = emit(inner)
|
|
2081
|
+
return isLit(iv) ? emitNum(~~litVal(iv)) : typed(toI32(isI32Num(iv) ? iv : toNumF64(inner, iv)), 'i32')
|
|
2082
|
+
}
|
|
2083
|
+
const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(isI32Num(v) ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32')
|
|
2084
|
+
},
|
|
1673
2085
|
...Object.fromEntries([
|
|
1674
2086
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
1675
2087
|
].map(([op, fn]) => [op, (a, b) => {
|
|
@@ -1682,19 +2094,28 @@ export const emitter = {
|
|
|
1682
2094
|
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
1683
2095
|
if (op === '>>') return emitNum(la >> lb)
|
|
1684
2096
|
}
|
|
1685
|
-
const ca = va
|
|
1686
|
-
const cb = vb
|
|
2097
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
2098
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1687
2099
|
return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
|
|
1688
2100
|
}])),
|
|
1689
2101
|
'>>>': (a, b) => {
|
|
1690
|
-
const va = emit(a), vb = emit(b)
|
|
1691
|
-
if (
|
|
2102
|
+
const va = emit(a), vb = emit(b)
|
|
2103
|
+
if (isLit(va) && isLit(vb)) {
|
|
2104
|
+
const r = litVal(va) >>> litVal(vb) // JS uint32 result ∈ [0, 2^32)
|
|
2105
|
+
// ≥ 2^31 doesn't fit signed i32: materialize the wrapped bits as an i32 const
|
|
2106
|
+
// tagged `.unsigned` so `asF64` lifts via `convert_i32_u`. Emitting `f64.const r`
|
|
2107
|
+
// here (the old foldConst path) would `trunc_sat_f64_s`-saturate to INT32_MAX
|
|
2108
|
+
// when the enclosing function narrows to an i32 result. Values < 2^31 fold to a
|
|
2109
|
+
// plain i32 const (signed == unsigned, stays foldable downstream).
|
|
2110
|
+
if (r >= 0x80000000) { const node = typed(['i32.const', r | 0], 'i32'); node.unsigned = true; return node }
|
|
2111
|
+
return emitNum(r)
|
|
2112
|
+
}
|
|
1692
2113
|
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
1693
2114
|
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
1694
2115
|
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
1695
2116
|
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
1696
|
-
const ca = va
|
|
1697
|
-
const cb = vb
|
|
2117
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
2118
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1698
2119
|
const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
|
|
1699
2120
|
node.unsigned = true
|
|
1700
2121
|
return node
|
|
@@ -1773,8 +2194,24 @@ export const emitter = {
|
|
|
1773
2194
|
},
|
|
1774
2195
|
|
|
1775
2196
|
'while': (cond, body) => emitter['for'](null, cond, null, body),
|
|
1776
|
-
'
|
|
1777
|
-
|
|
2197
|
+
'label': (name, body) => {
|
|
2198
|
+
const brk = `$label${ctx.func.uniq++}`
|
|
2199
|
+
ctx.func.stack.push({ label: name, brk })
|
|
2200
|
+
const result = ['block', brk, ...emitFlat(body)]
|
|
2201
|
+
ctx.func.stack.pop()
|
|
2202
|
+
return result
|
|
2203
|
+
},
|
|
2204
|
+
'break': (label) => {
|
|
2205
|
+
const target = label == null
|
|
2206
|
+
? loopTop().brk
|
|
2207
|
+
: ctx.func.stack.findLast(frame => frame.label === label)?.brk
|
|
2208
|
+
if (!target) err(`break label '${label}' is not in scope`)
|
|
2209
|
+
return [...emitFinalizers(), ['br', target]]
|
|
2210
|
+
},
|
|
2211
|
+
'continue': (label) => {
|
|
2212
|
+
if (label != null) err(`continue label '${label}' is not supported`)
|
|
2213
|
+
return [...emitFinalizers(), ['br', loopTop().loop]]
|
|
2214
|
+
},
|
|
1778
2215
|
|
|
1779
2216
|
// === Call ===
|
|
1780
2217
|
|
|
@@ -1823,8 +2260,6 @@ export const emitter = {
|
|
|
1823
2260
|
let argList = Array.isArray(callArgs)
|
|
1824
2261
|
? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
|
|
1825
2262
|
: callArgs ? [callArgs] : []
|
|
1826
|
-
// Trailing comma in call: parser emits a trailing null sentinel — drop it
|
|
1827
|
-
while (argList.length && argList[argList.length - 1] == null) argList.pop()
|
|
1828
2263
|
|
|
1829
2264
|
// Helper: expand spread arguments into flat list of normal arguments + spread markers
|
|
1830
2265
|
// Returns { normal: [...], spreads: [(pos, expr), ...] }
|
|
@@ -1844,6 +2279,13 @@ export const emitter = {
|
|
|
1844
2279
|
|
|
1845
2280
|
const parsed = parseArgs(argList)
|
|
1846
2281
|
|
|
2282
|
+
// Closure devirtualization: a callee that is a module global proven (by
|
|
2283
|
+
// plan.js) to hold one statically-known function for the whole post-init
|
|
2284
|
+
// program rewrites to that function — the known-top-level-function branch
|
|
2285
|
+
// below then emits a direct `call`, dropping the indirect/trampoline path.
|
|
2286
|
+
if (typeof callee === 'string' && ctx.func.globalDevirt?.has(callee))
|
|
2287
|
+
callee = ctx.func.globalDevirt.get(callee)
|
|
2288
|
+
|
|
1847
2289
|
// Optional method call: obj?.method(args) — null if obj is nullish, else
|
|
1848
2290
|
// obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
|
|
1849
2291
|
// distinct from the regular method call's ['.', obj, method] callee. Receiver
|
|
@@ -1868,8 +2310,27 @@ export const emitter = {
|
|
|
1868
2310
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1869
2311
|
const [, obj, method] = callee
|
|
1870
2312
|
|
|
1871
|
-
//
|
|
1872
|
-
|
|
2313
|
+
// charCodeAt with a statically in-bounds index — emit the i32
|
|
2314
|
+
// (OOB-impossible) contract directly; the generic path keeps the
|
|
2315
|
+
// f64/NaN JS-spec result. See analyze.js inBoundsCharCodeAt.
|
|
2316
|
+
if (method === 'charCodeAt' && !parsed.hasSpread && parsed.normal.length === 1
|
|
2317
|
+
&& ctx.abi.string?.ops?.charCodeAt && inBoundsCharCodeAt(ctx).has(callee)) {
|
|
2318
|
+
const recv = emit(obj)
|
|
2319
|
+
// jsstring carrier: receiver is an externref boundary param. Route to
|
|
2320
|
+
// `wasm:js-string.charCodeAt` directly — the in-bounds proof rules out
|
|
2321
|
+
// the OOB trap the builtin would otherwise raise.
|
|
2322
|
+
if (recv?.type === 'externref') {
|
|
2323
|
+
ctx.core.jsstring.add('charCodeAt')
|
|
2324
|
+
return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
|
|
2325
|
+
}
|
|
2326
|
+
return typed(ctx.abi.string.ops.charCodeAt(
|
|
2327
|
+
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// Function property call: fn.prop(args) → direct call to fn$prop.
|
|
2331
|
+
// Skipped when the property was reassigned (wrapper composition) — then
|
|
2332
|
+
// it is a mutable slot and must be read dynamically before the call.
|
|
2333
|
+
if (typeof obj === 'string' && ctx.func.names.has(obj) && !ctx.func.multiProp.has(`${obj}.${method}`)) {
|
|
1873
2334
|
const fname = `${obj}$${method}`
|
|
1874
2335
|
if (ctx.func.names.has(fname)) {
|
|
1875
2336
|
const func = ctx.func.map.get(fname)
|
|
@@ -1885,9 +2346,19 @@ export const emitter = {
|
|
|
1885
2346
|
}
|
|
1886
2347
|
|
|
1887
2348
|
let vt = keyValType(obj)
|
|
2349
|
+
// A reassigned slice/concat receiver may carry a stale `vt` — a reassignment
|
|
2350
|
+
// inside a nested closure escapes analyzeValTypes' poisoning (its walk stops
|
|
2351
|
+
// at `=>`). Drop to runtime dispatch, but only for guessy types: STRING/ARRAY
|
|
2352
|
+
// dispatch correctly either way, and BUFFER/TYPED are construction proofs
|
|
2353
|
+
// (`new ArrayBuffer`/`new XxxArray`) — the runtime String/Array fallback has
|
|
2354
|
+
// no branch for them, so nulling `vt` would miscompile `ab.slice()` into an
|
|
2355
|
+
// f64-array copy. jzify also splits every `var x = init` into `let x; x = init`,
|
|
2356
|
+
// marking single-assignment vars "reassigned"; keeping definite BUFFER/TYPED
|
|
2357
|
+
// is what keeps `var`-declared buffers correct.
|
|
1888
2358
|
if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
|
|
1889
2359
|
&& (method === 'slice' || method === 'concat')
|
|
1890
|
-
&& vt !== VAL.STRING && vt !== VAL.ARRAY
|
|
2360
|
+
&& vt !== VAL.STRING && vt !== VAL.ARRAY
|
|
2361
|
+
&& vt !== VAL.BUFFER && vt !== VAL.TYPED) vt = null
|
|
1891
2362
|
|
|
1892
2363
|
// Helper to call method with arguments (handles spread expansion)
|
|
1893
2364
|
const callMethod = (objArg, methodEmitter) => {
|
|
@@ -1915,22 +2386,14 @@ export const emitter = {
|
|
|
1915
2386
|
ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
|
|
1916
2387
|
|
|
1917
2388
|
const objIsArr = lookupValType(objArg) === VAL.ARRAY
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
1920
|
-
const srcVT = valTypeOf(spreadExpr)
|
|
1921
|
-
const srcIsArr = !multiCount(spreadExpr) && srcVT === VAL.ARRAY
|
|
1922
|
-
const srcBase = srcIsArr ? `${T}psb${ctx.func.uniq++}` : null
|
|
1923
|
-
if (srcIsArr) ctx.func.locals.set(srcBase, 'i32')
|
|
2389
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
2390
|
+
// emitSpreadCopy resolve its kind once at runtime.
|
|
2391
|
+
const srcVT = multiCount(spreadExpr) ? undefined : valTypeOf(spreadExpr)
|
|
1924
2392
|
const n = multiCount(spreadExpr)
|
|
1925
2393
|
const ir = []
|
|
1926
2394
|
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1927
2395
|
ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1928
|
-
|
|
1929
|
-
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1930
|
-
ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
|
|
1931
|
-
} else {
|
|
1932
|
-
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1933
|
-
}
|
|
2396
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1934
2397
|
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1935
2398
|
if (objIsArr) {
|
|
1936
2399
|
ir.push(['local.set', `$${ol}`,
|
|
@@ -1943,20 +2406,9 @@ export const emitter = {
|
|
|
1943
2406
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1944
2407
|
// base captured AFTER grow (grow may relocate the array).
|
|
1945
2408
|
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1946
|
-
//
|
|
1947
|
-
ir.push(['local.set', `$${si}`, ['
|
|
1948
|
-
|
|
1949
|
-
const srcLoad = srcIsArr
|
|
1950
|
-
? ['f64.load', ['i32.add', ['local.get', `$${srcBase}`], ['i32.shl', ['local.get', `$${si}`], ['i32.const', 3]]]]
|
|
1951
|
-
: asF64(emit(['[]', sa, si]))
|
|
1952
|
-
ir.push(['block', `$break${loopId}`, ['loop', `$continue${loopId}`,
|
|
1953
|
-
['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${si}`], ['local.get', `$${sl}`]]],
|
|
1954
|
-
['f64.store',
|
|
1955
|
-
['i32.add', ['local.get', `$${base}`],
|
|
1956
|
-
['i32.shl', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${si}`]], ['i32.const', 3]]],
|
|
1957
|
-
srcLoad],
|
|
1958
|
-
['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
|
|
1959
|
-
['br', `$continue${loopId}`]]])
|
|
2409
|
+
// Bulk-copy the spread: an ARRAY source is a contiguous f64 block → memory.copy.
|
|
2410
|
+
ir.push(['local.set', `$${si}`, ['local.get', `$${ol}`]])
|
|
2411
|
+
ir.push(...emitSpreadCopy(base, si, sa, sl, srcVT))
|
|
1960
2412
|
// Single set_len for the full spread.
|
|
1961
2413
|
ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1962
2414
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
@@ -1978,6 +2430,9 @@ export const emitter = {
|
|
|
1978
2430
|
const acc = `${T}acc${ctx.func.uniq++}`, arr = `${T}sp${ctx.func.uniq++}`, len = `${T}splen${ctx.func.uniq++}`, idx = `${T}spidx${ctx.func.uniq++}`
|
|
1979
2431
|
ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
|
|
1980
2432
|
ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
|
|
2433
|
+
// Emit-time rep seeding for a fresh spread-staging local (no prior reader).
|
|
2434
|
+
// Without this, the loop body's `[]` read on `arr` falls back to polymorphic
|
|
2435
|
+
// dispatch — VAL.* on the rep elides STRING gate for ARRAY/TYPED spreads.
|
|
1981
2436
|
const spreadVT = valTypeOf(spreadExpr)
|
|
1982
2437
|
if (spreadVT) updateRep(arr, { val: spreadVT })
|
|
1983
2438
|
|
|
@@ -2042,6 +2497,7 @@ export const emitter = {
|
|
|
2042
2497
|
const spreadExpr = item[1]
|
|
2043
2498
|
const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
|
|
2044
2499
|
ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
|
|
2500
|
+
// Emit-time rep seeding for fresh spread-staging local (see arr-spread comment above).
|
|
2045
2501
|
const spreadVT = valTypeOf(spreadExpr)
|
|
2046
2502
|
if (spreadVT) updateRep(arrL, { val: spreadVT })
|
|
2047
2503
|
const n = multiCount(spreadExpr)
|
|
@@ -2117,11 +2573,11 @@ export const emitter = {
|
|
|
2117
2573
|
// Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
|
|
2118
2574
|
const loadInner = [
|
|
2119
2575
|
['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
|
|
2120
|
-
['local.set', `$${innerName}`,
|
|
2576
|
+
['local.set', `$${innerName}`, ctx.abi.object.ops.load(['local.get', `$${boxBase}`], 0)]]
|
|
2121
2577
|
const result = callMethod(innerName, emitter)
|
|
2122
2578
|
// Mutating methods may reallocate; writeback inner value to boxed slot
|
|
2123
2579
|
if (BOXED_MUTATORS.has(method)) {
|
|
2124
|
-
const wb =
|
|
2580
|
+
const wb = ctx.abi.object.ops.store(['local.get', `$${boxBase}`], 0, ['local.get', `$${innerName}`])
|
|
2125
2581
|
return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
|
|
2126
2582
|
}
|
|
2127
2583
|
// Non-mutating: just load inner and call
|
|
@@ -2129,6 +2585,35 @@ export const emitter = {
|
|
|
2129
2585
|
}
|
|
2130
2586
|
}
|
|
2131
2587
|
|
|
2588
|
+
// valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
|
|
2589
|
+
// property shadows. An assigned `obj.valueOf`/`obj.toString` must win over
|
|
2590
|
+
// the builtin emitter for any receiver that can carry a dynamic-prop
|
|
2591
|
+
// sidecar — a sidecar-bearing static type (array/typed/object) OR a
|
|
2592
|
+
// statically-unknown receiver (e.g. an array-element read `arr[0]`, whose
|
|
2593
|
+
// type is only known at runtime). Probe the sidecar and call it when it
|
|
2594
|
+
// holds a closure, else fall back to the builtin (generic when untyped:
|
|
2595
|
+
// `.valueOf` returns the receiver, `.toString` runs type-aware __to_str).
|
|
2596
|
+
// Parallels the member-READ check in module/core.js emitPropAccess (which
|
|
2597
|
+
// stays scoped to known sidecar types). (watr's `str()` attaches
|
|
2598
|
+
// `bytes.valueOf = () => s`, recovered via `.valueOf()`.)
|
|
2599
|
+
if ((method === 'valueOf' || method === 'toString') && ctx.closure.call
|
|
2600
|
+
&& !parsed.hasSpread && parsed.normal.length === 0
|
|
2601
|
+
&& (vt === VAL.ARRAY || vt === VAL.TYPED || vt === VAL.OBJECT || !vt)) {
|
|
2602
|
+
const builtin = (vt && ctx.core.emit[`.${vt}:${method}`]) || ctx.core.emit[`.${method}`]
|
|
2603
|
+
if (builtin) {
|
|
2604
|
+
const objTmp = temp('vobj'), propTmp = temp('vprop')
|
|
2605
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2606
|
+
return typed(['block', ['result', 'f64'],
|
|
2607
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
2608
|
+
['local.set', `$${propTmp}`, ['f64.reinterpret_i64',
|
|
2609
|
+
['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]]],
|
|
2610
|
+
['if', ['result', 'f64'],
|
|
2611
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
2612
|
+
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [])],
|
|
2613
|
+
['else', asF64(callMethod(objTmp, builtin))]]], 'f64')
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2132
2617
|
// Known type → static dispatch
|
|
2133
2618
|
if (vt && ctx.core.emit[`.${vt}:${method}`]) {
|
|
2134
2619
|
return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
|
|
@@ -2158,7 +2643,7 @@ export const emitter = {
|
|
|
2158
2643
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
|
|
2159
2644
|
const idx = ctx.schema.find(obj, method)
|
|
2160
2645
|
if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
|
|
2161
|
-
const propRead = typed(
|
|
2646
|
+
const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
|
|
2162
2647
|
if (parsed.hasSpread) {
|
|
2163
2648
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2164
2649
|
return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
|
|
@@ -2171,13 +2656,21 @@ export const emitter = {
|
|
|
2171
2656
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
|
|
2172
2657
|
const idx = ctx.schema.find(obj, method)
|
|
2173
2658
|
if (idx >= 0) {
|
|
2174
|
-
const propRead = typed(
|
|
2659
|
+
const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
|
|
2175
2660
|
return ctx.closure.call(propRead, parsed.normal)
|
|
2176
2661
|
}
|
|
2177
2662
|
}
|
|
2178
2663
|
|
|
2179
|
-
// Generic only
|
|
2180
|
-
|
|
2664
|
+
// Generic only — but a collection emitter (`.get`/`.set`/`.has`/`.add`/
|
|
2665
|
+
// `.delete`) assumes a Map/Set receiver: a proven collection already
|
|
2666
|
+
// dispatched via `.${vt}:${method}` above, so reaching here means the
|
|
2667
|
+
// receiver is not a proven collection. A zero-arg call then cannot be the
|
|
2668
|
+
// collection op (each needs ≥1 key/value arg) — it is a user/closure
|
|
2669
|
+
// method (e.g. `new C().get()`). Skip the collection emitter so it falls
|
|
2670
|
+
// through to closure/dynamic dispatch instead of crashing on `emit(key)`.
|
|
2671
|
+
const collectionMisfit = COLLECTION_METHODS.has(method) &&
|
|
2672
|
+
!parsed.hasSpread && parsed.normal.length === 0
|
|
2673
|
+
if (ctx.core.emit[genKey] && !collectionMisfit) {
|
|
2181
2674
|
return callMethod(obj, ctx.core.emit[genKey])
|
|
2182
2675
|
}
|
|
2183
2676
|
|
|
@@ -2233,6 +2726,10 @@ export const emitter = {
|
|
|
2233
2726
|
// Unknown callee - assume external method
|
|
2234
2727
|
if (ctx.transform.strict)
|
|
2235
2728
|
err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
|
|
2729
|
+
// Under wasi there is no host `__ext_call` — the call lowers to a
|
|
2730
|
+
// no-op returning `undefined`. This is by-design so polymorphic code
|
|
2731
|
+
// can target js and wasi from one source; users who want fail-fast
|
|
2732
|
+
// pass `strict: true` (handled above).
|
|
2236
2733
|
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
2237
2734
|
inc('__ext_call')
|
|
2238
2735
|
ctx.features.external = true
|
|
@@ -2244,7 +2741,7 @@ export const emitter = {
|
|
|
2244
2741
|
['i64.reinterpret_f64', arrayIR]]], 'f64');
|
|
2245
2742
|
}
|
|
2246
2743
|
|
|
2247
|
-
if (ctx.core.emit[callee]) {
|
|
2744
|
+
if (typeof callee === 'string' && ctx.core.emit[callee] && !isBoundName(callee) && !isUserFunc(callee)) {
|
|
2248
2745
|
// Pass spread args through to emitter (e.g. Math.max(...arr))
|
|
2249
2746
|
if (parsed.hasSpread) {
|
|
2250
2747
|
const allArgs = []
|
|
@@ -2260,7 +2757,7 @@ export const emitter = {
|
|
|
2260
2757
|
}
|
|
2261
2758
|
|
|
2262
2759
|
// Direct call if callee is a known top-level function
|
|
2263
|
-
if (typeof callee === 'string' && ctx.func.names.has(callee) && !
|
|
2760
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee) && !isBoundName(callee)) {
|
|
2264
2761
|
const func = ctx.func.map.get(callee)
|
|
2265
2762
|
|
|
2266
2763
|
// Rest param case: collect all args (including expanded spreads) into array
|
|
@@ -2429,15 +2926,19 @@ export function emit(node, expect) {
|
|
|
2429
2926
|
if (node.loc != null) ctx.error.loc = node.loc
|
|
2430
2927
|
}
|
|
2431
2928
|
if (node == null) return null
|
|
2432
|
-
|
|
2433
|
-
|
|
2929
|
+
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
2930
|
+
// AST), but their working representation is the plain number 0/1 — identical
|
|
2931
|
+
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|
|
2932
|
+
if (node === true) return emitNum(1)
|
|
2933
|
+
if (node === false) return emitNum(0)
|
|
2434
2934
|
if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
|
|
2435
2935
|
return nullExpr()
|
|
2436
2936
|
if (typeof node === 'bigint') {
|
|
2437
|
-
//
|
|
2438
|
-
//
|
|
2439
|
-
|
|
2440
|
-
|
|
2937
|
+
// Truncate to 64 bits — `BigInt.asUintN(64, …)` semantics, same as the
|
|
2938
|
+
// explicit mask `node & 0xFFFFFFFFFFFFFFFFn`. Decimal form (vs. the prior
|
|
2939
|
+
// unsigned-hex dance) is enough now that watr's optimize.js getConst
|
|
2940
|
+
// handles signed strings correctly (4.6.8 W5 fix).
|
|
2941
|
+
return typed(['f64.reinterpret_i64', ['i64.const', BigInt.asUintN(64, node).toString()]], 'f64')
|
|
2441
2942
|
}
|
|
2442
2943
|
if (typeof node === 'number') return emitNum(node)
|
|
2443
2944
|
if (typeof node === 'string') {
|
|
@@ -2473,7 +2974,15 @@ export function emit(node, expect) {
|
|
|
2473
2974
|
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (local $${arr} i32) ${tempLocals} (call $${node} ${fwd}) ${capture} (local.set $${arr} (call $__alloc (i32.const ${n * 8 + 8}))) (i32.store (local.get $${arr}) (i32.const ${n})) (i32.store (i32.add (local.get $${arr}) (i32.const 4)) (i32.const ${n})) (local.set $${arr} (i32.add (local.get $${arr}) (i32.const 8))) ${stores} (call $__mkptr (i32.const 1) (i32.const 0) (local.get $${arr})))`
|
|
2474
2975
|
inc(trampolineName, '__alloc', '__mkptr')
|
|
2475
2976
|
} else {
|
|
2476
|
-
|
|
2977
|
+
// Convert i32/i64 results back to f64 — uniform closure ABI returns f64.
|
|
2978
|
+
const resType = func?.sig.results[0]
|
|
2979
|
+
const callExpr = `(call $${node} ${fwd})`
|
|
2980
|
+
const wrapped = resType === 'i32'
|
|
2981
|
+
? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
|
|
2982
|
+
: resType === 'i64'
|
|
2983
|
+
? `(f64.reinterpret_i64 ${callExpr})`
|
|
2984
|
+
: callExpr
|
|
2985
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${wrapped})`
|
|
2477
2986
|
inc(trampolineName)
|
|
2478
2987
|
}
|
|
2479
2988
|
}
|