jz 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -90
- package/cli.js +34 -8
- package/index.js +100 -14
- 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 +783 -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 +439 -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 +627 -133
- package/src/autoload.js +15 -7
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +658 -145
- package/src/host.js +244 -51
- package/src/ir.js +87 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1167 -4
- package/src/prepare.js +322 -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,34 @@ 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
|
+
}
|
|
870
|
+
if (vta === VAL.DATE || vtb === VAL.DATE) {
|
|
871
|
+
const dateNum = (node, v, vt) => {
|
|
872
|
+
if (vt !== VAL.DATE) return toNumF64(node, v)
|
|
873
|
+
const ptr = v.ptrKind === VAL.DATE
|
|
874
|
+
? v
|
|
875
|
+
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
876
|
+
return typed(['f64.load', ptr], 'f64')
|
|
877
|
+
}
|
|
878
|
+
return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
|
|
879
|
+
}
|
|
880
|
+
if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
|
|
881
|
+
return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
|
|
882
|
+
if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
|
|
883
|
+
return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
|
|
713
884
|
const ai = intConstValue(a), bi = intConstValue(b)
|
|
714
885
|
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
715
886
|
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
@@ -717,6 +888,16 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
717
888
|
? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
718
889
|
}
|
|
719
890
|
|
|
891
|
+
function needsRelationalToNumber(expr, vt) {
|
|
892
|
+
if (vt === VAL.STRING) return true
|
|
893
|
+
if (vt != null) return false
|
|
894
|
+
return mayReadBoxedValue(expr)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function mayReadBoxedValue(expr) {
|
|
898
|
+
return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
|
|
899
|
+
}
|
|
900
|
+
|
|
720
901
|
function intConstValue(expr) {
|
|
721
902
|
if (typeof expr === 'number' && Number.isInteger(expr)) return expr
|
|
722
903
|
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
|
|
@@ -727,13 +908,45 @@ function intConstValue(expr) {
|
|
|
727
908
|
return null
|
|
728
909
|
}
|
|
729
910
|
|
|
911
|
+
function bigintUnsignedBound(expr) {
|
|
912
|
+
const n = bigintConstValue(expr)
|
|
913
|
+
return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function bigintConstValue(expr) {
|
|
917
|
+
if (typeof expr === 'bigint') return expr
|
|
918
|
+
if (!Array.isArray(expr)) return null
|
|
919
|
+
if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
|
|
920
|
+
if (expr[0] === 'u-') {
|
|
921
|
+
const n = bigintConstValue(expr[1])
|
|
922
|
+
return n == null ? null : -n
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function arrayIndexKey(key) {
|
|
928
|
+
const n = Number(key)
|
|
929
|
+
const u = n >>> 0
|
|
930
|
+
return String(u) === key && u !== 0xffffffff ? u : null
|
|
931
|
+
}
|
|
932
|
+
|
|
730
933
|
/** Compound assignment: read → op → write back (via readVar/writeVar). */
|
|
731
934
|
function compoundAssign(name, val, f64op, i32op) {
|
|
732
935
|
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
733
936
|
const void_ = _expect === 'void'
|
|
734
937
|
const va = readVar(name), vb = emit(val)
|
|
735
|
-
|
|
736
|
-
|
|
938
|
+
// Peel f64.convert_i32_s/u when va is i32 — typed-array integer reads wrap their
|
|
939
|
+
// i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
|
|
940
|
+
// raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
|
|
941
|
+
// hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
|
|
942
|
+
let vbi = vb
|
|
943
|
+
if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
|
|
944
|
+
Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
|
|
945
|
+
const inner = vb[1]
|
|
946
|
+
vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
|
|
947
|
+
}
|
|
948
|
+
if (i32op && va.type === 'i32' && vbi.type === 'i32')
|
|
949
|
+
return writeVar(name, i32op(va, vbi), void_)
|
|
737
950
|
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
738
951
|
}
|
|
739
952
|
|
|
@@ -880,6 +1093,7 @@ export const emitter = {
|
|
|
880
1093
|
|
|
881
1094
|
'=': (name, val) => {
|
|
882
1095
|
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
1096
|
+
const void_ = _expect === 'void'
|
|
883
1097
|
// Array index assignment: arr[i] = x
|
|
884
1098
|
if (Array.isArray(name) && name[0] === '[]') {
|
|
885
1099
|
const [, arr, idx] = name
|
|
@@ -899,28 +1113,34 @@ export const emitter = {
|
|
|
899
1113
|
['local.set', `$${arrTmp}`, arrExpr],
|
|
900
1114
|
['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
|
|
901
1115
|
['local.set', `$${valTmp}`, valueExpr],
|
|
902
|
-
['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${arrTmp}`], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
|
|
1116
|
+
['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
|
|
903
1117
|
]
|
|
904
1118
|
if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
|
|
905
1119
|
body.push(['local.get', `$${valTmp}`])
|
|
906
1120
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
907
1121
|
}
|
|
908
1122
|
const setDyn = () => {
|
|
1123
|
+
if (ctx.transform.strict)
|
|
1124
|
+
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
1125
|
inc('__dyn_set')
|
|
910
|
-
return typed(['call', '$__dyn_set',
|
|
1126
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
|
|
911
1127
|
}
|
|
912
1128
|
const dispatchKey = (numericIR) => {
|
|
1129
|
+
if (ctx.transform.strict)
|
|
1130
|
+
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
1131
|
const keyTmp = temp()
|
|
914
1132
|
return typed(['block', ['result', 'f64'],
|
|
915
1133
|
['local.set', `$${keyTmp}`, keyExpr],
|
|
916
|
-
['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
|
|
917
|
-
['then', ['call', '$__dyn_set',
|
|
1134
|
+
['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
|
|
1135
|
+
['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
|
|
918
1136
|
['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
|
|
919
1137
|
}
|
|
920
1138
|
// Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
|
|
921
|
-
const litKey =
|
|
1139
|
+
const litKey = isLiteralStr(idx) ? idx[1]
|
|
1140
|
+
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
1141
|
+
: null
|
|
922
1142
|
if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
|
|
923
|
-
const slot = ctx.schema.find(arr, litKey
|
|
1143
|
+
const slot = ctx.schema.find(arr, litKey)
|
|
924
1144
|
if (slot >= 0) {
|
|
925
1145
|
const t = temp()
|
|
926
1146
|
return typed(['block', ['result', 'f64'],
|
|
@@ -931,11 +1151,25 @@ export const emitter = {
|
|
|
931
1151
|
['local.get', `$${t}`]], 'f64')
|
|
932
1152
|
}
|
|
933
1153
|
}
|
|
1154
|
+
const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
|
|
1155
|
+
if (arrIndex != null && typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
|
|
1156
|
+
const persist = ptr => {
|
|
1157
|
+
if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
|
|
1158
|
+
if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
|
|
1159
|
+
return ['local.set', `$${arr}`, ptr]
|
|
1160
|
+
}
|
|
1161
|
+
return storeArrayValue(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), persist)
|
|
1162
|
+
}
|
|
934
1163
|
if (keyType === VAL.STRING) return setDyn()
|
|
935
1164
|
if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
|
|
936
1165
|
lookupValType(arr) === 'typed') {
|
|
937
|
-
const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val)
|
|
1166
|
+
const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
|
|
938
1167
|
if (r) return r
|
|
1168
|
+
// Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
|
|
1169
|
+
// returns the stored value as f64, used directly as the expr result.
|
|
1170
|
+
inc('__typed_set_idx')
|
|
1171
|
+
return typed(['call', '$__typed_set_idx',
|
|
1172
|
+
asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
|
|
939
1173
|
}
|
|
940
1174
|
if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
|
|
941
1175
|
const inner = ctx.schema.emitInner(arr)
|
|
@@ -962,9 +1196,34 @@ export const emitter = {
|
|
|
962
1196
|
return storeArrayValue(asF64(va), keyExpr, persist)
|
|
963
1197
|
}
|
|
964
1198
|
// arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
|
|
965
|
-
const
|
|
1199
|
+
const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
|
|
1200
|
+
const arrVT = knownArrVT || VAL.OBJECT
|
|
966
1201
|
if (useRuntimeKeyDispatch) {
|
|
967
1202
|
inc('__dyn_set', '__is_str_key')
|
|
1203
|
+
// When arr type is unknown (could be TypedArray) and __typed_set_idx is
|
|
1204
|
+
// available, dispatch the numeric branch through __ptr_type so TypedArray
|
|
1205
|
+
// writes go by element type. Without this, ternary-typed arrays (e.g.
|
|
1206
|
+
// `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
|
|
1207
|
+
// f64.store boxed bytes regardless of element width.
|
|
1208
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1209
|
+
if (knownArrVT == null && hasTypedSet) {
|
|
1210
|
+
const objTmp = temp('asu')
|
|
1211
|
+
const idxTmp = tempI32('asi')
|
|
1212
|
+
inc('__ptr_type', '__typed_set_idx')
|
|
1213
|
+
return dispatchKey(keyNode => {
|
|
1214
|
+
const keyI32 = asI32(typed(keyNode, 'f64'))
|
|
1215
|
+
return ['block', ['result', 'f64'],
|
|
1216
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1217
|
+
['local.set', `$${idxTmp}`, keyI32],
|
|
1218
|
+
['local.set', `$${t}`, vv],
|
|
1219
|
+
['if', ['result', 'f64'],
|
|
1220
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1221
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1222
|
+
['else', ['block', ['result', 'f64'],
|
|
1223
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1224
|
+
['local.get', `$${t}`]]]]]
|
|
1225
|
+
})
|
|
1226
|
+
}
|
|
968
1227
|
return dispatchKey(keyNode => {
|
|
969
1228
|
const keyI32 = asI32(typed(keyNode, 'f64'))
|
|
970
1229
|
return ['block', ['result', 'f64'],
|
|
@@ -973,6 +1232,62 @@ export const emitter = {
|
|
|
973
1232
|
['local.get', `$${t}`]]
|
|
974
1233
|
})
|
|
975
1234
|
}
|
|
1235
|
+
if (typeof arr !== 'string') {
|
|
1236
|
+
const objTmp = temp('asu')
|
|
1237
|
+
const idxTmp = tempI32('asi')
|
|
1238
|
+
const ptrTmp = temp('asp')
|
|
1239
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1240
|
+
inc('__ptr_type', '__arr_set_idx_ptr')
|
|
1241
|
+
if (hasTypedSet) inc('__typed_set_idx')
|
|
1242
|
+
return typed(['block', ['result', 'f64'],
|
|
1243
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1244
|
+
['local.set', `$${idxTmp}`, vi],
|
|
1245
|
+
['local.set', `$${t}`, vv],
|
|
1246
|
+
['if', ['result', 'f64'],
|
|
1247
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
|
|
1248
|
+
['then', ['block', ['result', 'f64'],
|
|
1249
|
+
['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1250
|
+
['local.get', `$${t}`]]],
|
|
1251
|
+
['else', hasTypedSet ? ['if', ['result', 'f64'],
|
|
1252
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1253
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1254
|
+
['else', ['block', ['result', 'f64'],
|
|
1255
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1256
|
+
['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
|
|
1257
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1258
|
+
['local.get', `$${t}`]]]]], 'f64')
|
|
1259
|
+
}
|
|
1260
|
+
if (typeof arr === 'string' && knownArrVT == null) {
|
|
1261
|
+
const objTmp = temp('asu')
|
|
1262
|
+
const idxTmp = tempI32('asi')
|
|
1263
|
+
const ptrTmp = temp('asp')
|
|
1264
|
+
const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
|
|
1265
|
+
inc('__ptr_type', '__arr_set_idx_ptr')
|
|
1266
|
+
if (hasTypedSet) inc('__typed_set_idx')
|
|
1267
|
+
const persist = ptr => {
|
|
1268
|
+
if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
|
|
1269
|
+
if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
|
|
1270
|
+
return ['local.set', `$${arr}`, ptr]
|
|
1271
|
+
}
|
|
1272
|
+
return typed(['block', ['result', 'f64'],
|
|
1273
|
+
['local.set', `$${objTmp}`, asF64(va)],
|
|
1274
|
+
['local.set', `$${idxTmp}`, vi],
|
|
1275
|
+
['local.set', `$${t}`, vv],
|
|
1276
|
+
['if', ['result', 'f64'],
|
|
1277
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
|
|
1278
|
+
['then', ['block', ['result', 'f64'],
|
|
1279
|
+
['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1280
|
+
persist(['local.get', `$${ptrTmp}`]),
|
|
1281
|
+
['local.get', `$${t}`]]],
|
|
1282
|
+
['else', hasTypedSet ? ['if', ['result', 'f64'],
|
|
1283
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
|
|
1284
|
+
['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
|
|
1285
|
+
['else', ['block', ['result', 'f64'],
|
|
1286
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1287
|
+
['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
|
|
1288
|
+
['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
1289
|
+
['local.get', `$${t}`]]]]], 'f64')
|
|
1290
|
+
}
|
|
976
1291
|
return typed(['block', ['result', 'f64'],
|
|
977
1292
|
['local.set', `$${t}`, vv],
|
|
978
1293
|
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
@@ -982,10 +1297,8 @@ export const emitter = {
|
|
|
982
1297
|
if (Array.isArray(name) && name[0] === '.') {
|
|
983
1298
|
const [, obj, prop] = name
|
|
984
1299
|
// 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
1300
|
if (typeof obj === 'string' && ctx.schema.find) {
|
|
988
|
-
const idx = ctx.schema.find(obj, prop
|
|
1301
|
+
const idx = ctx.schema.find(obj, prop)
|
|
989
1302
|
if (idx >= 0) {
|
|
990
1303
|
const va = emit(obj), vv = asF64(emit(val)), t = temp()
|
|
991
1304
|
const shadow = needsDynShadow(obj)
|
|
@@ -995,39 +1308,60 @@ export const emitter = {
|
|
|
995
1308
|
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]], ['local.get', `$${t}`]],
|
|
996
1309
|
]
|
|
997
1310
|
if (shadow)
|
|
998
|
-
stmts.push(['drop', ['call', '$__dyn_set',
|
|
1311
|
+
stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
|
|
999
1312
|
stmts.push(['local.get', `$${t}`])
|
|
1000
1313
|
return typed(['block', ['result', 'f64'], ...stmts], 'f64')
|
|
1001
1314
|
}
|
|
1002
1315
|
}
|
|
1003
1316
|
if (typeof obj === 'string') {
|
|
1004
1317
|
const objType = keyValType(obj)
|
|
1005
|
-
|
|
1318
|
+
// OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
|
|
1319
|
+
// properties go through __dyn_set, which writes to the per-OBJECT
|
|
1320
|
+
// propsPtr at off-16 — same path as object-literal dyn shadow writes
|
|
1321
|
+
// (module/object.js). __hash_set assumes HASH bucket layout and would
|
|
1322
|
+
// corrupt OBJECT memory.
|
|
1323
|
+
if (usesDynProps(objType) || objType === VAL.OBJECT) {
|
|
1324
|
+
inc('__dyn_set')
|
|
1325
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1326
|
+
}
|
|
1327
|
+
if (ctx.func.names.has(obj) && !ctx.func.locals?.has(obj) && !ctx.func.current?.params?.some(p => p.name === obj)) {
|
|
1006
1328
|
inc('__dyn_set')
|
|
1007
|
-
return typed(['call', '$__dyn_set',
|
|
1329
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1330
|
+
}
|
|
1331
|
+
if (objType == null && ctx.transform.host !== 'wasi') {
|
|
1332
|
+
ctx.features.external = true
|
|
1008
1333
|
}
|
|
1009
|
-
if (objType == null) ctx.features.external = true
|
|
1010
1334
|
inc('__hash_set')
|
|
1011
|
-
const setCall = typed(['call', '$__hash_set',
|
|
1335
|
+
const setCall = typed(['f64.reinterpret_i64', ['call', '$__hash_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1012
1336
|
if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
|
|
1013
1337
|
['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
|
|
1338
|
+
// Closure-captured (boxed) locals store the value at the cell address — local.tee
|
|
1339
|
+
// would write to the i32 cell pointer, not the f64 value. Route through writeVar.
|
|
1340
|
+
if (ctx.func.boxed?.has(obj)) return writeVar(obj, setCall, false)
|
|
1014
1341
|
return typed(['local.tee', `$${obj}`, setCall], 'f64')
|
|
1015
1342
|
}
|
|
1016
|
-
ctx.features.external = true
|
|
1343
|
+
if (ctx.transform.host !== 'wasi') ctx.features.external = true
|
|
1017
1344
|
inc('__dyn_set')
|
|
1018
|
-
return typed(['call', '$__dyn_set',
|
|
1345
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
|
|
1019
1346
|
}
|
|
1020
1347
|
if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
|
|
1021
|
-
|
|
1348
|
+
if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
|
|
1349
|
+
inc('__to_num')
|
|
1350
|
+
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
1351
|
+
}
|
|
1022
1352
|
return writeVar(name, emit(val), void_)
|
|
1023
1353
|
},
|
|
1024
1354
|
|
|
1025
1355
|
// Compound assignments: read-modify-write with type coercion
|
|
1026
1356
|
'+=': (name, val) => {
|
|
1027
|
-
// String concatenation: desugar to name = name + val (+ handler knows about strings)
|
|
1357
|
+
// String concatenation: desugar to name = name + val (+ handler knows about strings).
|
|
1358
|
+
// Also desugar when either side has unknown type — the `+` operator picks runtime
|
|
1359
|
+
// string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
|
|
1360
|
+
// silently corrupt string concatenations through unknown-typed values.
|
|
1028
1361
|
const vt = typeof name === 'string' ? keyValType(name) : null
|
|
1029
1362
|
const vtB = keyValType(val)
|
|
1030
1363
|
if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
|
|
1364
|
+
if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
|
|
1031
1365
|
return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
|
|
1032
1366
|
},
|
|
1033
1367
|
...Object.fromEntries([
|
|
@@ -1068,11 +1402,8 @@ export const emitter = {
|
|
|
1068
1402
|
const t = temp()
|
|
1069
1403
|
const va = readVar(name)
|
|
1070
1404
|
// 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]]]
|
|
1405
|
+
const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1406
|
+
const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
|
|
1076
1407
|
// &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
|
|
1077
1408
|
const [thenExpr, elseExpr] = op === '||='
|
|
1078
1409
|
? [['local.get', `$${t}`], asF64(emit(val))]
|
|
@@ -1109,12 +1440,25 @@ export const emitter = {
|
|
|
1109
1440
|
const vtA = keyValType(a)
|
|
1110
1441
|
const vtB = keyValType(b)
|
|
1111
1442
|
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
1443
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
1444
|
+
// generic concat dispatch when rhs is a string-index. The byte flows
|
|
1445
|
+
// straight from __char_at into memory, and the bump-extend path elides
|
|
1446
|
+
// the alloc+copy when lhs is the heap-top STRING.
|
|
1447
|
+
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
1448
|
+
if (keyValType(b[1]) === VAL.STRING) {
|
|
1449
|
+
inc('__str_append_byte', '__char_at')
|
|
1450
|
+
return typed(['call', '$__str_append_byte',
|
|
1451
|
+
asI64(emit(a)),
|
|
1452
|
+
['call', '$__char_at', asI64(emit(b[1])), asI32(emit(b[2]))],
|
|
1453
|
+
], 'f64')
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1112
1456
|
inc('__str_concat_raw')
|
|
1113
|
-
return typed(['call', '$__str_concat_raw',
|
|
1457
|
+
return typed(['call', '$__str_concat_raw', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1114
1458
|
}
|
|
1115
1459
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
1116
1460
|
inc('__str_concat')
|
|
1117
|
-
return typed(['call', '$__str_concat',
|
|
1461
|
+
return typed(['call', '$__str_concat', asI64(emit(a)), asI64(emit(b))], 'f64')
|
|
1118
1462
|
}
|
|
1119
1463
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
1120
1464
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -1125,9 +1469,9 @@ export const emitter = {
|
|
|
1125
1469
|
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
1126
1470
|
const tA = temp('add'), tB = temp('add')
|
|
1127
1471
|
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}`]]
|
|
1472
|
+
const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
|
|
1473
|
+
const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
|
|
1474
|
+
const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
|
|
1131
1475
|
const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
1132
1476
|
if (checkA && checkB) {
|
|
1133
1477
|
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
@@ -1139,8 +1483,8 @@ export const emitter = {
|
|
|
1139
1483
|
['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
|
|
1140
1484
|
], 'f64')
|
|
1141
1485
|
}
|
|
1142
|
-
const va = emit(a), vb = emit(b)
|
|
1143
|
-
if (
|
|
1486
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
|
|
1487
|
+
if (_f) return _f
|
|
1144
1488
|
if (isLit(vb) && litVal(vb) === 0) return va
|
|
1145
1489
|
if (isLit(va) && litVal(va) === 0) return vb
|
|
1146
1490
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.add', va, vb], 'i32')
|
|
@@ -1152,9 +1496,9 @@ export const emitter = {
|
|
|
1152
1496
|
return b === undefined
|
|
1153
1497
|
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
1154
1498
|
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
1155
|
-
if (b === undefined)
|
|
1156
|
-
const va = emit(a), vb = emit(b)
|
|
1157
|
-
if (
|
|
1499
|
+
if (b === undefined) return emitNeg(a)
|
|
1500
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
1501
|
+
if (_f) return _f
|
|
1158
1502
|
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1159
1503
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.sub', va, vb], 'i32')
|
|
1160
1504
|
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
@@ -1162,18 +1506,18 @@ export const emitter = {
|
|
|
1162
1506
|
'u+': a => {
|
|
1163
1507
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
1164
1508
|
return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
|
|
1509
|
+
const v = emit(a)
|
|
1510
|
+
if (v.type === 'i32') return asF64(v)
|
|
1511
|
+
if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
|
|
1165
1512
|
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')
|
|
1513
|
+
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
1171
1514
|
},
|
|
1515
|
+
'u-': a => emitNeg(a),
|
|
1172
1516
|
'*': (a, b) => {
|
|
1173
1517
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1174
1518
|
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
1175
|
-
const va = emit(a), vb = emit(b)
|
|
1176
|
-
if (
|
|
1519
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
|
|
1520
|
+
if (_f) return _f
|
|
1177
1521
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1178
1522
|
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
1179
1523
|
if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
@@ -1184,23 +1528,24 @@ export const emitter = {
|
|
|
1184
1528
|
'/': (a, b) => {
|
|
1185
1529
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1186
1530
|
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
1187
|
-
const va = emit(a), vb = emit(b)
|
|
1188
|
-
if (
|
|
1531
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
1532
|
+
if (_f) return _f
|
|
1189
1533
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1190
1534
|
return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1191
1535
|
},
|
|
1192
1536
|
'%': (a, b) => {
|
|
1193
1537
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1194
1538
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
1195
|
-
const va = emit(a), vb = emit(b)
|
|
1196
|
-
if (
|
|
1539
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
1540
|
+
if (_f) return _f
|
|
1197
1541
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.rem_s', va, vb], 'i32')
|
|
1198
1542
|
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
1199
1543
|
},
|
|
1200
|
-
|
|
1201
1544
|
// === Comparisons (always i32 result) ===
|
|
1202
1545
|
|
|
1203
1546
|
'==': (a, b) => {
|
|
1547
|
+
const charCmp = emitSingleCharIndexCmp(a, b)
|
|
1548
|
+
if (charCmp) return charCmp
|
|
1204
1549
|
// JS loose nullish equality: x == null / x == undefined.
|
|
1205
1550
|
// If the non-literal side has a known non-null VAL type, fold to 0.
|
|
1206
1551
|
if (isNullishLit(a)) {
|
|
@@ -1215,11 +1560,13 @@ export const emitter = {
|
|
|
1215
1560
|
const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
|
|
1216
1561
|
const va = emit(a), vb = emit(b)
|
|
1217
1562
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1563
|
+
// Either side known-pure NUMBER (literal or typed) → f64.eq is correct regardless
|
|
1564
|
+
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1565
|
+
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
1566
|
+
// Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
|
|
1567
|
+
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1568
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1569
|
+
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
1223
1570
|
// Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
|
|
1224
1571
|
// JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
|
|
1225
1572
|
// STRING needs __eq (heap strings can be equal by content but different pointers).
|
|
@@ -1228,9 +1575,11 @@ export const emitter = {
|
|
|
1228
1575
|
return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1229
1576
|
}
|
|
1230
1577
|
inc('__eq')
|
|
1231
|
-
return typed(['call', '$__eq',
|
|
1578
|
+
return typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
1232
1579
|
},
|
|
1233
1580
|
'!=': (a, b) => {
|
|
1581
|
+
const charCmp = emitSingleCharIndexCmp(a, b, true)
|
|
1582
|
+
if (charCmp) return charCmp
|
|
1234
1583
|
if (isNullishLit(a)) {
|
|
1235
1584
|
if (valTypeOf(b)) return emitNum(1)
|
|
1236
1585
|
return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
|
|
@@ -1242,14 +1591,14 @@ export const emitter = {
|
|
|
1242
1591
|
const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
|
|
1243
1592
|
const va = emit(a), vb = emit(b)
|
|
1244
1593
|
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
|
|
1594
|
+
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1595
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1596
|
+
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
1248
1597
|
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1249
1598
|
return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1250
1599
|
}
|
|
1251
1600
|
inc('__eq')
|
|
1252
|
-
return typed(['i32.eqz', ['call', '$__eq',
|
|
1601
|
+
return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
|
|
1253
1602
|
},
|
|
1254
1603
|
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
1255
1604
|
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
@@ -1264,13 +1613,13 @@ export const emitter = {
|
|
|
1264
1613
|
// Unboxed pointer offsets: falsy iff zero offset.
|
|
1265
1614
|
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
1266
1615
|
// 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) {
|
|
1616
|
+
// Excludes STRING — empty string '' is a valid (non-null) pointer but is falsy.
|
|
1617
|
+
const vt = resolveValType(a, valTypeOf, lookupValType)
|
|
1618
|
+
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
|
|
1270
1619
|
return isNullish(asF64(v))
|
|
1271
1620
|
}
|
|
1272
1621
|
inc('__is_truthy')
|
|
1273
|
-
return typed(['i32.eqz', ['call', '$__is_truthy',
|
|
1622
|
+
return typed(['i32.eqz', ['call', '$__is_truthy', asI64(v)]], 'i32')
|
|
1274
1623
|
},
|
|
1275
1624
|
|
|
1276
1625
|
'?:': (a, b, c) => {
|
|
@@ -1311,7 +1660,21 @@ export const emitter = {
|
|
|
1311
1660
|
}
|
|
1312
1661
|
}
|
|
1313
1662
|
const fb = asF64(vb), fc = asF64(vc)
|
|
1314
|
-
|
|
1663
|
+
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1664
|
+
const vtc = resolveValType(c, valTypeOf, lookupValType)
|
|
1665
|
+
const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
|
|
1666
|
+
const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
|
|
1667
|
+
|| vb.closureFuncIdx != null || vc.closureFuncIdx != null
|
|
1668
|
+
|| isNaNBoxLit(fb) || isNaNBoxLit(fc)
|
|
1669
|
+
if (refPayload) {
|
|
1670
|
+
const ib = ['i64.reinterpret_f64', fb]
|
|
1671
|
+
const ic = ['i64.reinterpret_f64', fc]
|
|
1672
|
+
const bits = isPureIR(fb) && isPureIR(fc)
|
|
1673
|
+
? ['select', ib, ic, cond]
|
|
1674
|
+
: ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
|
|
1675
|
+
return typed(['f64.reinterpret_i64', bits], 'f64')
|
|
1676
|
+
}
|
|
1677
|
+
if (!refPayload && isPureIR(fb) && isPureIR(fc))
|
|
1315
1678
|
return typed(['select', fb, fc, cond], 'f64')
|
|
1316
1679
|
return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
|
|
1317
1680
|
},
|
|
@@ -1397,7 +1760,11 @@ export const emitter = {
|
|
|
1397
1760
|
|
|
1398
1761
|
// === Bitwise (i32 for numbers, i64 for BigInt) ===
|
|
1399
1762
|
|
|
1400
|
-
|
|
1763
|
+
// Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
|
|
1764
|
+
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
1765
|
+
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
1766
|
+
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
1767
|
+
'~': 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
1768
|
...Object.fromEntries([
|
|
1402
1769
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
1403
1770
|
].map(([op, fn]) => [op, (a, b) => {
|
|
@@ -1410,16 +1777,20 @@ export const emitter = {
|
|
|
1410
1777
|
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
1411
1778
|
if (op === '>>') return emitNum(la >> lb)
|
|
1412
1779
|
}
|
|
1413
|
-
|
|
1780
|
+
const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
|
|
1781
|
+
const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1782
|
+
return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
|
|
1414
1783
|
}])),
|
|
1415
1784
|
'>>>': (a, b) => {
|
|
1416
|
-
const va = emit(a), vb = emit(b)
|
|
1417
|
-
if (
|
|
1785
|
+
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a >>> b)
|
|
1786
|
+
if (_f) return _f
|
|
1418
1787
|
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
1419
1788
|
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
1420
1789
|
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
1421
1790
|
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
1422
|
-
const
|
|
1791
|
+
const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
|
|
1792
|
+
const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
|
|
1793
|
+
const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
|
|
1423
1794
|
node.unsigned = true
|
|
1424
1795
|
return node
|
|
1425
1796
|
},
|
|
@@ -1547,6 +1918,8 @@ export const emitter = {
|
|
|
1547
1918
|
let argList = Array.isArray(callArgs)
|
|
1548
1919
|
? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
|
|
1549
1920
|
: callArgs ? [callArgs] : []
|
|
1921
|
+
// Trailing comma in call: parser emits a trailing null sentinel — drop it
|
|
1922
|
+
while (argList.length && argList[argList.length - 1] == null) argList.pop()
|
|
1550
1923
|
|
|
1551
1924
|
// Helper: expand spread arguments into flat list of normal arguments + spread markers
|
|
1552
1925
|
// Returns { normal: [...], spreads: [(pos, expr), ...] }
|
|
@@ -1566,6 +1939,26 @@ export const emitter = {
|
|
|
1566
1939
|
|
|
1567
1940
|
const parsed = parseArgs(argList)
|
|
1568
1941
|
|
|
1942
|
+
// Optional method call: obj?.method(args) — null if obj is nullish, else
|
|
1943
|
+
// obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
|
|
1944
|
+
// distinct from the regular method call's ['.', obj, method] callee. Receiver
|
|
1945
|
+
// hoists into a temp so the nullish check and the method dispatch below see
|
|
1946
|
+
// the same evaluation; recursion with a synthetic '.'-callee reuses the type-
|
|
1947
|
+
// aware method-dispatch in this same handler rather than duplicating it.
|
|
1948
|
+
if (Array.isArray(callee) && callee[0] === '?.') {
|
|
1949
|
+
const [, obj, method] = callee
|
|
1950
|
+
const t = `${T}om${ctx.func.uniq++}`
|
|
1951
|
+
ctx.func.locals.set(t, 'f64')
|
|
1952
|
+
const va = asF64(emit(obj))
|
|
1953
|
+
const methodCall = emitter['()'](['.', t, method], callArgs)
|
|
1954
|
+
return typed(['block', ['result', 'f64'],
|
|
1955
|
+
['local.set', `$${t}`, va],
|
|
1956
|
+
['if', ['result', 'f64'],
|
|
1957
|
+
['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
|
|
1958
|
+
['then', asF64(methodCall)],
|
|
1959
|
+
['else', nullExpr()]]], 'f64')
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1569
1962
|
// Method call: obj.method(args) → type-aware dispatch
|
|
1570
1963
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1571
1964
|
const [, obj, method] = callee
|
|
@@ -1577,7 +1970,7 @@ export const emitter = {
|
|
|
1577
1970
|
const func = ctx.func.map.get(fname)
|
|
1578
1971
|
const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1579
1972
|
while (emittedArgs.length < func.sig.params.length)
|
|
1580
|
-
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
1973
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
1581
1974
|
const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
|
|
1582
1975
|
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1583
1976
|
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
@@ -1585,7 +1978,10 @@ export const emitter = {
|
|
|
1585
1978
|
}
|
|
1586
1979
|
}
|
|
1587
1980
|
|
|
1588
|
-
|
|
1981
|
+
let vt = keyValType(obj)
|
|
1982
|
+
if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
|
|
1983
|
+
&& (method === 'slice' || method === 'concat')
|
|
1984
|
+
&& vt !== VAL.STRING && vt !== VAL.ARRAY) vt = null
|
|
1589
1985
|
|
|
1590
1986
|
// Helper to call method with arguments (handles spread expansion)
|
|
1591
1987
|
const callMethod = (objArg, methodEmitter) => {
|
|
@@ -1624,23 +2020,23 @@ export const emitter = {
|
|
|
1624
2020
|
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1625
2021
|
ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1626
2022
|
if (srcIsArr) {
|
|
1627
|
-
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${sa}`]]])
|
|
2023
|
+
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1628
2024
|
ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
|
|
1629
2025
|
} else {
|
|
1630
|
-
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['local.get', `$${sa}`]]])
|
|
2026
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
|
|
1631
2027
|
}
|
|
1632
2028
|
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1633
2029
|
if (objIsArr) {
|
|
1634
2030
|
ir.push(['local.set', `$${ol}`,
|
|
1635
|
-
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${o}`]], ['i32.const', 8]]]])
|
|
2031
|
+
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
|
|
1636
2032
|
} else {
|
|
1637
|
-
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['local.get', `$${o}`]]])
|
|
2033
|
+
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1638
2034
|
}
|
|
1639
2035
|
// 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}`],
|
|
2036
|
+
ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1641
2037
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1642
2038
|
// base captured AFTER grow (grow may relocate the array).
|
|
1643
|
-
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${o}`]]])
|
|
2039
|
+
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
|
|
1644
2040
|
// Tight store loop.
|
|
1645
2041
|
ir.push(['local.set', `$${si}`, ['i32.const', 0]])
|
|
1646
2042
|
const loopId = ctx.func.uniq++
|
|
@@ -1656,7 +2052,7 @@ export const emitter = {
|
|
|
1656
2052
|
['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
|
|
1657
2053
|
['br', `$continue${loopId}`]]])
|
|
1658
2054
|
// Single set_len for the full spread.
|
|
1659
|
-
ir.push(['call', '$__set_len', ['local.get', `$${o}`],
|
|
2055
|
+
ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
|
|
1660
2056
|
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1661
2057
|
// Update source variable: grow may have moved the pointer.
|
|
1662
2058
|
if (ctx.func.boxed?.has(objArg)) {
|
|
@@ -1694,7 +2090,7 @@ export const emitter = {
|
|
|
1694
2090
|
inc('__len')
|
|
1695
2091
|
const n = multiCount(spreadExpr)
|
|
1696
2092
|
ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1697
|
-
ir.push(['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${arr}`]]])
|
|
2093
|
+
ir.push(['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]])
|
|
1698
2094
|
ir.push(['local.set', `$${idx}`,
|
|
1699
2095
|
reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
|
|
1700
2096
|
const loopId = ctx.func.uniq++
|
|
@@ -1745,7 +2141,7 @@ export const emitter = {
|
|
|
1745
2141
|
const n = multiCount(spreadExpr)
|
|
1746
2142
|
irG.push(
|
|
1747
2143
|
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1748
|
-
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
2144
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
|
|
1749
2145
|
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1750
2146
|
const loopId = ctx.func.uniq++
|
|
1751
2147
|
const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
|
|
@@ -1784,7 +2180,7 @@ export const emitter = {
|
|
|
1784
2180
|
const n = multiCount(spreadExpr)
|
|
1785
2181
|
irG.push(
|
|
1786
2182
|
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1787
|
-
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
2183
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
|
|
1788
2184
|
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1789
2185
|
const loopId = ctx.func.uniq++
|
|
1790
2186
|
const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
|
|
@@ -1845,15 +2241,26 @@ export const emitter = {
|
|
|
1845
2241
|
const genEmitter = ctx.core.emit[genKey]
|
|
1846
2242
|
return typed(['block', ['result', 'f64'],
|
|
1847
2243
|
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1848
|
-
['local.set', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]],
|
|
2244
|
+
['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1849
2245
|
['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]]],
|
|
2246
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
|
|
1853
2247
|
['then', callMethod(t, strEmitter)],
|
|
1854
2248
|
['else', callMethod(t, genEmitter)]]], 'f64')
|
|
1855
2249
|
}
|
|
1856
2250
|
|
|
2251
|
+
// Schema property function call: x.prop(args) where prop is a closure in boxed schema
|
|
2252
|
+
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
|
|
2253
|
+
const idx = ctx.schema.find(obj, method)
|
|
2254
|
+
if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
|
|
2255
|
+
const propRead = typed(['f64.load', ['i32.add', ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
|
|
2256
|
+
if (parsed.hasSpread) {
|
|
2257
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2258
|
+
return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
|
|
2259
|
+
}
|
|
2260
|
+
return ctx.closure.call(propRead, parsed.normal)
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1857
2264
|
// Schema property function call: x.prop(args) where prop is a closure in boxed schema
|
|
1858
2265
|
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
|
|
1859
2266
|
const idx = ctx.schema.find(obj, method)
|
|
@@ -1876,7 +2283,7 @@ export const emitter = {
|
|
|
1876
2283
|
ctx.func.locals.set(objTmp, 'f64')
|
|
1877
2284
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1878
2285
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
1879
|
-
const propRead = typed(['call', '$__dyn_get_expr', ['local.get', `$${objTmp}`],
|
|
2286
|
+
const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
|
|
1880
2287
|
const propTmp = `${T}mprop${ctx.func.uniq++}`
|
|
1881
2288
|
ctx.func.locals.set(propTmp, 'f64')
|
|
1882
2289
|
if (usesDynProps(vt)) {
|
|
@@ -1885,7 +2292,18 @@ export const emitter = {
|
|
|
1885
2292
|
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1886
2293
|
['local.set', `$${propTmp}`, propRead],
|
|
1887
2294
|
['if', ['result', 'f64'],
|
|
1888
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
|
|
2295
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
2296
|
+
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
2297
|
+
['else', undefExpr()]]], 'f64')
|
|
2298
|
+
}
|
|
2299
|
+
// WASI: no PTR.EXTERNAL values; closure-only dispatch is correct.
|
|
2300
|
+
if (ctx.transform.host === 'wasi') {
|
|
2301
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2302
|
+
return typed(['block', ['result', 'f64'],
|
|
2303
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
2304
|
+
['local.set', `$${propTmp}`, propRead],
|
|
2305
|
+
['if', ['result', 'f64'],
|
|
2306
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
1889
2307
|
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
1890
2308
|
['else', undefExpr()]]], 'f64')
|
|
1891
2309
|
}
|
|
@@ -1895,22 +2313,29 @@ export const emitter = {
|
|
|
1895
2313
|
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1896
2314
|
['local.set', `$${propTmp}`, propRead],
|
|
1897
2315
|
['if', ['result', 'f64'],
|
|
1898
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
|
|
2316
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
1899
2317
|
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
|
|
1900
2318
|
['else', ['if', ['result', 'f64'],
|
|
1901
|
-
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${objTmp}`]], ['i32.const', PTR.EXTERNAL]],
|
|
1902
|
-
['then', ['
|
|
2319
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.EXTERNAL]],
|
|
2320
|
+
['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2321
|
+
['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
|
|
2322
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2323
|
+
['i64.reinterpret_f64', arrayIR]]]],
|
|
1903
2324
|
['else', undefExpr()]]]]], 'f64')
|
|
1904
2325
|
}
|
|
1905
2326
|
|
|
1906
2327
|
// Unknown callee - assume external method
|
|
1907
2328
|
if (ctx.transform.strict)
|
|
1908
2329
|
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 }.`)
|
|
2330
|
+
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
1909
2331
|
inc('__ext_call')
|
|
1910
2332
|
ctx.features.external = true
|
|
1911
2333
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1912
2334
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
1913
|
-
return typed(['
|
|
2335
|
+
return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
|
|
2336
|
+
['i64.reinterpret_f64', asF64(emit(obj))],
|
|
2337
|
+
['i64.reinterpret_f64', asF64(emit(['str', method]))],
|
|
2338
|
+
['i64.reinterpret_f64', arrayIR]]], 'f64');
|
|
1914
2339
|
}
|
|
1915
2340
|
|
|
1916
2341
|
if (ctx.core.emit[callee]) {
|
|
@@ -1929,17 +2354,17 @@ export const emitter = {
|
|
|
1929
2354
|
}
|
|
1930
2355
|
|
|
1931
2356
|
// Direct call if callee is a known top-level function
|
|
1932
|
-
if (typeof callee === 'string' && ctx.func.names.has(callee)) {
|
|
2357
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee) && !ctx.func.locals?.has(callee)) {
|
|
1933
2358
|
const func = ctx.func.map.get(callee)
|
|
1934
2359
|
|
|
1935
2360
|
// Rest param case: collect all args (including expanded spreads) into array
|
|
1936
2361
|
if (func?.rest) {
|
|
1937
2362
|
const fixedParamCount = func.sig.params.length - 1
|
|
1938
2363
|
const fixedArgs = parsed.normal.slice(0, fixedParamCount)
|
|
1939
|
-
// Pad missing fixed args with
|
|
2364
|
+
// Pad missing fixed args with `undefined` so default-param init triggers per spec.
|
|
1940
2365
|
const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1941
2366
|
while (emittedFixed.length < fixedParamCount)
|
|
1942
|
-
emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') :
|
|
2367
|
+
emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
1943
2368
|
|
|
1944
2369
|
// Reconstruct with spreads, then take rest args
|
|
1945
2370
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
@@ -1957,10 +2382,16 @@ export const emitter = {
|
|
|
1957
2382
|
|
|
1958
2383
|
// Regular function call without rest params
|
|
1959
2384
|
if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
|
|
1960
|
-
// Pad missing args with
|
|
2385
|
+
// Pad missing args with `undefined` so default-param init triggers per spec
|
|
2386
|
+
// (only undefined, not null, should trigger defaults). Drop extras to match
|
|
2387
|
+
// JS calling convention — emitting them anyway produces an invalid call
|
|
2388
|
+
// when the callee is a fixed-arity import (e.g. `_interp`-registered host
|
|
2389
|
+
// stubs) since wasm validates arg count. Use ?? rather than || so a
|
|
2390
|
+
// legitimate 0-arity callee isn't bypassed.
|
|
1961
2391
|
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') :
|
|
2392
|
+
const expected = func?.sig.params.length ?? args.length
|
|
2393
|
+
while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
|
|
2394
|
+
if (args.length > expected) args.length = expected
|
|
1964
2395
|
// Multi-value return: materialize as heap array (caller expects single pointer)
|
|
1965
2396
|
if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
|
|
1966
2397
|
const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
|
|
@@ -2001,13 +2432,80 @@ export const emitter = {
|
|
|
2001
2432
|
return ctx.closure.call(emit(callee), parsed.normal)
|
|
2002
2433
|
}
|
|
2003
2434
|
|
|
2004
|
-
// Unknown callee — assume direct call
|
|
2005
|
-
|
|
2435
|
+
// Unknown callee — assume direct call. Match arg count to the declared
|
|
2436
|
+
// signature when the callee is a registered env import (e.g. `_interp`-
|
|
2437
|
+
// wired host stubs). JS calling convention drops extras and pads missing;
|
|
2438
|
+
// wasm requires exact arity, so emitting raw argList against a fixed-arity
|
|
2439
|
+
// import would invalidate the module.
|
|
2440
|
+
let calleeArity = null
|
|
2441
|
+
if (typeof callee === 'string') {
|
|
2442
|
+
const imp = ctx.module.imports?.find(i =>
|
|
2443
|
+
Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
|
|
2444
|
+
if (imp) {
|
|
2445
|
+
let n = 0
|
|
2446
|
+
for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
|
|
2447
|
+
calleeArity = n
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
const emittedArgs = argList.map(a => asF64(emit(a)))
|
|
2451
|
+
if (calleeArity != null) {
|
|
2452
|
+
while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
|
|
2453
|
+
if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
|
|
2454
|
+
}
|
|
2455
|
+
return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
|
|
2006
2456
|
},
|
|
2007
2457
|
}
|
|
2008
2458
|
|
|
2009
2459
|
// === Emit dispatch ===
|
|
2010
2460
|
|
|
2461
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
|
|
2462
|
+
// Per ECMAScript, an optional access short-circuits the entire continuation, not just
|
|
2463
|
+
// its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
|
|
2464
|
+
// nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
|
|
2465
|
+
//
|
|
2466
|
+
// At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
|
|
2467
|
+
// the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
|
|
2468
|
+
// with that optional replaced by a regular access. The single guard short-circuits the
|
|
2469
|
+
// whole continuation. Nested optionals further inside the chain are left intact and
|
|
2470
|
+
// handle their own short-circuiting on recursion.
|
|
2471
|
+
function liftOptionalChain(node) {
|
|
2472
|
+
const path = []
|
|
2473
|
+
let cur = node
|
|
2474
|
+
while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
|
|
2475
|
+
cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
|
|
2476
|
+
path.push(cur)
|
|
2477
|
+
cur = cur[1]
|
|
2478
|
+
}
|
|
2479
|
+
// Find the deepest optional with continuation outside it. optIdx === 0 means the
|
|
2480
|
+
// chain root itself is optional with no continuation — handled by the regular
|
|
2481
|
+
// `?.` / `?.[]` / `?.()` emitters.
|
|
2482
|
+
let optIdx = -1
|
|
2483
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
2484
|
+
if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
|
|
2485
|
+
optIdx = i
|
|
2486
|
+
break
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (optIdx <= 0) return null
|
|
2490
|
+
const opt = path[optIdx]
|
|
2491
|
+
const headExpr = opt[1]
|
|
2492
|
+
const t = temp('oc')
|
|
2493
|
+
let rebuilt
|
|
2494
|
+
if (opt[0] === '?.') rebuilt = ['.', t, opt[2]]
|
|
2495
|
+
else if (opt[0] === '?.[]') rebuilt = ['[]', t, opt[2]]
|
|
2496
|
+
else rebuilt = ['()', t, ...opt.slice(2)]
|
|
2497
|
+
for (let i = optIdx - 1; i >= 0; i--) {
|
|
2498
|
+
const outer = path[i]
|
|
2499
|
+
rebuilt = [outer[0], rebuilt, ...outer.slice(2)]
|
|
2500
|
+
}
|
|
2501
|
+
return typed(['block', ['result', 'f64'],
|
|
2502
|
+
['local.set', `$${t}`, asF64(emit(headExpr))],
|
|
2503
|
+
['if', ['result', 'f64'],
|
|
2504
|
+
['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
|
|
2505
|
+
['then', asF64(emit(rebuilt))],
|
|
2506
|
+
['else', undefExpr()]]], 'f64')
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2011
2509
|
/**
|
|
2012
2510
|
* Emit single AST node to typed WASM IR.
|
|
2013
2511
|
* Every returned node has .type = 'i32' | 'f64'.
|
|
@@ -2028,14 +2526,10 @@ export function emit(node, expect) {
|
|
|
2028
2526
|
const n = node & 0xFFFFFFFFFFFFFFFFn
|
|
2029
2527
|
return typed(['f64.reinterpret_i64', ['i64.const', '0x' + n.toString(16)]], 'f64')
|
|
2030
2528
|
}
|
|
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
|
-
}
|
|
2529
|
+
if (typeof node === 'number') return emitNum(node)
|
|
2036
2530
|
if (typeof node === 'string') {
|
|
2037
2531
|
// 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))
|
|
2532
|
+
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
2533
|
return readVar(node)
|
|
2040
2534
|
// Top-level function used as value → wrap as closure pointer for call_indirect
|
|
2041
2535
|
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 +2566,28 @@ export function emit(node, expect) {
|
|
|
2072
2566
|
}
|
|
2073
2567
|
let idx = ctx.closure.table.indexOf(trampolineName)
|
|
2074
2568
|
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
|
|
2075
|
-
|
|
2569
|
+
const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
2570
|
+
ir.closureFuncIdx = idx
|
|
2571
|
+
return ir
|
|
2076
2572
|
}
|
|
2077
2573
|
// Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
|
|
2078
|
-
if (node.includes('.') && ctx.core.emit[node])
|
|
2574
|
+
if (node.includes('.') && ctx.core.emit[node]) {
|
|
2575
|
+
const handler = ctx.core.emit[node]
|
|
2576
|
+
return handler.length > 0 ? builtinFunctionValue(node) : handler()
|
|
2577
|
+
}
|
|
2079
2578
|
// Auto-import known host globals (WebAssembly, globalThis, etc.)
|
|
2080
2579
|
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
2081
2580
|
if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
|
|
2581
|
+
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
2582
|
ctx.features.external = true
|
|
2083
2583
|
ctx.scope.globals.set(node, null)
|
|
2084
|
-
|
|
2085
|
-
|
|
2584
|
+
// Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed
|
|
2585
|
+
// external-ref payload across the wasm↔JS global boundary (same hazard
|
|
2586
|
+
// as env.print/setTimeout — see module/console.js header). asF64()
|
|
2587
|
+
// reinterprets to f64 at every read site.
|
|
2588
|
+
ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, 'i64']])
|
|
2589
|
+
ctx.scope.globalTypes.set(node, 'i64')
|
|
2590
|
+
return typed(['global.get', `$${node}`], 'i64')
|
|
2086
2591
|
}
|
|
2087
2592
|
const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
|
|
2088
2593
|
return typed(['local.get', `$${node}`], t)
|
|
@@ -2099,6 +2604,14 @@ export function emit(node, expect) {
|
|
|
2099
2604
|
return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
|
|
2100
2605
|
}
|
|
2101
2606
|
|
|
2607
|
+
// Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
|
|
2608
|
+
// Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
|
|
2609
|
+
// with the optional already replaced by a non-optional access on a guarded temp.
|
|
2610
|
+
if (op === '.' || op === '[]' || op === '()') {
|
|
2611
|
+
const lifted = liftOptionalChain(node)
|
|
2612
|
+
if (lifted) return lifted
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2102
2615
|
const handler = ctx.core.emit[op]
|
|
2103
2616
|
if (!handler) err(`Unknown op: ${op}`)
|
|
2104
2617
|
return handler(...args)
|