jz 0.1.1 → 0.2.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 +257 -90
- package/cli.js +34 -8
- package/index.js +102 -13
- package/module/array.js +396 -133
- package/module/collection.js +455 -212
- package/module/console.js +169 -88
- package/module/core.js +246 -144
- package/module/date.js +691 -0
- package/module/function.js +81 -21
- package/module/index.js +2 -1
- package/module/json.js +483 -138
- package/module/math.js +79 -39
- package/module/number.js +188 -78
- package/module/object.js +227 -47
- package/module/regex.js +33 -30
- package/module/schema.js +14 -17
- package/module/string.js +434 -235
- package/module/symbol.js +4 -3
- package/module/timer.js +89 -31
- package/module/typedarray.js +174 -44
- package/package.json +3 -10
- package/src/analyze.js +626 -132
- package/src/autoload.js +14 -6
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +634 -145
- package/src/host.js +244 -51
- package/src/ir.js +81 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1132 -4
- package/src/prepare.js +321 -75
- package/src/vectorize.js +1016 -0
- package/wasi.js +13 -0
- package/src/key.js +0 -73
- package/src/source.js +0 -76
package/src/emit.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { ctx, err, inc, PTR } from './ctx.js'
|
|
26
|
-
import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal } from './analyze.js'
|
|
26
|
+
import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
|
|
27
27
|
import {
|
|
28
28
|
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
29
29
|
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
|
|
35
35
|
temp, tempI32, tempI64, allocPtr,
|
|
36
36
|
boxedAddr, readVar, writeVar, isNullish,
|
|
37
|
+
isLiteralStr, resolveValType, isFuncRef,
|
|
37
38
|
multiCount, loopTop, flat,
|
|
38
39
|
reconstructArgsWithSpreads,
|
|
39
40
|
} from './ir.js'
|
|
@@ -42,6 +43,46 @@ import {
|
|
|
42
43
|
// to decide whether to emit a value-returning or side-effect-only form.
|
|
43
44
|
let _expect = null
|
|
44
45
|
|
|
46
|
+
const FIRST_CLASS_UNARY_MATH = {
|
|
47
|
+
'math.abs': 'f64.abs',
|
|
48
|
+
'math.sqrt': 'f64.sqrt',
|
|
49
|
+
'math.ceil': 'f64.ceil',
|
|
50
|
+
'math.floor': 'f64.floor',
|
|
51
|
+
'math.trunc': 'f64.trunc',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function builtinFunctionValue(name) {
|
|
55
|
+
const op = FIRST_CLASS_UNARY_MATH[name]
|
|
56
|
+
if (!op) err(`Builtin function '${name}' cannot be used as a first-class value`)
|
|
57
|
+
if (!ctx.closure.table) err(`Builtin function '${name}' used as value requires closure support`)
|
|
58
|
+
const fn = `${T}builtin_${name.replace(/\W/g, '_')}`
|
|
59
|
+
if (!ctx.core.stdlib[fn]) {
|
|
60
|
+
const width = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
61
|
+
const params = ['(param $__env f64)', '(param $__argc i32)']
|
|
62
|
+
for (let i = 0; i < width; i++) params.push(`(param $__a${i} f64)`)
|
|
63
|
+
ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) (${op} (local.get $__a0)))`
|
|
64
|
+
inc(fn)
|
|
65
|
+
}
|
|
66
|
+
let idx = ctx.closure.table.indexOf(fn)
|
|
67
|
+
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(fn) }
|
|
68
|
+
const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
69
|
+
ir.closureFuncIdx = idx
|
|
70
|
+
return ir
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Emit unary negation: constant-fold, or i32 sub from 0 / f64.neg. */
|
|
74
|
+
const emitNeg = (a) => {
|
|
75
|
+
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
76
|
+
const v = emit(a)
|
|
77
|
+
return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32'
|
|
78
|
+
? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32')
|
|
79
|
+
: typed(['f64.neg', toNumF64(a, v)], 'f64')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Try constant-folding binary arith: returns emitNum(result) or null. */
|
|
83
|
+
const foldConst = (va, vb, fn, guard) =>
|
|
84
|
+
isLit(va) && isLit(vb) && (!guard || guard(litVal(vb))) ? emitNum(fn(litVal(va), litVal(vb))) : null
|
|
85
|
+
|
|
45
86
|
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
46
87
|
export function emitTypeofCmp(a, b, cmpOp) {
|
|
47
88
|
let typeofExpr, code
|
|
@@ -62,10 +103,7 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
62
103
|
if (code === -2) {
|
|
63
104
|
inc('__ptr_type')
|
|
64
105
|
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
65
|
-
const
|
|
66
|
-
const isStr = ['i32.or',
|
|
67
|
-
['i32.eq', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
|
|
68
|
-
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]]
|
|
106
|
+
const isStr = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]]
|
|
69
107
|
return typed(eq ? ['i32.and', isPtr, isStr]
|
|
70
108
|
: ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isStr]], 'i32')
|
|
71
109
|
}
|
|
@@ -81,9 +119,7 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
81
119
|
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
82
120
|
const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
|
|
83
121
|
const notStrFn = ['i32.and',
|
|
84
|
-
['i32.
|
|
85
|
-
['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
|
|
86
|
-
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
|
|
122
|
+
['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]], ['i32.const', PTR.STRING]],
|
|
87
123
|
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]]
|
|
88
124
|
const notNullish = ['i32.eqz', isNullish(['local.get', `$${t}`])]
|
|
89
125
|
const check = ['i32.and', ['i32.and', isPtr, notStrFn], notNullish]
|
|
@@ -92,13 +128,24 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
92
128
|
if (code === -6) {
|
|
93
129
|
inc('__ptr_type')
|
|
94
130
|
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
95
|
-
const isFn = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', PTR.CLOSURE]]
|
|
131
|
+
const isFn = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.CLOSURE]]
|
|
96
132
|
return typed(eq ? ['i32.and', isPtr, isFn] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isFn]], 'i32')
|
|
97
133
|
}
|
|
134
|
+
if (code === -7) {
|
|
135
|
+
const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
|
|
136
|
+
if (vt) return typed(['i32.const', (vt === VAL.BIGINT) === eq ? 1 : 0], 'i32')
|
|
137
|
+
const n = ['local.tee', `$${t}`, va]
|
|
138
|
+
const isBigInt = ['i32.and',
|
|
139
|
+
['f64.eq', n, ['local.get', `$${t}`]],
|
|
140
|
+
['i32.and',
|
|
141
|
+
['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]],
|
|
142
|
+
['f64.lt', ['f64.abs', ['local.get', `$${t}`]], ['f64.const', 2.2250738585072014e-308]]]]
|
|
143
|
+
return typed(eq ? isBigInt : ['i32.eqz', isBigInt], 'i32')
|
|
144
|
+
}
|
|
98
145
|
if (code >= 0) {
|
|
99
146
|
inc('__ptr_type')
|
|
100
147
|
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
101
|
-
const check = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', code]]
|
|
148
|
+
const check = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', code]]
|
|
102
149
|
return typed(eq ? ['i32.and', isPtr, check] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', check]], 'i32')
|
|
103
150
|
}
|
|
104
151
|
return null
|
|
@@ -112,9 +159,74 @@ const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
|
|
|
112
159
|
// heap strings) and BIGINT (content compare).
|
|
113
160
|
const REF_EQ_KINDS = new Set([
|
|
114
161
|
VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
|
|
115
|
-
VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX,
|
|
162
|
+
VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX, VAL.DATE,
|
|
116
163
|
])
|
|
117
164
|
|
|
165
|
+
function stringLiteral(node) {
|
|
166
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
167
|
+
if (Array.isArray(node) && node[0] == null && typeof node[1] === 'string') return node[1]
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function nonNegIntLiteral(node) {
|
|
172
|
+
const n = intLiteralValue(node)
|
|
173
|
+
return n != null && n >= 0 ? n : null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
177
|
+
const leftLit = stringLiteral(a)
|
|
178
|
+
const rightLit = stringLiteral(b)
|
|
179
|
+
const aIdx = Array.isArray(a) && a[0] === '[]'
|
|
180
|
+
const bIdx = Array.isArray(b) && b[0] === '[]'
|
|
181
|
+
let indexed, lit
|
|
182
|
+
if (bIdx && leftLit != null) { indexed = b; lit = leftLit }
|
|
183
|
+
else if (aIdx && rightLit != null) { indexed = a; lit = rightLit }
|
|
184
|
+
else return null
|
|
185
|
+
|
|
186
|
+
if (lit.length === 0) return null
|
|
187
|
+
if ([...lit].some(c => c.charCodeAt(0) > 0x7F)) return null
|
|
188
|
+
|
|
189
|
+
const [, obj, key] = indexed
|
|
190
|
+
const idx = nonNegIntLiteral(key)
|
|
191
|
+
if (idx == null) return null
|
|
192
|
+
|
|
193
|
+
const vt = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
|
|
194
|
+
if (vt && vt !== VAL.STRING) return null
|
|
195
|
+
|
|
196
|
+
const finish = expr => negate ? ['i32.eqz', expr] : expr
|
|
197
|
+
|
|
198
|
+
// Known STRING: s[i] always returns 1-char SSO. Multi-char literal → always false.
|
|
199
|
+
if (vt === VAL.STRING && lit.length > 1) return emitNum(negate ? 1 : 0)
|
|
200
|
+
|
|
201
|
+
// Single-char literal: compare byte directly, skipping __str_idx allocation.
|
|
202
|
+
if (lit.length !== 1 || !ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
|
|
203
|
+
|
|
204
|
+
const ptr = temp('sc'), idxIR = ['i32.const', idx]
|
|
205
|
+
inc('__str_byteLen', '__char_at')
|
|
206
|
+
const charEq = ['if', ['result', 'i32'],
|
|
207
|
+
['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], idxIR],
|
|
208
|
+
['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxIR], ['i32.const', lit.charCodeAt(0)]]],
|
|
209
|
+
['else', ['i32.const', 0]]]
|
|
210
|
+
|
|
211
|
+
if (vt === VAL.STRING) {
|
|
212
|
+
return typed(['block', ['result', 'i32'],
|
|
213
|
+
['local.set', `$${ptr}`, asF64(emit(obj))],
|
|
214
|
+
finish(charEq)], 'i32')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
inc('__ptr_type', '__typed_idx', '__eq')
|
|
218
|
+
const genericEq = ['call', '$__eq',
|
|
219
|
+
['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxIR]],
|
|
220
|
+
asI64(emit(['str', lit]))]
|
|
221
|
+
const cmp = ['if', ['result', 'i32'],
|
|
222
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], ['i32.const', PTR.STRING]],
|
|
223
|
+
['then', charEq],
|
|
224
|
+
['else', genericEq]]
|
|
225
|
+
return typed(['block', ['result', 'i32'],
|
|
226
|
+
['local.set', `$${ptr}`, asF64(emit(obj))],
|
|
227
|
+
finish(cmp)], 'i32')
|
|
228
|
+
}
|
|
229
|
+
|
|
118
230
|
// === Flow-sensitive type refinement ===
|
|
119
231
|
// Map typeof code (from resolveTypeof in prepare.js) → VAL kind. Undef/boolean/object have no
|
|
120
232
|
// single VAL refinement, so they're excluded. String/number/function do.
|
|
@@ -219,6 +331,19 @@ function containsNestedLoop(body) {
|
|
|
219
331
|
return false
|
|
220
332
|
}
|
|
221
333
|
|
|
334
|
+
function nestedSmallLoopBudget(body) {
|
|
335
|
+
if (!Array.isArray(body)) return 1
|
|
336
|
+
if (body[0] === '=>') return 1
|
|
337
|
+
if (body[0] === 'for') {
|
|
338
|
+
const [, init, cond, step, loopBody] = body
|
|
339
|
+
const n = smallConstForTripCount(init, cond, step)
|
|
340
|
+
return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
|
|
341
|
+
}
|
|
342
|
+
let max = 1
|
|
343
|
+
for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
|
|
344
|
+
return max
|
|
345
|
+
}
|
|
346
|
+
|
|
222
347
|
function containsDeclOf(body, name) {
|
|
223
348
|
if (!Array.isArray(body)) return false
|
|
224
349
|
const op = body[0]
|
|
@@ -251,8 +376,17 @@ function cloneWithSubst(node, name, value) {
|
|
|
251
376
|
}
|
|
252
377
|
|
|
253
378
|
const MAX_SMALL_FOR_UNROLL = 8
|
|
379
|
+
const MAX_NESTED_FOR_UNROLL = 64
|
|
254
380
|
|
|
255
|
-
function
|
|
381
|
+
function containsKnownTypedArrayIndex(body) {
|
|
382
|
+
if (!Array.isArray(body)) return false
|
|
383
|
+
if (body[0] === '=>') return false
|
|
384
|
+
if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
|
|
385
|
+
for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function smallConstForTripCount(init, cond, step) {
|
|
256
390
|
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
257
391
|
const decl = init[1]
|
|
258
392
|
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
@@ -268,8 +402,19 @@ function unrollSmallConstFor(init, cond, step, body) {
|
|
|
268
402
|
(step[0] === '++' && step[1] === name) ||
|
|
269
403
|
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
270
404
|
)
|
|
271
|
-
|
|
272
|
-
|
|
405
|
+
return stepOk ? end : null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function unrollSmallConstFor(init, cond, step, body) {
|
|
409
|
+
const end = smallConstForTripCount(init, cond, step)
|
|
410
|
+
if (end == null) return null
|
|
411
|
+
const name = init[1][1]
|
|
412
|
+
if (containsNestedLoop(body)) {
|
|
413
|
+
const nestedMode = ctx.transform.optimize?.nestedSmallConstForUnroll
|
|
414
|
+
if (nestedMode !== true && (nestedMode !== 'auto' || !containsKnownTypedArrayIndex(body))) return null
|
|
415
|
+
if (end * nestedSmallLoopBudget(body) > MAX_NESTED_FOR_UNROLL) return null
|
|
416
|
+
}
|
|
417
|
+
if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsDeclOf(body, name)) return null
|
|
273
418
|
if (isReassigned(body, name)) return null
|
|
274
419
|
|
|
275
420
|
const out = []
|
|
@@ -374,7 +519,7 @@ export function materializeMulti(callNode) {
|
|
|
374
519
|
? rawArgs[0].slice(1) : rawArgs
|
|
375
520
|
const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
376
521
|
while (emittedArgs.length < func.sig.params.length)
|
|
377
|
-
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
522
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
378
523
|
const temps = Array.from({ length: n }, () => temp())
|
|
379
524
|
const out = allocPtr({ type: 1, len: n, tag: 'marr' })
|
|
380
525
|
const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
|
|
@@ -391,13 +536,13 @@ export function emitDecl(...inits) {
|
|
|
391
536
|
for (let ii = 0; ii < inits.length; ii++) {
|
|
392
537
|
const i = inits[ii]
|
|
393
538
|
if (typeof i === 'string') {
|
|
394
|
-
const undef =
|
|
539
|
+
const undef = undefExpr()
|
|
395
540
|
if (ctx.func.boxed.has(i)) {
|
|
396
541
|
const cell = ctx.func.boxed.get(i)
|
|
397
542
|
ctx.func.locals.set(cell, 'i32')
|
|
398
|
-
|
|
399
|
-
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]]
|
|
400
|
-
|
|
543
|
+
if (!ctx.func.preboxed?.has(i))
|
|
544
|
+
result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
|
|
545
|
+
result.push(['f64.store', ['local.get', `$${cell}`], undef])
|
|
401
546
|
continue
|
|
402
547
|
}
|
|
403
548
|
if (isGlobal(i)) {
|
|
@@ -436,7 +581,7 @@ export function emitDecl(...inits) {
|
|
|
436
581
|
? rawArgs[0].slice(1) : rawArgs
|
|
437
582
|
const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
438
583
|
while (emittedArgs.length < func.sig.params.length)
|
|
439
|
-
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
584
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
440
585
|
result.push(['call', `$${init[1]}`, ...emittedArgs])
|
|
441
586
|
for (let k = n - 1; k >= 0; k--)
|
|
442
587
|
result.push(['local.set', `$${targets[k]}`])
|
|
@@ -446,14 +591,14 @@ export function emitDecl(...inits) {
|
|
|
446
591
|
}
|
|
447
592
|
}
|
|
448
593
|
const isObjLit = Array.isArray(init) && init[0] === '{}'
|
|
449
|
-
if (isObjLit) ctx.schema.targetStack.push(name)
|
|
594
|
+
if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
|
|
450
595
|
const val = emit(init)
|
|
451
596
|
if (isObjLit) ctx.schema.targetStack.pop()
|
|
452
597
|
// Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
|
|
453
598
|
// Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
|
|
454
599
|
// isReassigned is conservative across nested arrow shadows — we miss the optimization
|
|
455
600
|
// rather than emit a wrong direct call.
|
|
456
|
-
if (val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
601
|
+
if (Array.isArray(init) && init[0] === '=>' && val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
457
602
|
&& ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
458
603
|
if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
|
|
459
604
|
ctx.func.directClosures.set(name, val.closureBodyName)
|
|
@@ -461,9 +606,9 @@ export function emitDecl(...inits) {
|
|
|
461
606
|
if (ctx.func.boxed.has(name)) {
|
|
462
607
|
const cell = ctx.func.boxed.get(name)
|
|
463
608
|
ctx.func.locals.set(cell, 'i32')
|
|
464
|
-
|
|
465
|
-
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]]
|
|
466
|
-
|
|
609
|
+
if (!ctx.func.preboxed?.has(name))
|
|
610
|
+
result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
|
|
611
|
+
result.push(['f64.store', ['local.get', `$${cell}`], asF64(val)])
|
|
467
612
|
continue
|
|
468
613
|
}
|
|
469
614
|
if (isGlobal(name)) {
|
|
@@ -510,21 +655,21 @@ export function emitDecl(...inits) {
|
|
|
510
655
|
} else {
|
|
511
656
|
coerced = localType === 'f64' ? asF64(val) : asI32(val)
|
|
512
657
|
}
|
|
513
|
-
if (!(isLit(coerced) && coerced[1] === 0 && !ctx.func.stack.length))
|
|
658
|
+
if (!(isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length))
|
|
514
659
|
result.push(['local.set', `$${name}`, coerced])
|
|
515
660
|
|
|
516
661
|
const schemaId = ctx.schema.idOf?.(name)
|
|
517
662
|
if (ctx.func.localProps?.has(name) && schemaId != null) {
|
|
518
663
|
const schema = ctx.schema.resolve(name)
|
|
519
664
|
if (schema?.[0] === '__inner__') {
|
|
520
|
-
inc('
|
|
665
|
+
inc('__alloc_hdr', '__mkptr')
|
|
521
666
|
const bt = `${T}bx${ctx.func.uniq++}`
|
|
522
667
|
ctx.func.locals.set(bt, 'i32')
|
|
523
668
|
const innerName = `${name}${T}inner`
|
|
524
669
|
ctx.func.locals.set(innerName, 'f64')
|
|
525
670
|
result.push(
|
|
526
671
|
['local.set', `$${innerName}`, ['local.get', `$${name}`]],
|
|
527
|
-
['local.set', `$${bt}`, ['call', '$
|
|
672
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
|
|
528
673
|
['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
|
|
529
674
|
...schema.slice(1).map((_, j) =>
|
|
530
675
|
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
|
|
@@ -599,11 +744,11 @@ export function buildArrayWithSpreads(items) {
|
|
|
599
744
|
const n = multiCount(sec.expr)
|
|
600
745
|
ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
|
|
601
746
|
if (sec.baseLocal) {
|
|
602
|
-
ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['local.get', `$${sec.local}`]]])
|
|
747
|
+
ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
603
748
|
ir.push(['local.set', `$${sec.lenLocal}`, ['i32.load', ['i32.sub', ['local.get', `$${sec.baseLocal}`], ['i32.const', 8]]]])
|
|
604
749
|
} else {
|
|
605
750
|
// Cache __len once per spread; reused below for total-len sum and inner copy bound.
|
|
606
|
-
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['local.get', `$${sec.local}`]]])
|
|
751
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
|
|
607
752
|
}
|
|
608
753
|
}
|
|
609
754
|
}
|
|
@@ -636,12 +781,10 @@ export function buildArrayWithSpreads(items) {
|
|
|
636
781
|
? ['f64.load', ['i32.add', ['local.get', `$${sec.baseLocal}`], ['i32.shl', ['local.get', `$${sidx}`], ['i32.const', 3]]]]
|
|
637
782
|
: ctx.module.modules['string']
|
|
638
783
|
? ['if', ['result', 'f64'],
|
|
639
|
-
['i32.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])]]
|
|
644
|
-
: (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])
|
|
784
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]], ['i32.const', PTR.STRING]],
|
|
785
|
+
['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])],
|
|
786
|
+
['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])]]
|
|
787
|
+
: (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])
|
|
645
788
|
ir.push(
|
|
646
789
|
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
647
790
|
['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
|
|
@@ -710,6 +853,20 @@ export function emitBody(node) {
|
|
|
710
853
|
const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
711
854
|
const va = emit(a), vb = emit(b)
|
|
712
855
|
if (isLit(va) && isLit(vb)) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
|
|
856
|
+
// String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
|
|
857
|
+
// (always false), so without this the spec-correct `"a" < "b"` returns 0.
|
|
858
|
+
// Route both-STRING operands through __str_cmp's three-way result, then apply
|
|
859
|
+
// the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
|
|
860
|
+
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
861
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
862
|
+
if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
|
|
863
|
+
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
864
|
+
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
865
|
+
}
|
|
866
|
+
if (vta === VAL.STRING && vtb === VAL.STRING) {
|
|
867
|
+
inc('__str_cmp')
|
|
868
|
+
return typed([`i32.${i32op}`, ['call', '$__str_cmp', asI64(va), asI64(vb)], ['i32.const', 0]], 'i32')
|
|
869
|
+
}
|
|
713
870
|
const ai = intConstValue(a), bi = intConstValue(b)
|
|
714
871
|
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
715
872
|
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
@@ -727,13 +884,45 @@ function intConstValue(expr) {
|
|
|
727
884
|
return null
|
|
728
885
|
}
|
|
729
886
|
|
|
887
|
+
function bigintUnsignedBound(expr) {
|
|
888
|
+
const n = bigintConstValue(expr)
|
|
889
|
+
return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function bigintConstValue(expr) {
|
|
893
|
+
if (typeof expr === 'bigint') return expr
|
|
894
|
+
if (!Array.isArray(expr)) return null
|
|
895
|
+
if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
|
|
896
|
+
if (expr[0] === 'u-') {
|
|
897
|
+
const n = bigintConstValue(expr[1])
|
|
898
|
+
return n == null ? null : -n
|
|
899
|
+
}
|
|
900
|
+
return null
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function arrayIndexKey(key) {
|
|
904
|
+
const n = Number(key)
|
|
905
|
+
const u = n >>> 0
|
|
906
|
+
return String(u) === key && u !== 0xffffffff ? u : null
|
|
907
|
+
}
|
|
908
|
+
|
|
730
909
|
/** Compound assignment: read → op → write back (via readVar/writeVar). */
|
|
731
910
|
function compoundAssign(name, val, f64op, i32op) {
|
|
732
911
|
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
733
912
|
const void_ = _expect === 'void'
|
|
734
913
|
const va = readVar(name), vb = emit(val)
|
|
735
|
-
|
|
736
|
-
|
|
914
|
+
// Peel f64.convert_i32_s/u when va is i32 — typed-array integer reads wrap their
|
|
915
|
+
// i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
|
|
916
|
+
// raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
|
|
917
|
+
// hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
|
|
918
|
+
let vbi = vb
|
|
919
|
+
if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
|
|
920
|
+
Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
|
|
921
|
+
const inner = vb[1]
|
|
922
|
+
vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
|
|
923
|
+
}
|
|
924
|
+
if (i32op && va.type === 'i32' && vbi.type === 'i32')
|
|
925
|
+
return writeVar(name, i32op(va, vbi), void_)
|
|
737
926
|
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
738
927
|
}
|
|
739
928
|
|
|
@@ -880,6 +1069,7 @@ export const emitter = {
|
|
|
880
1069
|
|
|
881
1070
|
'=': (name, val) => {
|
|
882
1071
|
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
1072
|
+
const void_ = _expect === 'void'
|
|
883
1073
|
// Array index assignment: arr[i] = x
|
|
884
1074
|
if (Array.isArray(name) && name[0] === '[]') {
|
|
885
1075
|
const [, arr, idx] = name
|
|
@@ -899,28 +1089,34 @@ export const emitter = {
|
|
|
899
1089
|
['local.set', `$${arrTmp}`, arrExpr],
|
|
900
1090
|
['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
|
|
901
1091
|
['local.set', `$${valTmp}`, valueExpr],
|
|
902
|
-
['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${arrTmp}`], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
|
|
1092
|
+
['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
|
|
903
1093
|
]
|
|
904
1094
|
if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
|
|
905
1095
|
body.push(['local.get', `$${valTmp}`])
|
|
906
1096
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
907
1097
|
}
|
|
908
1098
|
const setDyn = () => {
|
|
1099
|
+
if (ctx.transform.strict)
|
|
1100
|
+
err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
|
|
909
1101
|
inc('__dyn_set')
|
|
910
|
-
return typed(['call', '$__dyn_set',
|
|
1102
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
|
|
911
1103
|
}
|
|
912
1104
|
const dispatchKey = (numericIR) => {
|
|
1105
|
+
if (ctx.transform.strict)
|
|
1106
|
+
err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
|
|
913
1107
|
const keyTmp = temp()
|
|
914
1108
|
return typed(['block', ['result', 'f64'],
|
|
915
1109
|
['local.set', `$${keyTmp}`, keyExpr],
|
|
916
|
-
['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
|
|
917
|
-
['then', ['call', '$__dyn_set',
|
|
1110
|
+
['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
|
|
1111
|
+
['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
|
|
918
1112
|
['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
|
|
919
1113
|
}
|
|
920
1114
|
// Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
|
|
921
|
-
const litKey =
|
|
1115
|
+
const litKey = isLiteralStr(idx) ? idx[1]
|
|
1116
|
+
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
1117
|
+
: null
|
|
922
1118
|
if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
|
|
923
|
-
const slot = ctx.schema.find(arr, litKey
|
|
1119
|
+
const slot = ctx.schema.find(arr, litKey)
|
|
924
1120
|
if (slot >= 0) {
|
|
925
1121
|
const t = temp()
|
|
926
1122
|
return typed(['block', ['result', 'f64'],
|
|
@@ -931,11 +1127,25 @@ export const emitter = {
|
|
|
931
1127
|
['local.get', `$${t}`]], 'f64')
|
|
932
1128
|
}
|
|
933
1129
|
}
|
|
1130
|
+
const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
|
|
1131
|
+
if (arrIndex != null && typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
|
|
1132
|
+
const persist = ptr => {
|
|
1133
|
+
if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
|
|
1134
|
+
if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
|
|
1135
|
+
return ['local.set', `$${arr}`, ptr]
|
|
1136
|
+
}
|
|
1137
|
+
return storeArrayValue(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), persist)
|
|
1138
|
+
}
|
|
934
1139
|
if (keyType === VAL.STRING) return setDyn()
|
|
935
1140
|
if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
|
|
936
1141
|
lookupValType(arr) === 'typed') {
|
|
937
|
-
const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val)
|
|
1142
|
+
const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
|
|
938
1143
|
if (r) return r
|
|
1144
|
+
// Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
|
|
1145
|
+
// returns the stored value as f64, used directly as the expr result.
|
|
1146
|
+
inc('__typed_set_idx')
|
|
1147
|
+
return typed(['call', '$__typed_set_idx',
|
|
1148
|
+
asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
|
|
939
1149
|
}
|
|
940
1150
|
if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
|
|
941
1151
|
const inner = ctx.schema.emitInner(arr)
|
|
@@ -962,9 +1172,34 @@ export const emitter = {
|
|
|
962
1172
|
return storeArrayValue(asF64(va), keyExpr, persist)
|
|
963
1173
|
}
|
|
964
1174
|
// arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
|
|
965
|
-
const
|
|
1175
|
+
const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
|
|
1176
|
+
const arrVT = knownArrVT || VAL.OBJECT
|
|
966
1177
|
if (useRuntimeKeyDispatch) {
|
|
967
1178
|
inc('__dyn_set', '__is_str_key')
|
|
1179
|
+
// When arr type is unknown (could be TypedArray) and __typed_set_idx is
|
|
1180
|
+
// available, dispatch the numeric branch through __ptr_type so TypedArray
|
|
1181
|
+
// writes go by element type. Without this, ternary-typed arrays (e.g.
|
|
1182
|
+
// `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
|
|
1183
|
+
// f64.store boxed bytes regardless of element width.
|
|
1184
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1185
|
+
if (knownArrVT == null && hasTypedSet) {
|
|
1186
|
+
const objTmp = temp('asu')
|
|
1187
|
+
const idxTmp = tempI32('asi')
|
|
1188
|
+
inc('__ptr_type', '__typed_set_idx')
|
|
1189
|
+
return dispatchKey(keyNode => {
|
|
1190
|
+
const keyI32 = asI32(typed(keyNode, 'f64'))
|
|
1191
|
+
return ['block', ['result', 'f64'],
|
|
1192
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1193
|
+
['local.set', `$${idxTmp}`, keyI32],
|
|
1194
|
+
['local.set', `$${t}`, vv],
|
|
1195
|
+
['if', ['result', 'f64'],
|
|
1196
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1197
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1198
|
+
['else', ['block', ['result', 'f64'],
|
|
1199
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1200
|
+
['local.get', `$${t}`]]]]]
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
968
1203
|
return dispatchKey(keyNode => {
|
|
969
1204
|
const keyI32 = asI32(typed(keyNode, 'f64'))
|
|
970
1205
|
return ['block', ['result', 'f64'],
|
|
@@ -973,6 +1208,62 @@ export const emitter = {
|
|
|
973
1208
|
['local.get', `$${t}`]]
|
|
974
1209
|
})
|
|
975
1210
|
}
|
|
1211
|
+
if (typeof arr !== 'string') {
|
|
1212
|
+
const objTmp = temp('asu')
|
|
1213
|
+
const idxTmp = tempI32('asi')
|
|
1214
|
+
const ptrTmp = temp('asp')
|
|
1215
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1216
|
+
inc('__ptr_type', '__arr_set_idx_ptr')
|
|
1217
|
+
if (hasTypedSet) inc('__typed_set_idx')
|
|
1218
|
+
return typed(['block', ['result', 'f64'],
|
|
1219
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1220
|
+
['local.set', `$${idxTmp}`, vi],
|
|
1221
|
+
['local.set', `$${t}`, vv],
|
|
1222
|
+
['if', ['result', 'f64'],
|
|
1223
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
|
|
1224
|
+
['then', ['block', ['result', 'f64'],
|
|
1225
|
+
['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1226
|
+
['local.get', `$${t}`]]],
|
|
1227
|
+
['else', hasTypedSet ? ['if', ['result', 'f64'],
|
|
1228
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1229
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1230
|
+
['else', ['block', ['result', 'f64'],
|
|
1231
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1232
|
+
['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
|
|
1233
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1234
|
+
['local.get', `$${t}`]]]]], 'f64')
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof arr === 'string' && knownArrVT == null) {
|
|
1237
|
+
const objTmp = temp('asu')
|
|
1238
|
+
const idxTmp = tempI32('asi')
|
|
1239
|
+
const ptrTmp = temp('asp')
|
|
1240
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1241
|
+
inc('__ptr_type', '__arr_set_idx_ptr')
|
|
1242
|
+
if (hasTypedSet) inc('__typed_set_idx')
|
|
1243
|
+
const persist = ptr => {
|
|
1244
|
+
if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
|
|
1245
|
+
if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
|
|
1246
|
+
return ['local.set', `$${arr}`, ptr]
|
|
1247
|
+
}
|
|
1248
|
+
return typed(['block', ['result', 'f64'],
|
|
1249
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1250
|
+
['local.set', `$${idxTmp}`, vi],
|
|
1251
|
+
['local.set', `$${t}`, vv],
|
|
1252
|
+
['if', ['result', 'f64'],
|
|
1253
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
|
|
1254
|
+
['then', ['block', ['result', 'f64'],
|
|
1255
|
+
['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1256
|
+
persist(['local.get', `$${ptrTmp}`]),
|
|
1257
|
+
['local.get', `$${t}`]]],
|
|
1258
|
+
['else', hasTypedSet ? ['if', ['result', 'f64'],
|
|
1259
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1260
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1261
|
+
['else', ['block', ['result', 'f64'],
|
|
1262
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1263
|
+
['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
|
|
1264
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1265
|
+
['local.get', `$${t}`]]]]], 'f64')
|
|
1266
|
+
}
|
|
976
1267
|
return typed(['block', ['result', 'f64'],
|
|
977
1268
|
['local.set', `$${t}`, vv],
|
|
978
1269
|
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
@@ -982,10 +1273,8 @@ export const emitter = {
|
|
|
982
1273
|
if (Array.isArray(name) && name[0] === '.') {
|
|
983
1274
|
const [, obj, prop] = name
|
|
984
1275
|
// Schema-based object → f64.store at fixed offset.
|
|
985
|
-
// safe=true: skip structural subtyping when variable's type is unknown,
|
|
986
|
-
// otherwise a slot write could clobber an array/string's payload.
|
|
987
1276
|
if (typeof obj === 'string' && ctx.schema.find) {
|
|
988
|
-
const idx = ctx.schema.find(obj, prop
|
|
1277
|
+
const idx = ctx.schema.find(obj, prop)
|
|
989
1278
|
if (idx >= 0) {
|
|
990
1279
|
const va = emit(obj), vv = asF64(emit(val)), t = temp()
|
|
991
1280
|
const shadow = needsDynShadow(obj)
|
|
@@ -995,39 +1284,60 @@ export const emitter = {
|
|
|
995
1284
|
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]], ['local.get', `$${t}`]],
|
|
996
1285
|
]
|
|
997
1286
|
if (shadow)
|
|
998
|
-
stmts.push(['drop', ['call', '$__dyn_set',
|
|
1287
|
+
stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
|
|
999
1288
|
stmts.push(['local.get', `$${t}`])
|
|
1000
1289
|
return typed(['block', ['result', 'f64'], ...stmts], 'f64')
|
|
1001
1290
|
}
|
|
1002
1291
|
}
|
|
1003
1292
|
if (typeof obj === 'string') {
|
|
1004
1293
|
const objType = keyValType(obj)
|
|
1005
|
-
|
|
1294
|
+
// OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
|
|
1295
|
+
// properties go through __dyn_set, which writes to the per-OBJECT
|
|
1296
|
+
// propsPtr at off-16 — same path as object-literal dyn shadow writes
|
|
1297
|
+
// (module/object.js). __hash_set assumes HASH bucket layout and would
|
|
1298
|
+
// corrupt OBJECT memory.
|
|
1299
|
+
if (usesDynProps(objType) || objType === VAL.OBJECT) {
|
|
1006
1300
|
inc('__dyn_set')
|
|
1007
|
-
return typed(['call', '$__dyn_set',
|
|
1301
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1302
|
+
}
|
|
1303
|
+
if (ctx.func.names.has(obj) && !ctx.func.locals?.has(obj) && !ctx.func.current?.params?.some(p => p.name === obj)) {
|
|
1304
|
+
inc('__dyn_set')
|
|
1305
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1306
|
+
}
|
|
1307
|
+
if (objType == null && ctx.transform.host !== 'wasi') {
|
|
1308
|
+
ctx.features.external = true
|
|
1008
1309
|
}
|
|
1009
|
-
if (objType == null) ctx.features.external = true
|
|
1010
1310
|
inc('__hash_set')
|
|
1011
|
-
const setCall = typed(['call', '$__hash_set',
|
|
1311
|
+
const setCall = typed(['f64.reinterpret_i64', ['call', '$__hash_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1012
1312
|
if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
|
|
1013
1313
|
['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
|
|
1314
|
+
// Closure-captured (boxed) locals store the value at the cell address — local.tee
|
|
1315
|
+
// would write to the i32 cell pointer, not the f64 value. Route through writeVar.
|
|
1316
|
+
if (ctx.func.boxed?.has(obj)) return writeVar(obj, setCall, false)
|
|
1014
1317
|
return typed(['local.tee', `$${obj}`, setCall], 'f64')
|
|
1015
1318
|
}
|
|
1016
|
-
ctx.features.external = true
|
|
1319
|
+
if (ctx.transform.host !== 'wasi') ctx.features.external = true
|
|
1017
1320
|
inc('__dyn_set')
|
|
1018
|
-
return typed(['call', '$__dyn_set',
|
|
1321
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1019
1322
|
}
|
|
1020
1323
|
if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
|
|
1021
|
-
|
|
1324
|
+
if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
|
|
1325
|
+
inc('__to_num')
|
|
1326
|
+
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
1327
|
+
}
|
|
1022
1328
|
return writeVar(name, emit(val), void_)
|
|
1023
1329
|
},
|
|
1024
1330
|
|
|
1025
1331
|
// Compound assignments: read-modify-write with type coercion
|
|
1026
1332
|
'+=': (name, val) => {
|
|
1027
|
-
// String concatenation: desugar to name = name + val (+ handler knows about strings)
|
|
1333
|
+
// String concatenation: desugar to name = name + val (+ handler knows about strings).
|
|
1334
|
+
// Also desugar when either side has unknown type — the `+` operator picks runtime
|
|
1335
|
+
// string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
|
|
1336
|
+
// silently corrupt string concatenations through unknown-typed values.
|
|
1028
1337
|
const vt = typeof name === 'string' ? keyValType(name) : null
|
|
1029
1338
|
const vtB = keyValType(val)
|
|
1030
1339
|
if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
|
|
1340
|
+
if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
|
|
1031
1341
|
return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
|
|
1032
1342
|
},
|
|
1033
1343
|
...Object.fromEntries([
|
|
@@ -1068,11 +1378,8 @@ export const emitter = {
|
|
|
1068
1378
|
const t = temp()
|
|
1069
1379
|
const va = readVar(name)
|
|
1070
1380
|
// Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
: ['i32.and',
|
|
1074
|
-
['f64.eq', ['local.tee', `$${t}`, asF64(va)], ['local.get', `$${t}`]],
|
|
1075
|
-
['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]]]
|
|
1381
|
+
const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1382
|
+
const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
|
|
1076
1383
|
// &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
|
|
1077
1384
|
const [thenExpr, elseExpr] = op === '||='
|
|
1078
1385
|
? [['local.get', `$${t}`], asF64(emit(val))]
|
|
@@ -1109,12 +1416,25 @@ export const emitter = {
|
|
|
1109
1416
|
const vtA = keyValType(a)
|
|
1110
1417
|
const vtB = keyValType(b)
|
|
1111
1418
|
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
1419
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
1420
|
+
// generic concat dispatch when rhs is a string-index. The byte flows
|
|
1421
|
+
// straight from __char_at into memory, and the bump-extend path elides
|
|
1422
|
+
// the alloc+copy when lhs is the heap-top STRING.
|
|
1423
|
+
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
1424
|
+
if (keyValType(b[1]) === VAL.STRING) {
|
|
1425
|
+
inc('__str_append_byte', '__char_at')
|
|
1426
|
+
return typed(['call', '$__str_append_byte',
|
|
1427
|
+
asI64(emit(a)),
|
|
1428
|
+
['call', '$__char_at', asI64(emit(b[1])), asI32(emit(b[2]))],
|
|
1429
|
+
], 'f64')
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1112
1432
|
inc('__str_concat_raw')
|
|
1113
|
-
return typed(['call', '$__str_concat_raw',
|
|
1433
|
+
return typed(['call', '$__str_concat_raw', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1114
1434
|
}
|
|
1115
1435
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
1116
1436
|
inc('__str_concat')
|
|
1117
|
-
return typed(['call', '$__str_concat',
|
|
1437
|
+
return typed(['call', '$__str_concat', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1118
1438
|
}
|
|
1119
1439
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
1120
1440
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -1125,9 +1445,9 @@ export const emitter = {
|
|
|
1125
1445
|
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
1126
1446
|
const tA = temp('add'), tB = temp('add')
|
|
1127
1447
|
inc('__str_concat', '__is_str_key')
|
|
1128
|
-
const checkA = vtA == null ? ['call', '$__is_str_key', ['local.tee', `$${tA}`, asF64(emit(a))]] : null
|
|
1129
|
-
const checkB = vtB == null ? ['call', '$__is_str_key', ['local.tee', `$${tB}`, asF64(emit(b))]] : null
|
|
1130
|
-
const concat = ['call', '$__str_concat', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
1448
|
+
const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
|
|
1449
|
+
const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
|
|
1450
|
+
const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
|
|
1131
1451
|
const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
1132
1452
|
if (checkA && checkB) {
|
|
1133
1453
|
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
@@ -1139,8 +1459,8 @@ export const emitter = {
|
|
|
1139
1459
|
['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
|
|
1140
1460
|
], 'f64')
|
|
1141
1461
|
}
|
|
1142
|
-
const va = emit(a), vb = emit(b)
|
|
1143
|
-
if (
|
|
1462
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
|
|
1463
|
+
if (_f) return _f
|
|
1144
1464
|
if (isLit(vb) && litVal(vb) === 0) return va
|
|
1145
1465
|
if (isLit(va) && litVal(va) === 0) return vb
|
|
1146
1466
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.add', va, vb], 'i32')
|
|
@@ -1152,9 +1472,9 @@ export const emitter = {
|
|
|
1152
1472
|
return b === undefined
|
|
1153
1473
|
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
1154
1474
|
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
1155
|
-
if (b === undefined)
|
|
1156
|
-
const va = emit(a), vb = emit(b)
|
|
1157
|
-
if (
|
|
1475
|
+
if (b === undefined) return emitNeg(a)
|
|
1476
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
1477
|
+
if (_f) return _f
|
|
1158
1478
|
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1159
1479
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.sub', va, vb], 'i32')
|
|
1160
1480
|
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
@@ -1162,18 +1482,18 @@ export const emitter = {
|
|
|
1162
1482
|
'u+': a => {
|
|
1163
1483
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
1164
1484
|
return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
|
|
1485
|
+
const v = emit(a)
|
|
1486
|
+
if (v.type === 'i32') return asF64(v)
|
|
1487
|
+
if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
|
|
1165
1488
|
inc('__to_num')
|
|
1166
|
-
return typed(['call', '$__to_num',
|
|
1167
|
-
},
|
|
1168
|
-
'u-': a => {
|
|
1169
|
-
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
1170
|
-
const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64')
|
|
1489
|
+
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
1171
1490
|
},
|
|
1491
|
+
'u-': a => emitNeg(a),
|
|
1172
1492
|
'*': (a, b) => {
|
|
1173
1493
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1174
1494
|
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
1175
|
-
const va = emit(a), vb = emit(b)
|
|
1176
|
-
if (
|
|
1495
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
|
|
1496
|
+
if (_f) return _f
|
|
1177
1497
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1178
1498
|
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
1179
1499
|
if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
@@ -1184,23 +1504,24 @@ export const emitter = {
|
|
|
1184
1504
|
'/': (a, b) => {
|
|
1185
1505
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1186
1506
|
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
1187
|
-
const va = emit(a), vb = emit(b)
|
|
1188
|
-
if (
|
|
1507
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
1508
|
+
if (_f) return _f
|
|
1189
1509
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1190
1510
|
return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1191
1511
|
},
|
|
1192
1512
|
'%': (a, b) => {
|
|
1193
1513
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1194
1514
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
1195
|
-
const va = emit(a), vb = emit(b)
|
|
1196
|
-
if (
|
|
1515
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
1516
|
+
if (_f) return _f
|
|
1197
1517
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.rem_s', va, vb], 'i32')
|
|
1198
1518
|
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
1199
1519
|
},
|
|
1200
|
-
|
|
1201
1520
|
// === Comparisons (always i32 result) ===
|
|
1202
1521
|
|
|
1203
1522
|
'==': (a, b) => {
|
|
1523
|
+
const charCmp = emitSingleCharIndexCmp(a, b)
|
|
1524
|
+
if (charCmp) return charCmp
|
|
1204
1525
|
// JS loose nullish equality: x == null / x == undefined.
|
|
1205
1526
|
// If the non-literal side has a known non-null VAL type, fold to 0.
|
|
1206
1527
|
if (isNullishLit(a)) {
|
|
@@ -1215,11 +1536,13 @@ export const emitter = {
|
|
|
1215
1536
|
const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
|
|
1216
1537
|
const va = emit(a), vb = emit(b)
|
|
1217
1538
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1539
|
+
// Either side known-pure NUMBER (literal or typed) → f64.eq is correct regardless
|
|
1540
|
+
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1541
|
+
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
1542
|
+
// Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
|
|
1543
|
+
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1544
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1545
|
+
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
1223
1546
|
// Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
|
|
1224
1547
|
// JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
|
|
1225
1548
|
// STRING needs __eq (heap strings can be equal by content but different pointers).
|
|
@@ -1228,9 +1551,11 @@ export const emitter = {
|
|
|
1228
1551
|
return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1229
1552
|
}
|
|
1230
1553
|
inc('__eq')
|
|
1231
|
-
return typed(['call', '$__eq',
|
|
1554
|
+
return typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
1232
1555
|
},
|
|
1233
1556
|
'!=': (a, b) => {
|
|
1557
|
+
const charCmp = emitSingleCharIndexCmp(a, b, true)
|
|
1558
|
+
if (charCmp) return charCmp
|
|
1234
1559
|
if (isNullishLit(a)) {
|
|
1235
1560
|
if (valTypeOf(b)) return emitNum(1)
|
|
1236
1561
|
return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
|
|
@@ -1242,14 +1567,14 @@ export const emitter = {
|
|
|
1242
1567
|
const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
|
|
1243
1568
|
const va = emit(a), vb = emit(b)
|
|
1244
1569
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
|
|
1245
|
-
const vta =
|
|
1246
|
-
const vtb =
|
|
1247
|
-
if (vta === VAL.NUMBER
|
|
1570
|
+
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1571
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1572
|
+
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
1248
1573
|
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1249
1574
|
return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1250
1575
|
}
|
|
1251
1576
|
inc('__eq')
|
|
1252
|
-
return typed(['i32.eqz', ['call', '$__eq',
|
|
1577
|
+
return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
|
|
1253
1578
|
},
|
|
1254
1579
|
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
1255
1580
|
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
@@ -1264,13 +1589,13 @@ export const emitter = {
|
|
|
1264
1589
|
// Unboxed pointer offsets: falsy iff zero offset.
|
|
1265
1590
|
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
1266
1591
|
// Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
|
|
1267
|
-
//
|
|
1268
|
-
const vt =
|
|
1269
|
-
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT) {
|
|
1592
|
+
// Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
|
|
1593
|
+
const vt = resolveValType(a, valTypeOf, lookupValType)
|
|
1594
|
+
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
|
|
1270
1595
|
return isNullish(asF64(v))
|
|
1271
1596
|
}
|
|
1272
1597
|
inc('__is_truthy')
|
|
1273
|
-
return typed(['i32.eqz', ['call', '$__is_truthy',
|
|
1598
|
+
return typed(['i32.eqz', ['call', '$__is_truthy', asI64(v)]], 'i32')
|
|
1274
1599
|
},
|
|
1275
1600
|
|
|
1276
1601
|
'?:': (a, b, c) => {
|
|
@@ -1311,7 +1636,21 @@ export const emitter = {
|
|
|
1311
1636
|
}
|
|
1312
1637
|
}
|
|
1313
1638
|
const fb = asF64(vb), fc = asF64(vc)
|
|
1314
|
-
|
|
1639
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1640
|
+
const vtc = resolveValType(c, valTypeOf, lookupValType)
|
|
1641
|
+
const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
|
|
1642
|
+
const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
|
|
1643
|
+
|| vb.closureFuncIdx != null || vc.closureFuncIdx != null
|
|
1644
|
+
|| isNaNBoxLit(fb) || isNaNBoxLit(fc)
|
|
1645
|
+
if (refPayload) {
|
|
1646
|
+
const ib = ['i64.reinterpret_f64', fb]
|
|
1647
|
+
const ic = ['i64.reinterpret_f64', fc]
|
|
1648
|
+
const bits = isPureIR(fb) && isPureIR(fc)
|
|
1649
|
+
? ['select', ib, ic, cond]
|
|
1650
|
+
: ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
|
|
1651
|
+
return typed(['f64.reinterpret_i64', bits], 'f64')
|
|
1652
|
+
}
|
|
1653
|
+
if (!refPayload && isPureIR(fb) && isPureIR(fc))
|
|
1315
1654
|
return typed(['select', fb, fc, cond], 'f64')
|
|
1316
1655
|
return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
|
|
1317
1656
|
},
|
|
@@ -1397,7 +1736,11 @@ export const emitter = {
|
|
|
1397
1736
|
|
|
1398
1737
|
// === Bitwise (i32 for numbers, i64 for BigInt) ===
|
|
1399
1738
|
|
|
1400
|
-
|
|
1739
|
+
// Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
|
|
1740
|
+
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
1741
|
+
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
1742
|
+
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
1743
|
+
'~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v.type === 'i32' ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32') },
|
|
1401
1744
|
...Object.fromEntries([
|
|
1402
1745
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
1403
1746
|
].map(([op, fn]) => [op, (a, b) => {
|
|
@@ -1410,16 +1753,20 @@ export const emitter = {
|
|
|
1410
1753
|
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
1411
1754
|
if (op === '>>') return emitNum(la >> lb)
|
|
1412
1755
|
}
|
|
1413
|
-
|
|
1756
|
+
const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
|
|
1757
|
+
const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1758
|
+
return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
|
|
1414
1759
|
}])),
|
|
1415
1760
|
'>>>': (a, b) => {
|
|
1416
|
-
const va = emit(a), vb = emit(b)
|
|
1417
|
-
if (
|
|
1761
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a >>> b)
|
|
1762
|
+
if (_f) return _f
|
|
1418
1763
|
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
1419
1764
|
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
1420
1765
|
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
1421
1766
|
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
1422
|
-
const
|
|
1767
|
+
const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
|
|
1768
|
+
const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1769
|
+
const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
|
|
1423
1770
|
node.unsigned = true
|
|
1424
1771
|
return node
|
|
1425
1772
|
},
|
|
@@ -1547,6 +1894,8 @@ export const emitter = {
|
|
|
1547
1894
|
let argList = Array.isArray(callArgs)
|
|
1548
1895
|
? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
|
|
1549
1896
|
: callArgs ? [callArgs] : []
|
|
1897
|
+
// Trailing comma in call: parser emits a trailing null sentinel — drop it
|
|
1898
|
+
while (argList.length && argList[argList.length - 1] == null) argList.pop()
|
|
1550
1899
|
|
|
1551
1900
|
// Helper: expand spread arguments into flat list of normal arguments + spread markers
|
|
1552
1901
|
// Returns { normal: [...], spreads: [(pos, expr), ...] }
|
|
@@ -1566,6 +1915,26 @@ export const emitter = {
|
|
|
1566
1915
|
|
|
1567
1916
|
const parsed = parseArgs(argList)
|
|
1568
1917
|
|
|
1918
|
+
// Optional method call: obj?.method(args) — null if obj is nullish, else
|
|
1919
|
+
// obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
|
|
1920
|
+
// distinct from the regular method call's ['.', obj, method] callee. Receiver
|
|
1921
|
+
// hoists into a temp so the nullish check and the method dispatch below see
|
|
1922
|
+
// the same evaluation; recursion with a synthetic '.'-callee reuses the type-
|
|
1923
|
+
// aware method-dispatch in this same handler rather than duplicating it.
|
|
1924
|
+
if (Array.isArray(callee) && callee[0] === '?.') {
|
|
1925
|
+
const [, obj, method] = callee
|
|
1926
|
+
const t = `${T}om${ctx.func.uniq++}`
|
|
1927
|
+
ctx.func.locals.set(t, 'f64')
|
|
1928
|
+
const va = asF64(emit(obj))
|
|
1929
|
+
const methodCall = emitter['()'](['.', t, method], callArgs)
|
|
1930
|
+
return typed(['block', ['result', 'f64'],
|
|
1931
|
+
['local.set', `$${t}`, va],
|
|
1932
|
+
['if', ['result', 'f64'],
|
|
1933
|
+
['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
|
|
1934
|
+
['then', asF64(methodCall)],
|
|
1935
|
+
['else', nullExpr()]]], 'f64')
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1569
1938
|
// Method call: obj.method(args) → type-aware dispatch
|
|
1570
1939
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1571
1940
|
const [, obj, method] = callee
|
|
@@ -1577,7 +1946,7 @@ export const emitter = {
|
|
|
1577
1946
|
const func = ctx.func.map.get(fname)
|
|
1578
1947
|
const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1579
1948
|
while (emittedArgs.length < func.sig.params.length)
|
|
1580
|
-
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
1949
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
1581
1950
|
const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
|
|
1582
1951
|
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1583
1952
|
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
@@ -1585,7 +1954,10 @@ export const emitter = {
|
|
|
1585
1954
|
}
|
|
1586
1955
|
}
|
|
1587
1956
|
|
|
1588
|
-
|
|
1957
|
+
let vt = keyValType(obj)
|
|
1958
|
+
if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
|
|
1959
|
+
&& (method === 'slice' || method === 'concat')
|
|
1960
|
+
&& vt !== VAL.STRING && vt !== VAL.ARRAY) vt = null
|
|
1589
1961
|
|
|
1590
1962
|
// Helper to call method with arguments (handles spread expansion)
|
|
1591
1963
|
const callMethod = (objArg, methodEmitter) => {
|
|
@@ -1624,23 +1996,23 @@ export const emitter = {
|
|
|
1624
1996
|
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1625
1997
|
ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1626
1998
|
if (srcIsArr) {
|
|
1627
|
-
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${sa}`]]])
|
|
1999
|
+
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1628
2000
|
ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
|
|
1629
2001
|
} else {
|
|
1630
|
-
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['local.get', `$${sa}`]]])
|
|
2002
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1631
2003
|
}
|
|
1632
2004
|
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1633
2005
|
if (objIsArr) {
|
|
1634
2006
|
ir.push(['local.set', `$${ol}`,
|
|
1635
|
-
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${o}`]], ['i32.const', 8]]]])
|
|
2007
|
+
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
|
|
1636
2008
|
} else {
|
|
1637
|
-
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['local.get', `$${o}`]]])
|
|
2009
|
+
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1638
2010
|
}
|
|
1639
2011
|
// Single grow for the full spread (vs per-element grow check in the generic loop).
|
|
1640
|
-
ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['local.get', `$${o}`],
|
|
2012
|
+
ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1641
2013
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1642
2014
|
// base captured AFTER grow (grow may relocate the array).
|
|
1643
|
-
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${o}`]]])
|
|
2015
|
+
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1644
2016
|
// Tight store loop.
|
|
1645
2017
|
ir.push(['local.set', `$${si}`, ['i32.const', 0]])
|
|
1646
2018
|
const loopId = ctx.func.uniq++
|
|
@@ -1656,7 +2028,7 @@ export const emitter = {
|
|
|
1656
2028
|
['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
|
|
1657
2029
|
['br', `$continue${loopId}`]]])
|
|
1658
2030
|
// Single set_len for the full spread.
|
|
1659
|
-
ir.push(['call', '$__set_len', ['local.get', `$${o}`],
|
|
2031
|
+
ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1660
2032
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1661
2033
|
// Update source variable: grow may have moved the pointer.
|
|
1662
2034
|
if (ctx.func.boxed?.has(objArg)) {
|
|
@@ -1694,7 +2066,7 @@ export const emitter = {
|
|
|
1694
2066
|
inc('__len')
|
|
1695
2067
|
const n = multiCount(spreadExpr)
|
|
1696
2068
|
ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1697
|
-
ir.push(['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${arr}`]]])
|
|
2069
|
+
ir.push(['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]])
|
|
1698
2070
|
ir.push(['local.set', `$${idx}`,
|
|
1699
2071
|
reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
|
|
1700
2072
|
const loopId = ctx.func.uniq++
|
|
@@ -1745,7 +2117,7 @@ export const emitter = {
|
|
|
1745
2117
|
const n = multiCount(spreadExpr)
|
|
1746
2118
|
irG.push(
|
|
1747
2119
|
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1748
|
-
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
2120
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
|
|
1749
2121
|
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1750
2122
|
const loopId = ctx.func.uniq++
|
|
1751
2123
|
const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
|
|
@@ -1784,7 +2156,7 @@ export const emitter = {
|
|
|
1784
2156
|
const n = multiCount(spreadExpr)
|
|
1785
2157
|
irG.push(
|
|
1786
2158
|
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1787
|
-
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
2159
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
|
|
1788
2160
|
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1789
2161
|
const loopId = ctx.func.uniq++
|
|
1790
2162
|
const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
|
|
@@ -1845,15 +2217,26 @@ export const emitter = {
|
|
|
1845
2217
|
const genEmitter = ctx.core.emit[genKey]
|
|
1846
2218
|
return typed(['block', ['result', 'f64'],
|
|
1847
2219
|
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1848
|
-
['local.set', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]],
|
|
2220
|
+
['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1849
2221
|
['if', ['result', 'f64'],
|
|
1850
|
-
['i32.
|
|
1851
|
-
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
|
|
1852
|
-
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
|
|
2222
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
|
|
1853
2223
|
['then', callMethod(t, strEmitter)],
|
|
1854
2224
|
['else', callMethod(t, genEmitter)]]], 'f64')
|
|
1855
2225
|
}
|
|
1856
2226
|
|
|
2227
|
+
// Schema property function call: x.prop(args) where prop is a closure in boxed schema
|
|
2228
|
+
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
|
|
2229
|
+
const idx = ctx.schema.find(obj, method)
|
|
2230
|
+
if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
|
|
2231
|
+
const propRead = typed(['f64.load', ['i32.add', ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
|
|
2232
|
+
if (parsed.hasSpread) {
|
|
2233
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2234
|
+
return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
|
|
2235
|
+
}
|
|
2236
|
+
return ctx.closure.call(propRead, parsed.normal)
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
1857
2240
|
// Schema property function call: x.prop(args) where prop is a closure in boxed schema
|
|
1858
2241
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
|
|
1859
2242
|
const idx = ctx.schema.find(obj, method)
|
|
@@ -1876,7 +2259,7 @@ export const emitter = {
|
|
|
1876
2259
|
ctx.func.locals.set(objTmp, 'f64')
|
|
1877
2260
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1878
2261
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
1879
|
-
const propRead = typed(['call', '$__dyn_get_expr', ['local.get', `$${objTmp}`],
|
|
2262
|
+
const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
|
|
1880
2263
|
const propTmp = `${T}mprop${ctx.func.uniq++}`
|
|
1881
2264
|
ctx.func.locals.set(propTmp, 'f64')
|
|
1882
2265
|
if (usesDynProps(vt)) {
|
|
@@ -1885,7 +2268,18 @@ export const emitter = {
|
|
|
1885
2268
|
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1886
2269
|
['local.set', `$${propTmp}`, propRead],
|
|
1887
2270
|
['if', ['result', 'f64'],
|
|
1888
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
|
|
2271
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
2272
|
+
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
2273
|
+
['else', undefExpr()]]], 'f64')
|
|
2274
|
+
}
|
|
2275
|
+
// WASI: no PTR.EXTERNAL values; closure-only dispatch is correct.
|
|
2276
|
+
if (ctx.transform.host === 'wasi') {
|
|
2277
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2278
|
+
return typed(['block', ['result', 'f64'],
|
|
2279
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
2280
|
+
['local.set', `$${propTmp}`, propRead],
|
|
2281
|
+
['if', ['result', 'f64'],
|
|
2282
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
1889
2283
|
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
1890
2284
|
['else', undefExpr()]]], 'f64')
|
|
1891
2285
|
}
|
|
@@ -1895,22 +2289,29 @@ export const emitter = {
|
|
|
1895
2289
|
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1896
2290
|
['local.set', `$${propTmp}`, propRead],
|
|
1897
2291
|
['if', ['result', 'f64'],
|
|
1898
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
|
|
2292
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
1899
2293
|
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
1900
2294
|
['else', ['if', ['result', 'f64'],
|
|
1901
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${objTmp}`]], ['i32.const', PTR.EXTERNAL]],
|
|
1902
|
-
['then', ['
|
|
2295
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.EXTERNAL]],
|
|
2296
|
+
['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2297
|
+
['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
|
|
2298
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2299
|
+
['i64.reinterpret_f64', arrayIR]]]],
|
|
1903
2300
|
['else', undefExpr()]]]]], 'f64')
|
|
1904
2301
|
}
|
|
1905
2302
|
|
|
1906
2303
|
// Unknown callee - assume external method
|
|
1907
2304
|
if (ctx.transform.strict)
|
|
1908
2305
|
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 }.`)
|
|
2306
|
+
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
1909
2307
|
inc('__ext_call')
|
|
1910
2308
|
ctx.features.external = true
|
|
1911
2309
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1912
2310
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
1913
|
-
return typed(['
|
|
2311
|
+
return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2312
|
+
['i64.reinterpret_f64', asF64(emit(obj))],
|
|
2313
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2314
|
+
['i64.reinterpret_f64', arrayIR]]], 'f64');
|
|
1914
2315
|
}
|
|
1915
2316
|
|
|
1916
2317
|
if (ctx.core.emit[callee]) {
|
|
@@ -1929,17 +2330,17 @@ export const emitter = {
|
|
|
1929
2330
|
}
|
|
1930
2331
|
|
|
1931
2332
|
// Direct call if callee is a known top-level function
|
|
1932
|
-
if (typeof callee === 'string' && ctx.func.names.has(callee)) {
|
|
2333
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee) && !ctx.func.locals?.has(callee)) {
|
|
1933
2334
|
const func = ctx.func.map.get(callee)
|
|
1934
2335
|
|
|
1935
2336
|
// Rest param case: collect all args (including expanded spreads) into array
|
|
1936
2337
|
if (func?.rest) {
|
|
1937
2338
|
const fixedParamCount = func.sig.params.length - 1
|
|
1938
2339
|
const fixedArgs = parsed.normal.slice(0, fixedParamCount)
|
|
1939
|
-
// Pad missing fixed args with
|
|
2340
|
+
// Pad missing fixed args with `undefined` so default-param init triggers per spec.
|
|
1940
2341
|
const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1941
2342
|
while (emittedFixed.length < fixedParamCount)
|
|
1942
|
-
emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
2343
|
+
emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
1943
2344
|
|
|
1944
2345
|
// Reconstruct with spreads, then take rest args
|
|
1945
2346
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
@@ -1957,10 +2358,16 @@ export const emitter = {
|
|
|
1957
2358
|
|
|
1958
2359
|
// Regular function call without rest params
|
|
1959
2360
|
if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
|
|
1960
|
-
// Pad missing args with
|
|
2361
|
+
// Pad missing args with `undefined` so default-param init triggers per spec
|
|
2362
|
+
// (only undefined, not null, should trigger defaults). Drop extras to match
|
|
2363
|
+
// JS calling convention — emitting them anyway produces an invalid call
|
|
2364
|
+
// when the callee is a fixed-arity import (e.g. `_interp`-registered host
|
|
2365
|
+
// stubs) since wasm validates arg count. Use ?? rather than || so a
|
|
2366
|
+
// legitimate 0-arity callee isn't bypassed.
|
|
1961
2367
|
const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
|
|
1962
|
-
const expected = func?.sig.params.length
|
|
1963
|
-
while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
2368
|
+
const expected = func?.sig.params.length ?? args.length
|
|
2369
|
+
while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
2370
|
+
if (args.length > expected) args.length = expected
|
|
1964
2371
|
// Multi-value return: materialize as heap array (caller expects single pointer)
|
|
1965
2372
|
if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
|
|
1966
2373
|
const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
|
|
@@ -2001,13 +2408,80 @@ export const emitter = {
|
|
|
2001
2408
|
return ctx.closure.call(emit(callee), parsed.normal)
|
|
2002
2409
|
}
|
|
2003
2410
|
|
|
2004
|
-
// Unknown callee — assume direct call
|
|
2005
|
-
|
|
2411
|
+
// Unknown callee — assume direct call. Match arg count to the declared
|
|
2412
|
+
// signature when the callee is a registered env import (e.g. `_interp`-
|
|
2413
|
+
// wired host stubs). JS calling convention drops extras and pads missing;
|
|
2414
|
+
// wasm requires exact arity, so emitting raw argList against a fixed-arity
|
|
2415
|
+
// import would invalidate the module.
|
|
2416
|
+
let calleeArity = null
|
|
2417
|
+
if (typeof callee === 'string') {
|
|
2418
|
+
const imp = ctx.module.imports?.find(i =>
|
|
2419
|
+
Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
|
|
2420
|
+
if (imp) {
|
|
2421
|
+
let n = 0
|
|
2422
|
+
for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
|
|
2423
|
+
calleeArity = n
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const emittedArgs = argList.map(a => asF64(emit(a)))
|
|
2427
|
+
if (calleeArity != null) {
|
|
2428
|
+
while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
|
|
2429
|
+
if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
|
|
2430
|
+
}
|
|
2431
|
+
return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
|
|
2006
2432
|
},
|
|
2007
2433
|
}
|
|
2008
2434
|
|
|
2009
2435
|
// === Emit dispatch ===
|
|
2010
2436
|
|
|
2437
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
|
|
2438
|
+
// Per ECMAScript, an optional access short-circuits the entire continuation, not just
|
|
2439
|
+
// its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
|
|
2440
|
+
// nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
|
|
2441
|
+
//
|
|
2442
|
+
// At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
|
|
2443
|
+
// the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
|
|
2444
|
+
// with that optional replaced by a regular access. The single guard short-circuits the
|
|
2445
|
+
// whole continuation. Nested optionals further inside the chain are left intact and
|
|
2446
|
+
// handle their own short-circuiting on recursion.
|
|
2447
|
+
function liftOptionalChain(node) {
|
|
2448
|
+
const path = []
|
|
2449
|
+
let cur = node
|
|
2450
|
+
while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
|
|
2451
|
+
cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
|
|
2452
|
+
path.push(cur)
|
|
2453
|
+
cur = cur[1]
|
|
2454
|
+
}
|
|
2455
|
+
// Find the deepest optional with continuation outside it. optIdx === 0 means the
|
|
2456
|
+
// chain root itself is optional with no continuation — handled by the regular
|
|
2457
|
+
// `?.` / `?.[]` / `?.()` emitters.
|
|
2458
|
+
let optIdx = -1
|
|
2459
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
2460
|
+
if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
|
|
2461
|
+
optIdx = i
|
|
2462
|
+
break
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
if (optIdx <= 0) return null
|
|
2466
|
+
const opt = path[optIdx]
|
|
2467
|
+
const headExpr = opt[1]
|
|
2468
|
+
const t = temp('oc')
|
|
2469
|
+
let rebuilt
|
|
2470
|
+
if (opt[0] === '?.') rebuilt = ['.', t, opt[2]]
|
|
2471
|
+
else if (opt[0] === '?.[]') rebuilt = ['[]', t, opt[2]]
|
|
2472
|
+
else rebuilt = ['()', t, ...opt.slice(2)]
|
|
2473
|
+
for (let i = optIdx - 1; i >= 0; i--) {
|
|
2474
|
+
const outer = path[i]
|
|
2475
|
+
rebuilt = [outer[0], rebuilt, ...outer.slice(2)]
|
|
2476
|
+
}
|
|
2477
|
+
return typed(['block', ['result', 'f64'],
|
|
2478
|
+
['local.set', `$${t}`, asF64(emit(headExpr))],
|
|
2479
|
+
['if', ['result', 'f64'],
|
|
2480
|
+
['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
|
|
2481
|
+
['then', asF64(emit(rebuilt))],
|
|
2482
|
+
['else', undefExpr()]]], 'f64')
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2011
2485
|
/**
|
|
2012
2486
|
* Emit single AST node to typed WASM IR.
|
|
2013
2487
|
* Every returned node has .type = 'i32' | 'f64'.
|
|
@@ -2028,14 +2502,10 @@ export function emit(node, expect) {
|
|
|
2028
2502
|
const n = node & 0xFFFFFFFFFFFFFFFFn
|
|
2029
2503
|
return typed(['f64.reinterpret_i64', ['i64.const', '0x' + n.toString(16)]], 'f64')
|
|
2030
2504
|
}
|
|
2031
|
-
if (typeof node === 'number')
|
|
2032
|
-
if (Number.isInteger(node) && node >= -2147483648 && node <= 2147483647)
|
|
2033
|
-
return typed(['i32.const', node], 'i32')
|
|
2034
|
-
return typed(['f64.const', node], 'f64')
|
|
2035
|
-
}
|
|
2505
|
+
if (typeof node === 'number') return emitNum(node)
|
|
2036
2506
|
if (typeof node === 'string') {
|
|
2037
2507
|
// Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
|
|
2038
|
-
if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node))
|
|
2508
|
+
if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node) || repOf(node)?.intConst != null)
|
|
2039
2509
|
return readVar(node)
|
|
2040
2510
|
// Top-level function used as value → wrap as closure pointer for call_indirect
|
|
2041
2511
|
if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
|
|
@@ -2072,17 +2542,28 @@ export function emit(node, expect) {
|
|
|
2072
2542
|
}
|
|
2073
2543
|
let idx = ctx.closure.table.indexOf(trampolineName)
|
|
2074
2544
|
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
|
|
2075
|
-
|
|
2545
|
+
const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
2546
|
+
ir.closureFuncIdx = idx
|
|
2547
|
+
return ir
|
|
2076
2548
|
}
|
|
2077
2549
|
// Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
|
|
2078
|
-
if (node.includes('.') && ctx.core.emit[node])
|
|
2550
|
+
if (node.includes('.') && ctx.core.emit[node]) {
|
|
2551
|
+
const handler = ctx.core.emit[node]
|
|
2552
|
+
return handler.length > 0 ? builtinFunctionValue(node) : handler()
|
|
2553
|
+
}
|
|
2079
2554
|
// Auto-import known host globals (WebAssembly, globalThis, etc.)
|
|
2080
2555
|
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
2081
2556
|
if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
|
|
2557
|
+
if (ctx.transform.host === 'wasi') err(`host:'wasi': reference to host global \`${node}\` requires an env import. Remove the reference or use host:'js'.`)
|
|
2082
2558
|
ctx.features.external = true
|
|
2083
2559
|
ctx.scope.globals.set(node, null)
|
|
2084
|
-
|
|
2085
|
-
|
|
2560
|
+
// Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed
|
|
2561
|
+
// external-ref payload across the wasm↔JS global boundary (same hazard
|
|
2562
|
+
// as env.print/setTimeout — see module/console.js header). asF64()
|
|
2563
|
+
// reinterprets to f64 at every read site.
|
|
2564
|
+
ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, 'i64']])
|
|
2565
|
+
ctx.scope.globalTypes.set(node, 'i64')
|
|
2566
|
+
return typed(['global.get', `$${node}`], 'i64')
|
|
2086
2567
|
}
|
|
2087
2568
|
const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
|
|
2088
2569
|
return typed(['local.get', `$${node}`], t)
|
|
@@ -2099,6 +2580,14 @@ export function emit(node, expect) {
|
|
|
2099
2580
|
return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
|
|
2100
2581
|
}
|
|
2101
2582
|
|
|
2583
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
|
|
2584
|
+
// Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
|
|
2585
|
+
// with the optional already replaced by a non-optional access on a guarded temp.
|
|
2586
|
+
if (op === '.' || op === '[]' || op === '()') {
|
|
2587
|
+
const lifted = liftOptionalChain(node)
|
|
2588
|
+
if (lifted) return lifted
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2102
2591
|
const handler = ctx.core.emit[op]
|
|
2103
2592
|
if (!handler) err(`Unknown op: ${op}`)
|
|
2104
2593
|
return handler(...args)
|