jz 0.4.0 → 0.5.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 +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 +251 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +551 -128
- 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 +540 -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 +662 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +786 -94
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +822 -150
- 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,51 @@ 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
|
+
return ['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
|
|
793
|
+
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
|
|
794
|
+
['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
|
|
795
|
+
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
796
|
+
['br', `$loop${loopId}`]]]
|
|
797
|
+
}
|
|
798
|
+
const advance = ['local.set', `$${posLocal}`,
|
|
799
|
+
['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
|
|
800
|
+
if (staticVT === VAL.ARRAY) return [arrCopy(), advance]
|
|
801
|
+
if (staticVT === VAL.STRING || staticVT === VAL.TYPED) return [scalarLoop(), advance]
|
|
802
|
+
inc('__ptr_type')
|
|
803
|
+
return [['if',
|
|
804
|
+
['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.ARRAY]],
|
|
805
|
+
['then', arrCopy()],
|
|
806
|
+
['else', scalarLoop()]], advance]
|
|
807
|
+
}
|
|
808
|
+
|
|
550
809
|
/**
|
|
551
810
|
* Build an array from items, handling ['__spread', expr] markers.
|
|
552
811
|
* Split into sections (normal arrays and spreads), then copy all into result.
|
|
@@ -581,9 +840,12 @@ export function buildArrayWithSpreads(items) {
|
|
|
581
840
|
sections.push({ type: 'array', items: currentArray })
|
|
582
841
|
}
|
|
583
842
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
843
|
+
// A single all-normal section is a plain literal — defer to the `[` emitter.
|
|
844
|
+
// A single *spread* section is NOT shortcut to `emit(sec.expr)`: that would
|
|
845
|
+
// alias the source, but `[...x]` must yield a fresh array. It falls through
|
|
846
|
+
// to the alloc + emitSpreadCopy path below, which copies.
|
|
847
|
+
if (sections.length === 1 && sections[0].type === 'array') {
|
|
848
|
+
return emit(['[', ...sections[0].items])
|
|
587
849
|
}
|
|
588
850
|
|
|
589
851
|
const len = tempI32('len')
|
|
@@ -591,35 +853,39 @@ export function buildArrayWithSpreads(items) {
|
|
|
591
853
|
const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
|
|
592
854
|
const result = out.local
|
|
593
855
|
|
|
594
|
-
const ir = [
|
|
595
|
-
|
|
596
|
-
]
|
|
856
|
+
const ir = []
|
|
857
|
+
inc('__len')
|
|
597
858
|
|
|
598
|
-
|
|
859
|
+
// Pass 1 — evaluate every section IN SOURCE ORDER into temps. JS spread keeps
|
|
860
|
+
// strict left-to-right order: a later spread whose source mutates an earlier
|
|
861
|
+
// element's input must still observe the pre-mutation value. Array items
|
|
862
|
+
// become per-item f64 temps; spreads become a ptr temp + a cached __len.
|
|
599
863
|
for (const sec of sections) {
|
|
600
|
-
if (sec.type === '
|
|
864
|
+
if (sec.type === 'array') {
|
|
865
|
+
sec.itemLocals = []
|
|
866
|
+
for (let i = 0; i < sec.items.length; i++) {
|
|
867
|
+
const it = `${T}ai${ctx.func.uniq++}`
|
|
868
|
+
ctx.func.locals.set(it, 'f64')
|
|
869
|
+
sec.itemLocals.push(it)
|
|
870
|
+
ir.push(['local.set', `$${it}`, asF64(emit(sec.items[i]))])
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
601
873
|
sec.local = `${T}sp${ctx.func.uniq++}`
|
|
602
874
|
ctx.func.locals.set(sec.local, 'f64')
|
|
603
875
|
sec.lenLocal = `${T}spl${ctx.func.uniq++}`
|
|
604
876
|
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
|
-
}
|
|
877
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
878
|
+
// emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
|
|
879
|
+
sec.val = multiCount(sec.expr) ? undefined : valTypeOf(sec.expr)
|
|
611
880
|
const n = multiCount(sec.expr)
|
|
612
881
|
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
|
-
}
|
|
882
|
+
// Cache __len once per spread; reused below for total-len sum and the copy.
|
|
883
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
620
884
|
}
|
|
621
885
|
}
|
|
622
886
|
|
|
887
|
+
// Pass 2 — total length (array sections statically sized, spreads cached above).
|
|
888
|
+
ir.push(['local.set', `$${len}`, ['i32.const', 0]])
|
|
623
889
|
for (const sec of sections) {
|
|
624
890
|
if (sec.type === 'array') {
|
|
625
891
|
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
|
|
@@ -628,41 +894,20 @@ export function buildArrayWithSpreads(items) {
|
|
|
628
894
|
}
|
|
629
895
|
}
|
|
630
896
|
|
|
897
|
+
// Pass 3 — allocate exact, then store the pre-evaluated temps.
|
|
631
898
|
ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
|
|
632
|
-
|
|
633
899
|
for (const sec of sections) {
|
|
634
900
|
if (sec.type === 'array') {
|
|
635
|
-
for (
|
|
901
|
+
for (const it of sec.itemLocals) {
|
|
636
902
|
ir.push(
|
|
637
903
|
['f64.store',
|
|
638
904
|
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
639
|
-
|
|
905
|
+
['local.get', `$${it}`]],
|
|
640
906
|
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
|
|
641
907
|
)
|
|
642
908
|
}
|
|
643
909
|
} 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
|
-
)
|
|
910
|
+
ir.push(...emitSpreadCopy(result, pos, sec.local, sec.lenLocal, sec.val))
|
|
666
911
|
}
|
|
667
912
|
}
|
|
668
913
|
|
|
@@ -698,13 +943,17 @@ export function emitBody(node) {
|
|
|
698
943
|
// Skip names that are reassigned later — refinement would be unsound past the assignment.
|
|
699
944
|
if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
|
|
700
945
|
const refs = extractRefinements(s[1], new Map(), false)
|
|
701
|
-
for (const [name,
|
|
946
|
+
for (const [name, fact] of refs) {
|
|
702
947
|
let reassigned = false
|
|
703
948
|
for (let j = i + 1; j < stmts.length; j++)
|
|
704
949
|
if (isReassigned(stmts[j], name)) { reassigned = true; break }
|
|
705
950
|
if (reassigned) continue
|
|
706
|
-
|
|
707
|
-
|
|
951
|
+
const cur = ctx.func.refinements.get(name)
|
|
952
|
+
accumulated.push([name, cur])
|
|
953
|
+
// Merge so sibling early-returns layering on the same name compose
|
|
954
|
+
// (e.g. `if (typeof x === 'string') return; if (Array.isArray(x)) return;`
|
|
955
|
+
// leaves both `notString: true` and would-be array exclusion stacked).
|
|
956
|
+
ctx.func.refinements.set(name, cur ? { ...cur, ...fact } : fact)
|
|
708
957
|
}
|
|
709
958
|
}
|
|
710
959
|
}
|
|
@@ -716,23 +965,86 @@ export function emitBody(node) {
|
|
|
716
965
|
return out
|
|
717
966
|
}
|
|
718
967
|
|
|
968
|
+
// A VAL.BOOL value rides the cheap 0/1 numeric carrier, and `ToNumber(bool)` is
|
|
969
|
+
// exactly that carrier — so for relational / loose-equality coercion a boolean
|
|
970
|
+
// behaves identically to a number. Normalize it before the type-directed compare
|
|
971
|
+
// dispatch (the BOOL fact still drives typeof / String / boundary boxing; only
|
|
972
|
+
// these arithmetic-shaped operators read it as numeric).
|
|
973
|
+
const numericVal = vt => vt === VAL.BOOL ? VAL.NUMBER : vt
|
|
974
|
+
|
|
975
|
+
// Primitive value-type classes for strict-equality type-mismatch folding. Two
|
|
976
|
+
// operands of different known classes — when at least one is a primitive — can
|
|
977
|
+
// never be `===` (number/boolean/string/bigint don't cross-coerce under `===`).
|
|
978
|
+
// Two *reference* kinds (array vs object, …) fall through to the shared ref-eq
|
|
979
|
+
// path instead, which already resolves distinct pointers to `false`.
|
|
980
|
+
const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Strict `===`/`!==`. Unlike loose `==`, no coercion: a statically-known type
|
|
984
|
+
* mismatch folds to a constant (`true === 1` → false, `"1" === 1` → false). When
|
|
985
|
+
* the types match — or one side is statically unknown — the result is bit-for-bit
|
|
986
|
+
* identical to loose `==` on same-type operands, so we delegate to it.
|
|
987
|
+
*
|
|
988
|
+
* Two carrier-level limitations remain (documented gaps, not regressions):
|
|
989
|
+
* • booleans and numbers share the 0/1 carrier, so `1 === trueDynamic` can only
|
|
990
|
+
* be told apart when the boolean's type is statically known;
|
|
991
|
+
* • jz unifies `null` and `undefined` into one NaN-boxed sentinel, so
|
|
992
|
+
* `null === undefined` is `true` (same as `==`).
|
|
993
|
+
*/
|
|
994
|
+
function emitStrictEq(a, b, negate) {
|
|
995
|
+
// `typeof x === 'type'` (prepare rewrote the literal to a numeric code) — typeof
|
|
996
|
+
// always yields a string, so strict and loose agree; reuse the loose lowering.
|
|
997
|
+
const tc = emitTypeofCmp(a, b, negate ? 'ne' : 'eq'); if (tc) return tc
|
|
998
|
+
// Known, differing primitive classes can never be strictly equal.
|
|
999
|
+
const rawA = resolveValType(a, valTypeOf, lookupValType)
|
|
1000
|
+
const rawB = resolveValType(b, valTypeOf, lookupValType)
|
|
1001
|
+
if (rawA && rawB && rawA !== rawB && (STRICT_PRIM.has(rawA) || STRICT_PRIM.has(rawB)))
|
|
1002
|
+
return emitNum(negate ? 1 : 0)
|
|
1003
|
+
// Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
|
|
1004
|
+
return emitter[negate ? '!=' : '=='](a, b)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
719
1007
|
/** Comparison op factory with constant folding. */
|
|
720
1008
|
const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
721
1009
|
const va = emit(a), vb = emit(b)
|
|
722
|
-
|
|
1010
|
+
// Skip the const-fold for `.unsigned` operands: `litVal` is the signed bit pattern
|
|
1011
|
+
// (-1, not 4294967295), so folding the order would be wrong. Fall through to the
|
|
1012
|
+
// f64 widen path below, which converts each operand by its own signedness.
|
|
1013
|
+
if (isLit(va) && isLit(vb) && !va.unsigned && !vb.unsigned) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
|
|
723
1014
|
// String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
|
|
724
1015
|
// (always false), so without this the spec-correct `"a" < "b"` returns 0.
|
|
725
1016
|
// Route both-STRING operands through __str_cmp's three-way result, then apply
|
|
726
1017
|
// 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)
|
|
1018
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1019
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
729
1020
|
if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
|
|
730
1021
|
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
731
1022
|
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
732
1023
|
}
|
|
733
1024
|
if (vta === VAL.STRING && vtb === VAL.STRING) {
|
|
734
|
-
|
|
735
|
-
|
|
1025
|
+
return typed([`i32.${i32op}`, ctx.abi.string.ops.cmp(asF64(va), asF64(vb), ctx), ['i32.const', 0]], 'i32')
|
|
1026
|
+
}
|
|
1027
|
+
// Exactly one operand is a known string; the other has no static type, so it
|
|
1028
|
+
// may hold a string pointer at runtime (e.g. `c >= '0'` where `c` came from
|
|
1029
|
+
// `s[i]` on an untyped receiver). JS relational compare is lexicographic only
|
|
1030
|
+
// when *both* sides are strings, else it ToNumbers both. The f64 path below
|
|
1031
|
+
// would compare the unknown side's NaN-boxed string bits as a float (NaN ⇒
|
|
1032
|
+
// always false), so dispatch at runtime on the unknown side: string → __str_cmp
|
|
1033
|
+
// three-way; else ToNumber both. Mirrors `+`'s __is_str_key string dispatch.
|
|
1034
|
+
// Gated on a *known-string* counterpart, so numeric loops (`i < n`) never pay
|
|
1035
|
+
// the check — comparing against a string literal signals string intent.
|
|
1036
|
+
if (((vta === VAL.STRING && vtb == null) || (vtb === VAL.STRING && vta == null)) && ctx.abi.string?.ops?.cmp) {
|
|
1037
|
+
const unkIsA = vta == null
|
|
1038
|
+
const ta = temp('cmp'), tb = temp('cmp')
|
|
1039
|
+
inc('__is_str_key')
|
|
1040
|
+
const getA = typed(['local.get', `$${ta}`], 'f64'), getB = typed(['local.get', `$${tb}`], 'f64')
|
|
1041
|
+
const check = ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${unkIsA ? ta : tb}`]]]
|
|
1042
|
+
const strCmp = [`i32.${i32op}`, ctx.abi.string.ops.cmp(getA, getB, ctx), ['i32.const', 0]]
|
|
1043
|
+
const numCmp = [`f64.${f64op}`, toNumF64(a, getA), toNumF64(b, getB)]
|
|
1044
|
+
return typed(['block', ['result', 'i32'],
|
|
1045
|
+
['local.set', `$${ta}`, asF64(va)],
|
|
1046
|
+
['local.set', `$${tb}`, asF64(vb)],
|
|
1047
|
+
['if', ['result', 'i32'], check, ['then', strCmp], ['else', numCmp]]], 'i32')
|
|
736
1048
|
}
|
|
737
1049
|
if (vta === VAL.DATE || vtb === VAL.DATE) {
|
|
738
1050
|
const dateNum = (node, v, vt) => {
|
|
@@ -748,11 +1060,17 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
748
1060
|
return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
|
|
749
1061
|
if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
|
|
750
1062
|
return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1063
|
+
// An `.unsigned` i32 operand ([0, 2^32)) can't share a signed i32 compare with a
|
|
1064
|
+
// possibly-signed one: mixed sign inverts the order (3 < 0xFFFFFFFF unsigned, but
|
|
1065
|
+
// 3 > -1 signed). Widen to f64, where asF64 converts each operand by its own
|
|
1066
|
+
// signedness (convert_i32_u for unsigned, _s otherwise) to its true numeric value.
|
|
1067
|
+
if (!va.unsigned && !vb.unsigned) {
|
|
1068
|
+
const ai = intConstValue(a), bi = intConstValue(b)
|
|
1069
|
+
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
1070
|
+
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
1071
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${i32op}`, va, vb], 'i32')
|
|
1072
|
+
}
|
|
1073
|
+
return typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
756
1074
|
}
|
|
757
1075
|
|
|
758
1076
|
function needsRelationalToNumber(expr, vt) {
|
|
@@ -885,7 +1203,7 @@ export const emitter = {
|
|
|
885
1203
|
},
|
|
886
1204
|
|
|
887
1205
|
'throw': expr => {
|
|
888
|
-
ctx.runtime.throws = true
|
|
1206
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
889
1207
|
const thrown = temp()
|
|
890
1208
|
return typed(['block',
|
|
891
1209
|
['local.set', `$${thrown}`, asF64(emit(expr))],
|
|
@@ -896,7 +1214,7 @@ export const emitter = {
|
|
|
896
1214
|
'catch': (body, errName, handler) => {
|
|
897
1215
|
if (!canThrow(body)) return emitFlat(body)
|
|
898
1216
|
|
|
899
|
-
ctx.runtime.throws = true
|
|
1217
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
900
1218
|
const id = ctx.func.uniq++
|
|
901
1219
|
ctx.func.locals.set(errName, 'f64')
|
|
902
1220
|
const prev = ctx.func.inTry; ctx.func.inTry = true
|
|
@@ -922,7 +1240,7 @@ export const emitter = {
|
|
|
922
1240
|
return [...bodyIR, ...cleanupIR]
|
|
923
1241
|
}
|
|
924
1242
|
|
|
925
|
-
ctx.runtime.throws = true
|
|
1243
|
+
ctx.runtime.throws = ctx.runtime.userThrows = true
|
|
926
1244
|
const id = ctx.func.uniq++
|
|
927
1245
|
const errLocal = temp('err')
|
|
928
1246
|
const parentStack = ctx.func.finallyStack || []
|
|
@@ -966,19 +1284,20 @@ export const emitter = {
|
|
|
966
1284
|
const rt = ctx.func.current?.results[0] || 'f64'
|
|
967
1285
|
const pk = ctx.func.current?.ptrKind
|
|
968
1286
|
const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1287
|
+
const ty = pk != null ? 'i32' : rt
|
|
1288
|
+
const tcoed = tcoTailRewrite(ir, ty)
|
|
1289
|
+
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
1290
|
+
return typed(tcoed, 'void')
|
|
1291
|
+
}
|
|
972
1292
|
if (finalizers.length > 0) {
|
|
973
|
-
const ty = pk != null ? 'i32' : rt
|
|
974
1293
|
const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
|
|
975
1294
|
return [
|
|
976
|
-
['local.set', `$${name}`,
|
|
1295
|
+
['local.set', `$${name}`, tcoed],
|
|
977
1296
|
...finalizerBlock(),
|
|
978
1297
|
typed(['return', ['local.get', `$${name}`]], 'void'),
|
|
979
1298
|
]
|
|
980
1299
|
}
|
|
981
|
-
return typed(['return',
|
|
1300
|
+
return typed(['return', tcoed], 'void')
|
|
982
1301
|
},
|
|
983
1302
|
|
|
984
1303
|
// === Assignment ===
|
|
@@ -990,7 +1309,15 @@ export const emitter = {
|
|
|
990
1309
|
if (Array.isArray(name) && name[0] === '[]') {
|
|
991
1310
|
const [, arr, idx] = name
|
|
992
1311
|
const keyType = keyValType(idx)
|
|
993
|
-
|
|
1312
|
+
// A provably-numeric index name — an int-certain loop counter or a
|
|
1313
|
+
// NUMBER-typed local — can never be a string key, so the runtime
|
|
1314
|
+
// `__is_str_key` → `__dyn_set` dispatch is dead. Mirrors the index *read*
|
|
1315
|
+
// path (`intIndexIR`), closing the read/write asymmetry on `arr[i] = …`
|
|
1316
|
+
// inside refined-array loops (e.g. watr's recursive AST walkers).
|
|
1317
|
+
const idxNumericName = typeof idx === 'string' &&
|
|
1318
|
+
(repOf(idx)?.intCertain === true || repOf(idx)?.val === VAL.NUMBER)
|
|
1319
|
+
const useRuntimeKeyDispatch = !idxNumericName &&
|
|
1320
|
+
(keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING))
|
|
994
1321
|
const keyExpr = asF64(emit(idx))
|
|
995
1322
|
const valueExpr = asF64(emit(val))
|
|
996
1323
|
const storeArrayValue = (arrExpr, idxNode, persist) => {
|
|
@@ -1031,15 +1358,25 @@ export const emitter = {
|
|
|
1031
1358
|
const litKey = isLiteralStr(idx) ? idx[1]
|
|
1032
1359
|
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
1033
1360
|
: null
|
|
1361
|
+
// SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
|
|
1362
|
+
if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
|
|
1363
|
+
const fo = ctx.func.flatObjects.get(arr)
|
|
1364
|
+
const fi = fo.names.indexOf(litKey)
|
|
1365
|
+
if (fi >= 0) {
|
|
1366
|
+
const t = temp()
|
|
1367
|
+
return typed(['block', ['result', 'f64'],
|
|
1368
|
+
['local.set', `$${t}`, valueExpr],
|
|
1369
|
+
['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
|
|
1370
|
+
['local.get', `$${t}`]], 'f64')
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1034
1373
|
if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
|
|
1035
1374
|
const slot = ctx.schema.find(arr, litKey)
|
|
1036
1375
|
if (slot >= 0) {
|
|
1037
1376
|
const t = temp()
|
|
1038
1377
|
return typed(['block', ['result', 'f64'],
|
|
1039
1378
|
['local.set', `$${t}`, valueExpr],
|
|
1040
|
-
['
|
|
1041
|
-
['i32.add', ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), ['i32.const', slot * 8]],
|
|
1042
|
-
['local.get', `$${t}`]],
|
|
1379
|
+
ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), slot, ['local.get', `$${t}`]),
|
|
1043
1380
|
['local.get', `$${t}`]], 'f64')
|
|
1044
1381
|
}
|
|
1045
1382
|
}
|
|
@@ -1188,6 +1525,18 @@ export const emitter = {
|
|
|
1188
1525
|
// Object property assignment: obj.prop = x
|
|
1189
1526
|
if (Array.isArray(name) && name[0] === '.') {
|
|
1190
1527
|
const [, obj, prop] = name
|
|
1528
|
+
// SRoA flat object: `o.prop = x` → `local.set $o#i` (no heap store).
|
|
1529
|
+
const flatW = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
|
|
1530
|
+
if (flatW) {
|
|
1531
|
+
const fi = flatW.names.indexOf(prop)
|
|
1532
|
+
if (fi >= 0) {
|
|
1533
|
+
const t = temp()
|
|
1534
|
+
return typed(['block', ['result', 'f64'],
|
|
1535
|
+
['local.set', `$${t}`, asF64(emit(val))],
|
|
1536
|
+
['local.set', `$${obj}#${fi}`, ['local.get', `$${t}`]],
|
|
1537
|
+
['local.get', `$${t}`]], 'f64')
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1191
1540
|
// Schema-based object → f64.store at fixed offset.
|
|
1192
1541
|
if (typeof obj === 'string' && ctx.schema.find) {
|
|
1193
1542
|
const idx = ctx.schema.find(obj, prop)
|
|
@@ -1197,7 +1546,7 @@ export const emitter = {
|
|
|
1197
1546
|
if (shadow) inc('__dyn_set')
|
|
1198
1547
|
const stmts = [
|
|
1199
1548
|
['local.set', `$${t}`, vv],
|
|
1200
|
-
|
|
1549
|
+
ctx.abi.object.ops.store(ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), idx, ['local.get', `$${t}`]),
|
|
1201
1550
|
]
|
|
1202
1551
|
if (shadow)
|
|
1203
1552
|
stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
|
|
@@ -1350,16 +1699,21 @@ export const emitter = {
|
|
|
1350
1699
|
inc('__str_append_byte', '__char_at')
|
|
1351
1700
|
return typed(['call', '$__str_append_byte',
|
|
1352
1701
|
asI64(emit(a)),
|
|
1353
|
-
|
|
1702
|
+
ctx.abi.string.ops.charCodeAt(asF64(emit(b[1])), asI32(emit(b[2])), ctx),
|
|
1354
1703
|
], 'f64')
|
|
1355
1704
|
}
|
|
1356
1705
|
}
|
|
1357
|
-
|
|
1358
|
-
return typed(['call', '$__str_concat_raw', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1706
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
1359
1707
|
}
|
|
1360
1708
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1709
|
+
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
1710
|
+
// __str_concat's runtime __to_str cannot invoke a user-defined toString.
|
|
1711
|
+
// A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
|
|
1712
|
+
const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
1713
|
+
: vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
|
|
1714
|
+
const ea = strOperand(vtA, a)
|
|
1715
|
+
const eb = strOperand(vtB, b)
|
|
1716
|
+
return typed(ctx.abi.string.ops.concat(ea, eb, ctx), 'f64')
|
|
1363
1717
|
}
|
|
1364
1718
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
1365
1719
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -1386,10 +1740,15 @@ export const emitter = {
|
|
|
1386
1740
|
}
|
|
1387
1741
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
|
|
1388
1742
|
if (_f) return _f
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1743
|
+
// Neither side is a string here (string paths handled above), but either may
|
|
1744
|
+
// still be null/undefined/pointer — numeric `+` performs ToNumber like `-`/`*`.
|
|
1745
|
+
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1746
|
+
if (isLit(va) && litVal(va) === 0) return toNumF64(b, vb)
|
|
1747
|
+
// An `.unsigned` operand is a uint32 (range [0, 2^32)); JS `+` is a float
|
|
1748
|
+
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
1749
|
+
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
1750
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
1751
|
+
return typed(['f64.add', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1393
1752
|
},
|
|
1394
1753
|
'-': (a, b) => {
|
|
1395
1754
|
if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
@@ -1401,7 +1760,9 @@ export const emitter = {
|
|
|
1401
1760
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
1402
1761
|
if (_f) return _f
|
|
1403
1762
|
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1404
|
-
|
|
1763
|
+
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
1764
|
+
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
1765
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
1405
1766
|
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1406
1767
|
},
|
|
1407
1768
|
'u+': a => {
|
|
@@ -1423,7 +1784,9 @@ export const emitter = {
|
|
|
1423
1784
|
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
1424
1785
|
if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
1425
1786
|
if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
1426
|
-
|
|
1787
|
+
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
1788
|
+
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
1789
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
|
|
1427
1790
|
return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1428
1791
|
},
|
|
1429
1792
|
'/': (a, b) => {
|
|
@@ -1439,7 +1802,12 @@ export const emitter = {
|
|
|
1439
1802
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
1440
1803
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
1441
1804
|
if (_f) return _f
|
|
1442
|
-
|
|
1805
|
+
// ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
|
|
1806
|
+
// The i32.rem_s fast path traps on a zero divisor, so divert a literal-zero divisor.
|
|
1807
|
+
if (isLit(vb) && litVal(vb) === 0) return emitNum(NaN)
|
|
1808
|
+
// `.unsigned` operand: `i32.rem_s` reads the uint32 as a negative signed value
|
|
1809
|
+
// ((2^32-1)%7 → rem_s(-1,7) = -1, not 3). Widen to f64 — see `+` above.
|
|
1810
|
+
if (isI32Num(va) && isI32Num(vb) && !va.unsigned && !vb.unsigned) return typed(['i32.rem_s', va, vb], 'i32')
|
|
1443
1811
|
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
1444
1812
|
},
|
|
1445
1813
|
// === Comparisons (always i32 result) ===
|
|
@@ -1447,6 +1815,8 @@ export const emitter = {
|
|
|
1447
1815
|
'==': (a, b) => {
|
|
1448
1816
|
const charCmp = emitSingleCharIndexCmp(a, b)
|
|
1449
1817
|
if (charCmp) return charCmp
|
|
1818
|
+
const subCmp = emitSubstringEqCmp(a, b)
|
|
1819
|
+
if (subCmp) return subCmp
|
|
1450
1820
|
// JS loose nullish equality: x == null / x == undefined.
|
|
1451
1821
|
// If the non-literal side has a known non-null VAL type, fold to 0.
|
|
1452
1822
|
if (isNullishLit(a)) {
|
|
@@ -1465,8 +1835,8 @@ export const emitter = {
|
|
|
1465
1835
|
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1466
1836
|
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
1467
1837
|
// 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)
|
|
1838
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1839
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1470
1840
|
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
|
|
1471
1841
|
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
|
|
1472
1842
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
@@ -1483,6 +1853,8 @@ export const emitter = {
|
|
|
1483
1853
|
'!=': (a, b) => {
|
|
1484
1854
|
const charCmp = emitSingleCharIndexCmp(a, b, true)
|
|
1485
1855
|
if (charCmp) return charCmp
|
|
1856
|
+
const subCmp = emitSubstringEqCmp(a, b, true)
|
|
1857
|
+
if (subCmp) return subCmp
|
|
1486
1858
|
if (isNullishLit(a)) {
|
|
1487
1859
|
if (valTypeOf(b)) return emitNum(1)
|
|
1488
1860
|
return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
|
|
@@ -1494,8 +1866,8 @@ export const emitter = {
|
|
|
1494
1866
|
const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
|
|
1495
1867
|
const va = emit(a), vb = emit(b)
|
|
1496
1868
|
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)
|
|
1869
|
+
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1870
|
+
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1499
1871
|
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
|
|
1500
1872
|
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
|
|
1501
1873
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
@@ -1505,6 +1877,8 @@ export const emitter = {
|
|
|
1505
1877
|
inc('__eq')
|
|
1506
1878
|
return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
|
|
1507
1879
|
},
|
|
1880
|
+
'===': (a, b) => emitStrictEq(a, b, false),
|
|
1881
|
+
'!==': (a, b) => emitStrictEq(a, b, true),
|
|
1508
1882
|
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
1509
1883
|
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
1510
1884
|
'<=': cmpOp('le_s', 'le', (a, b) => a <= b),
|
|
@@ -1519,7 +1893,9 @@ export const emitter = {
|
|
|
1519
1893
|
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
1520
1894
|
// Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
|
|
1521
1895
|
// Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
|
|
1522
|
-
|
|
1896
|
+
// VAL.BOOL rides the 0/1 numeric carrier (not a pointer), so normalize it to
|
|
1897
|
+
// NUMBER and let it fall to the truthy path — `!false` must be `true`.
|
|
1898
|
+
const vt = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1523
1899
|
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
|
|
1524
1900
|
return isNullish(asF64(v))
|
|
1525
1901
|
}
|
|
@@ -1586,11 +1962,23 @@ export const emitter = {
|
|
|
1586
1962
|
|
|
1587
1963
|
'&&': (a, b) => {
|
|
1588
1964
|
const va = emit(a)
|
|
1589
|
-
|
|
1965
|
+
// Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
|
|
1966
|
+
if (isLit(va)) {
|
|
1967
|
+
const v = litVal(va)
|
|
1968
|
+
if (v !== 0 && v === v) {
|
|
1969
|
+
const refs = extractRefinements(a, new Map(), true)
|
|
1970
|
+
return withRefinements(refs, b, () => emit(b))
|
|
1971
|
+
}
|
|
1972
|
+
return va
|
|
1973
|
+
}
|
|
1974
|
+
// a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
|
|
1975
|
+
// (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
|
|
1976
|
+
const rightRefs = extractRefinements(a, new Map(), true)
|
|
1977
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
1590
1978
|
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
1591
1979
|
// skip f64 round-trip and __is_truthy call entirely.
|
|
1592
1980
|
if (va.type === 'i32') {
|
|
1593
|
-
const vb =
|
|
1981
|
+
const vb = emitRight()
|
|
1594
1982
|
const t = tempI32()
|
|
1595
1983
|
if (vb.type === 'i32') {
|
|
1596
1984
|
return typed(['if', ['result', 'i32'],
|
|
@@ -1607,15 +1995,25 @@ export const emitter = {
|
|
|
1607
1995
|
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1608
1996
|
return typed(['if', ['result', 'f64'],
|
|
1609
1997
|
toBoolFromEmitted(teed),
|
|
1610
|
-
['then', asF64(
|
|
1998
|
+
['then', asF64(emitRight())],
|
|
1611
1999
|
['else', ['local.get', `$${t}`]]], 'f64')
|
|
1612
2000
|
},
|
|
1613
2001
|
|
|
1614
2002
|
'||': (a, b) => {
|
|
1615
2003
|
const va = emit(a)
|
|
1616
|
-
|
|
2004
|
+
// Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
|
|
2005
|
+
if (isLit(va)) {
|
|
2006
|
+
const v = litVal(va)
|
|
2007
|
+
if (v !== 0 && v === v) return va
|
|
2008
|
+
const refs = extractRefinements(a, new Map(), false)
|
|
2009
|
+
return withRefinements(refs, b, () => emit(b))
|
|
2010
|
+
}
|
|
2011
|
+
// a is falsy in the right-arm — `x == null || ...` proves x is null/undefined in b;
|
|
2012
|
+
// De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
|
|
2013
|
+
const rightRefs = extractRefinements(a, new Map(), false)
|
|
2014
|
+
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
1617
2015
|
if (va.type === 'i32') {
|
|
1618
|
-
const vb =
|
|
2016
|
+
const vb = emitRight()
|
|
1619
2017
|
const t = tempI32()
|
|
1620
2018
|
if (vb.type === 'i32') {
|
|
1621
2019
|
return typed(['if', ['result', 'i32'],
|
|
@@ -1633,7 +2031,7 @@ export const emitter = {
|
|
|
1633
2031
|
return typed(['if', ['result', 'f64'],
|
|
1634
2032
|
toBoolFromEmitted(teed),
|
|
1635
2033
|
['then', ['local.get', `$${t}`]],
|
|
1636
|
-
['else', asF64(
|
|
2034
|
+
['else', asF64(emitRight())]], 'f64')
|
|
1637
2035
|
},
|
|
1638
2036
|
|
|
1639
2037
|
// a ?? b: returns b only if a is nullish
|
|
@@ -1669,7 +2067,7 @@ export const emitter = {
|
|
|
1669
2067
|
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
1670
2068
|
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
1671
2069
|
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
1672
|
-
'~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v
|
|
2070
|
+
'~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(isI32Num(v) ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32') },
|
|
1673
2071
|
...Object.fromEntries([
|
|
1674
2072
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
1675
2073
|
].map(([op, fn]) => [op, (a, b) => {
|
|
@@ -1682,19 +2080,28 @@ export const emitter = {
|
|
|
1682
2080
|
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
1683
2081
|
if (op === '>>') return emitNum(la >> lb)
|
|
1684
2082
|
}
|
|
1685
|
-
const ca = va
|
|
1686
|
-
const cb = vb
|
|
2083
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
2084
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1687
2085
|
return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
|
|
1688
2086
|
}])),
|
|
1689
2087
|
'>>>': (a, b) => {
|
|
1690
|
-
const va = emit(a), vb = emit(b)
|
|
1691
|
-
if (
|
|
2088
|
+
const va = emit(a), vb = emit(b)
|
|
2089
|
+
if (isLit(va) && isLit(vb)) {
|
|
2090
|
+
const r = litVal(va) >>> litVal(vb) // JS uint32 result ∈ [0, 2^32)
|
|
2091
|
+
// ≥ 2^31 doesn't fit signed i32: materialize the wrapped bits as an i32 const
|
|
2092
|
+
// tagged `.unsigned` so `asF64` lifts via `convert_i32_u`. Emitting `f64.const r`
|
|
2093
|
+
// here (the old foldConst path) would `trunc_sat_f64_s`-saturate to INT32_MAX
|
|
2094
|
+
// when the enclosing function narrows to an i32 result. Values < 2^31 fold to a
|
|
2095
|
+
// plain i32 const (signed == unsigned, stays foldable downstream).
|
|
2096
|
+
if (r >= 0x80000000) { const node = typed(['i32.const', r | 0], 'i32'); node.unsigned = true; return node }
|
|
2097
|
+
return emitNum(r)
|
|
2098
|
+
}
|
|
1692
2099
|
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
1693
2100
|
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
1694
2101
|
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
1695
2102
|
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
1696
|
-
const ca = va
|
|
1697
|
-
const cb = vb
|
|
2103
|
+
const ca = isI32Num(va) || isLit(va) ? va : toNumF64(a, va)
|
|
2104
|
+
const cb = isI32Num(vb) || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1698
2105
|
const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
|
|
1699
2106
|
node.unsigned = true
|
|
1700
2107
|
return node
|
|
@@ -1773,8 +2180,24 @@ export const emitter = {
|
|
|
1773
2180
|
},
|
|
1774
2181
|
|
|
1775
2182
|
'while': (cond, body) => emitter['for'](null, cond, null, body),
|
|
1776
|
-
'
|
|
1777
|
-
|
|
2183
|
+
'label': (name, body) => {
|
|
2184
|
+
const brk = `$label${ctx.func.uniq++}`
|
|
2185
|
+
ctx.func.stack.push({ label: name, brk })
|
|
2186
|
+
const result = ['block', brk, ...emitFlat(body)]
|
|
2187
|
+
ctx.func.stack.pop()
|
|
2188
|
+
return result
|
|
2189
|
+
},
|
|
2190
|
+
'break': (label) => {
|
|
2191
|
+
const target = label == null
|
|
2192
|
+
? loopTop().brk
|
|
2193
|
+
: ctx.func.stack.findLast(frame => frame.label === label)?.brk
|
|
2194
|
+
if (!target) err(`break label '${label}' is not in scope`)
|
|
2195
|
+
return [...emitFinalizers(), ['br', target]]
|
|
2196
|
+
},
|
|
2197
|
+
'continue': (label) => {
|
|
2198
|
+
if (label != null) err(`continue label '${label}' is not supported`)
|
|
2199
|
+
return [...emitFinalizers(), ['br', loopTop().loop]]
|
|
2200
|
+
},
|
|
1778
2201
|
|
|
1779
2202
|
// === Call ===
|
|
1780
2203
|
|
|
@@ -1823,8 +2246,6 @@ export const emitter = {
|
|
|
1823
2246
|
let argList = Array.isArray(callArgs)
|
|
1824
2247
|
? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
|
|
1825
2248
|
: 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
2249
|
|
|
1829
2250
|
// Helper: expand spread arguments into flat list of normal arguments + spread markers
|
|
1830
2251
|
// Returns { normal: [...], spreads: [(pos, expr), ...] }
|
|
@@ -1844,6 +2265,13 @@ export const emitter = {
|
|
|
1844
2265
|
|
|
1845
2266
|
const parsed = parseArgs(argList)
|
|
1846
2267
|
|
|
2268
|
+
// Closure devirtualization: a callee that is a module global proven (by
|
|
2269
|
+
// plan.js) to hold one statically-known function for the whole post-init
|
|
2270
|
+
// program rewrites to that function — the known-top-level-function branch
|
|
2271
|
+
// below then emits a direct `call`, dropping the indirect/trampoline path.
|
|
2272
|
+
if (typeof callee === 'string' && ctx.func.globalDevirt?.has(callee))
|
|
2273
|
+
callee = ctx.func.globalDevirt.get(callee)
|
|
2274
|
+
|
|
1847
2275
|
// Optional method call: obj?.method(args) — null if obj is nullish, else
|
|
1848
2276
|
// obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
|
|
1849
2277
|
// distinct from the regular method call's ['.', obj, method] callee. Receiver
|
|
@@ -1868,8 +2296,27 @@ export const emitter = {
|
|
|
1868
2296
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1869
2297
|
const [, obj, method] = callee
|
|
1870
2298
|
|
|
1871
|
-
//
|
|
1872
|
-
|
|
2299
|
+
// charCodeAt with a statically in-bounds index — emit the i32
|
|
2300
|
+
// (OOB-impossible) contract directly; the generic path keeps the
|
|
2301
|
+
// f64/NaN JS-spec result. See analyze.js inBoundsCharCodeAt.
|
|
2302
|
+
if (method === 'charCodeAt' && !parsed.hasSpread && parsed.normal.length === 1
|
|
2303
|
+
&& ctx.abi.string?.ops?.charCodeAt && inBoundsCharCodeAt(ctx).has(callee)) {
|
|
2304
|
+
const recv = emit(obj)
|
|
2305
|
+
// jsstring carrier: receiver is an externref boundary param. Route to
|
|
2306
|
+
// `wasm:js-string.charCodeAt` directly — the in-bounds proof rules out
|
|
2307
|
+
// the OOB trap the builtin would otherwise raise.
|
|
2308
|
+
if (recv?.type === 'externref') {
|
|
2309
|
+
ctx.core.jsstring.add('charCodeAt')
|
|
2310
|
+
return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
|
|
2311
|
+
}
|
|
2312
|
+
return typed(ctx.abi.string.ops.charCodeAt(
|
|
2313
|
+
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// Function property call: fn.prop(args) → direct call to fn$prop.
|
|
2317
|
+
// Skipped when the property was reassigned (wrapper composition) — then
|
|
2318
|
+
// it is a mutable slot and must be read dynamically before the call.
|
|
2319
|
+
if (typeof obj === 'string' && ctx.func.names.has(obj) && !ctx.func.multiProp.has(`${obj}.${method}`)) {
|
|
1873
2320
|
const fname = `${obj}$${method}`
|
|
1874
2321
|
if (ctx.func.names.has(fname)) {
|
|
1875
2322
|
const func = ctx.func.map.get(fname)
|
|
@@ -1885,9 +2332,19 @@ export const emitter = {
|
|
|
1885
2332
|
}
|
|
1886
2333
|
|
|
1887
2334
|
let vt = keyValType(obj)
|
|
2335
|
+
// A reassigned slice/concat receiver may carry a stale `vt` — a reassignment
|
|
2336
|
+
// inside a nested closure escapes analyzeValTypes' poisoning (its walk stops
|
|
2337
|
+
// at `=>`). Drop to runtime dispatch, but only for guessy types: STRING/ARRAY
|
|
2338
|
+
// dispatch correctly either way, and BUFFER/TYPED are construction proofs
|
|
2339
|
+
// (`new ArrayBuffer`/`new XxxArray`) — the runtime String/Array fallback has
|
|
2340
|
+
// no branch for them, so nulling `vt` would miscompile `ab.slice()` into an
|
|
2341
|
+
// f64-array copy. jzify also splits every `var x = init` into `let x; x = init`,
|
|
2342
|
+
// marking single-assignment vars "reassigned"; keeping definite BUFFER/TYPED
|
|
2343
|
+
// is what keeps `var`-declared buffers correct.
|
|
1888
2344
|
if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
|
|
1889
2345
|
&& (method === 'slice' || method === 'concat')
|
|
1890
|
-
&& vt !== VAL.STRING && vt !== VAL.ARRAY
|
|
2346
|
+
&& vt !== VAL.STRING && vt !== VAL.ARRAY
|
|
2347
|
+
&& vt !== VAL.BUFFER && vt !== VAL.TYPED) vt = null
|
|
1891
2348
|
|
|
1892
2349
|
// Helper to call method with arguments (handles spread expansion)
|
|
1893
2350
|
const callMethod = (objArg, methodEmitter) => {
|
|
@@ -1915,22 +2372,14 @@ export const emitter = {
|
|
|
1915
2372
|
ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
|
|
1916
2373
|
|
|
1917
2374
|
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')
|
|
2375
|
+
// A materialized multi-value is not a statically-typed pointer — let
|
|
2376
|
+
// emitSpreadCopy resolve its kind once at runtime.
|
|
2377
|
+
const srcVT = multiCount(spreadExpr) ? undefined : valTypeOf(spreadExpr)
|
|
1924
2378
|
const n = multiCount(spreadExpr)
|
|
1925
2379
|
const ir = []
|
|
1926
2380
|
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1927
2381
|
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
|
-
}
|
|
2382
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1934
2383
|
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1935
2384
|
if (objIsArr) {
|
|
1936
2385
|
ir.push(['local.set', `$${ol}`,
|
|
@@ -1943,20 +2392,9 @@ export const emitter = {
|
|
|
1943
2392
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1944
2393
|
// base captured AFTER grow (grow may relocate the array).
|
|
1945
2394
|
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}`]]])
|
|
2395
|
+
// Bulk-copy the spread: an ARRAY source is a contiguous f64 block → memory.copy.
|
|
2396
|
+
ir.push(['local.set', `$${si}`, ['local.get', `$${ol}`]])
|
|
2397
|
+
ir.push(...emitSpreadCopy(base, si, sa, sl, srcVT))
|
|
1960
2398
|
// Single set_len for the full spread.
|
|
1961
2399
|
ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1962
2400
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
@@ -1978,6 +2416,9 @@ export const emitter = {
|
|
|
1978
2416
|
const acc = `${T}acc${ctx.func.uniq++}`, arr = `${T}sp${ctx.func.uniq++}`, len = `${T}splen${ctx.func.uniq++}`, idx = `${T}spidx${ctx.func.uniq++}`
|
|
1979
2417
|
ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
|
|
1980
2418
|
ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
|
|
2419
|
+
// Emit-time rep seeding for a fresh spread-staging local (no prior reader).
|
|
2420
|
+
// Without this, the loop body's `[]` read on `arr` falls back to polymorphic
|
|
2421
|
+
// dispatch — VAL.* on the rep elides STRING gate for ARRAY/TYPED spreads.
|
|
1981
2422
|
const spreadVT = valTypeOf(spreadExpr)
|
|
1982
2423
|
if (spreadVT) updateRep(arr, { val: spreadVT })
|
|
1983
2424
|
|
|
@@ -2042,6 +2483,7 @@ export const emitter = {
|
|
|
2042
2483
|
const spreadExpr = item[1]
|
|
2043
2484
|
const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
|
|
2044
2485
|
ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
|
|
2486
|
+
// Emit-time rep seeding for fresh spread-staging local (see arr-spread comment above).
|
|
2045
2487
|
const spreadVT = valTypeOf(spreadExpr)
|
|
2046
2488
|
if (spreadVT) updateRep(arrL, { val: spreadVT })
|
|
2047
2489
|
const n = multiCount(spreadExpr)
|
|
@@ -2117,11 +2559,11 @@ export const emitter = {
|
|
|
2117
2559
|
// Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
|
|
2118
2560
|
const loadInner = [
|
|
2119
2561
|
['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
|
|
2120
|
-
['local.set', `$${innerName}`,
|
|
2562
|
+
['local.set', `$${innerName}`, ctx.abi.object.ops.load(['local.get', `$${boxBase}`], 0)]]
|
|
2121
2563
|
const result = callMethod(innerName, emitter)
|
|
2122
2564
|
// Mutating methods may reallocate; writeback inner value to boxed slot
|
|
2123
2565
|
if (BOXED_MUTATORS.has(method)) {
|
|
2124
|
-
const wb =
|
|
2566
|
+
const wb = ctx.abi.object.ops.store(['local.get', `$${boxBase}`], 0, ['local.get', `$${innerName}`])
|
|
2125
2567
|
return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
|
|
2126
2568
|
}
|
|
2127
2569
|
// Non-mutating: just load inner and call
|
|
@@ -2158,7 +2600,7 @@ export const emitter = {
|
|
|
2158
2600
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
|
|
2159
2601
|
const idx = ctx.schema.find(obj, method)
|
|
2160
2602
|
if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
|
|
2161
|
-
const propRead = typed(
|
|
2603
|
+
const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
|
|
2162
2604
|
if (parsed.hasSpread) {
|
|
2163
2605
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2164
2606
|
return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
|
|
@@ -2171,13 +2613,21 @@ export const emitter = {
|
|
|
2171
2613
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
|
|
2172
2614
|
const idx = ctx.schema.find(obj, method)
|
|
2173
2615
|
if (idx >= 0) {
|
|
2174
|
-
const propRead = typed(
|
|
2616
|
+
const propRead = typed(ctx.abi.object.ops.load(ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), idx), 'f64')
|
|
2175
2617
|
return ctx.closure.call(propRead, parsed.normal)
|
|
2176
2618
|
}
|
|
2177
2619
|
}
|
|
2178
2620
|
|
|
2179
|
-
// Generic only
|
|
2180
|
-
|
|
2621
|
+
// Generic only — but a collection emitter (`.get`/`.set`/`.has`/`.add`/
|
|
2622
|
+
// `.delete`) assumes a Map/Set receiver: a proven collection already
|
|
2623
|
+
// dispatched via `.${vt}:${method}` above, so reaching here means the
|
|
2624
|
+
// receiver is not a proven collection. A zero-arg call then cannot be the
|
|
2625
|
+
// collection op (each needs ≥1 key/value arg) — it is a user/closure
|
|
2626
|
+
// method (e.g. `new C().get()`). Skip the collection emitter so it falls
|
|
2627
|
+
// through to closure/dynamic dispatch instead of crashing on `emit(key)`.
|
|
2628
|
+
const collectionMisfit = COLLECTION_METHODS.has(method) &&
|
|
2629
|
+
!parsed.hasSpread && parsed.normal.length === 0
|
|
2630
|
+
if (ctx.core.emit[genKey] && !collectionMisfit) {
|
|
2181
2631
|
return callMethod(obj, ctx.core.emit[genKey])
|
|
2182
2632
|
}
|
|
2183
2633
|
|
|
@@ -2233,6 +2683,10 @@ export const emitter = {
|
|
|
2233
2683
|
// Unknown callee - assume external method
|
|
2234
2684
|
if (ctx.transform.strict)
|
|
2235
2685
|
err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
|
|
2686
|
+
// Under wasi there is no host `__ext_call` — the call lowers to a
|
|
2687
|
+
// no-op returning `undefined`. This is by-design so polymorphic code
|
|
2688
|
+
// can target js and wasi from one source; users who want fail-fast
|
|
2689
|
+
// pass `strict: true` (handled above).
|
|
2236
2690
|
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
2237
2691
|
inc('__ext_call')
|
|
2238
2692
|
ctx.features.external = true
|
|
@@ -2244,7 +2698,7 @@ export const emitter = {
|
|
|
2244
2698
|
['i64.reinterpret_f64', arrayIR]]], 'f64');
|
|
2245
2699
|
}
|
|
2246
2700
|
|
|
2247
|
-
if (ctx.core.emit[callee]) {
|
|
2701
|
+
if (typeof callee === 'string' && ctx.core.emit[callee] && !isBoundName(callee) && !isUserFunc(callee)) {
|
|
2248
2702
|
// Pass spread args through to emitter (e.g. Math.max(...arr))
|
|
2249
2703
|
if (parsed.hasSpread) {
|
|
2250
2704
|
const allArgs = []
|
|
@@ -2260,7 +2714,7 @@ export const emitter = {
|
|
|
2260
2714
|
}
|
|
2261
2715
|
|
|
2262
2716
|
// Direct call if callee is a known top-level function
|
|
2263
|
-
if (typeof callee === 'string' && ctx.func.names.has(callee) && !
|
|
2717
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee) && !isBoundName(callee)) {
|
|
2264
2718
|
const func = ctx.func.map.get(callee)
|
|
2265
2719
|
|
|
2266
2720
|
// Rest param case: collect all args (including expanded spreads) into array
|
|
@@ -2429,15 +2883,19 @@ export function emit(node, expect) {
|
|
|
2429
2883
|
if (node.loc != null) ctx.error.loc = node.loc
|
|
2430
2884
|
}
|
|
2431
2885
|
if (node == null) return null
|
|
2432
|
-
|
|
2433
|
-
|
|
2886
|
+
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
2887
|
+
// AST), but their working representation is the plain number 0/1 — identical
|
|
2888
|
+
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|
|
2889
|
+
if (node === true) return emitNum(1)
|
|
2890
|
+
if (node === false) return emitNum(0)
|
|
2434
2891
|
if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
|
|
2435
2892
|
return nullExpr()
|
|
2436
2893
|
if (typeof node === 'bigint') {
|
|
2437
|
-
//
|
|
2438
|
-
//
|
|
2439
|
-
|
|
2440
|
-
|
|
2894
|
+
// Truncate to 64 bits — `BigInt.asUintN(64, …)` semantics, same as the
|
|
2895
|
+
// explicit mask `node & 0xFFFFFFFFFFFFFFFFn`. Decimal form (vs. the prior
|
|
2896
|
+
// unsigned-hex dance) is enough now that watr's optimize.js getConst
|
|
2897
|
+
// handles signed strings correctly (4.6.8 W5 fix).
|
|
2898
|
+
return typed(['f64.reinterpret_i64', ['i64.const', BigInt.asUintN(64, node).toString()]], 'f64')
|
|
2441
2899
|
}
|
|
2442
2900
|
if (typeof node === 'number') return emitNum(node)
|
|
2443
2901
|
if (typeof node === 'string') {
|
|
@@ -2473,7 +2931,15 @@ export function emit(node, expect) {
|
|
|
2473
2931
|
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (local $${arr} i32) ${tempLocals} (call $${node} ${fwd}) ${capture} (local.set $${arr} (call $__alloc (i32.const ${n * 8 + 8}))) (i32.store (local.get $${arr}) (i32.const ${n})) (i32.store (i32.add (local.get $${arr}) (i32.const 4)) (i32.const ${n})) (local.set $${arr} (i32.add (local.get $${arr}) (i32.const 8))) ${stores} (call $__mkptr (i32.const 1) (i32.const 0) (local.get $${arr})))`
|
|
2474
2932
|
inc(trampolineName, '__alloc', '__mkptr')
|
|
2475
2933
|
} else {
|
|
2476
|
-
|
|
2934
|
+
// Convert i32/i64 results back to f64 — uniform closure ABI returns f64.
|
|
2935
|
+
const resType = func?.sig.results[0]
|
|
2936
|
+
const callExpr = `(call $${node} ${fwd})`
|
|
2937
|
+
const wrapped = resType === 'i32'
|
|
2938
|
+
? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
|
|
2939
|
+
: resType === 'i64'
|
|
2940
|
+
? `(f64.reinterpret_i64 ${callExpr})`
|
|
2941
|
+
: callExpr
|
|
2942
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${wrapped})`
|
|
2477
2943
|
inc(trampolineName)
|
|
2478
2944
|
}
|
|
2479
2945
|
}
|