jz 0.2.0 → 0.3.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 +8 -28
- package/cli.js +19 -2
- package/index.js +14 -4
- package/module/array.js +66 -9
- package/module/collection.js +68 -35
- package/module/core.js +65 -19
- package/module/date.js +104 -7
- package/module/function.js +2 -1
- package/module/json.js +66 -8
- package/module/number.js +127 -5
- package/module/object.js +88 -1
- package/module/string.js +8 -2
- package/module/typedarray.js +7 -1
- package/package.json +3 -3
- package/src/analyze.js +18 -6
- package/src/assemble.js +544 -0
- package/src/ast.js +160 -0
- package/src/auto-config.js +120 -0
- package/src/autoload.js +6 -3
- package/src/compile.js +22 -620
- package/src/ctx.js +33 -5
- package/src/emit.js +97 -165
- package/src/host.js +12 -0
- package/src/ir.js +19 -0
- package/src/jzify.js +306 -7
- package/src/narrow.js +2 -1
- package/src/optimize.js +298 -24
- package/src/plan.js +258 -37
- package/src/prepare.js +23 -2
- package/src/vectorize.js +3 -12
package/src/ctx.js
CHANGED
|
@@ -209,6 +209,7 @@ export function reset(proto, globals) {
|
|
|
209
209
|
ctx.error = {
|
|
210
210
|
src: '',
|
|
211
211
|
loc: null,
|
|
212
|
+
node: null,
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
ctx.transform = {
|
|
@@ -259,15 +260,42 @@ export function reset(proto, globals) {
|
|
|
259
260
|
|
|
260
261
|
/** Throw with source location context. */
|
|
261
262
|
export function err(msg) {
|
|
263
|
+
let detail = msg
|
|
264
|
+
|
|
262
265
|
if (ctx.error.loc != null && ctx.error.src) {
|
|
263
266
|
const before = ctx.error.src.slice(0, ctx.error.loc)
|
|
264
267
|
const line = before.split('\n').length
|
|
265
268
|
const col = ctx.error.loc - before.lastIndexOf('\n')
|
|
266
269
|
const src = ctx.error.src.split('\n')[line - 1]
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
detail += `\n at line ${line}:${col}\n ${src}\n ${' '.repeat(col - 1)}^`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (ctx.func.current?.name) {
|
|
274
|
+
detail += `\n in function: ${ctx.func.current.name}`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (ctx.error.node != null) {
|
|
278
|
+
detail += `\n current AST: ${formatErrorNode(ctx.error.node)}`
|
|
271
279
|
}
|
|
272
|
-
|
|
280
|
+
|
|
281
|
+
const e = new Error(detail)
|
|
282
|
+
const stackLines = e.stack.split('\n')
|
|
283
|
+
const firstFrame = stackLines.findIndex(line => line.trimStart().startsWith('at '))
|
|
284
|
+
const frames = firstFrame >= 0 ? stackLines.slice(firstFrame) : stackLines.slice(1)
|
|
285
|
+
e.stack = `${e.name}: ${detail}\n${frames.join('\n')}`
|
|
286
|
+
throw e
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function formatErrorNode(node) {
|
|
290
|
+
const seen = new WeakSet()
|
|
291
|
+
const json = JSON.stringify(node, (_key, value) => {
|
|
292
|
+
if (typeof value === 'bigint') return `${value}n`
|
|
293
|
+
if (typeof value === 'symbol') return value.toString()
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
if (seen.has(value)) return '[Circular]'
|
|
296
|
+
seen.add(value)
|
|
297
|
+
}
|
|
298
|
+
return value
|
|
299
|
+
})
|
|
300
|
+
return json.length > 2000 ? `${json.slice(0, 2000)}...` : json
|
|
273
301
|
}
|
package/src/emit.js
CHANGED
|
@@ -23,7 +23,13 @@
|
|
|
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, staticPropertyKey } from './analyze.js'
|
|
26
|
+
import { T, VAL, nonNegIntLiteral, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
|
|
27
|
+
import {
|
|
28
|
+
isReassigned, hasOwnContinue, hasOwnBreakOrContinue, containsNestedClosure,
|
|
29
|
+
containsNestedLoop, nestedSmallLoopBudget, containsDeclOf, cloneWithSubst,
|
|
30
|
+
containsKnownTypedArrayIndex, smallConstForTripCount, isTerminator,
|
|
31
|
+
MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
|
|
32
|
+
} from './ast.js'
|
|
27
33
|
import {
|
|
28
34
|
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
29
35
|
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
@@ -168,11 +174,6 @@ function stringLiteral(node) {
|
|
|
168
174
|
return null
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
function nonNegIntLiteral(node) {
|
|
172
|
-
const n = intLiteralValue(node)
|
|
173
|
-
return n != null && n >= 0 ? n : null
|
|
174
|
-
}
|
|
175
|
-
|
|
176
177
|
function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
177
178
|
const leftLit = stringLiteral(a)
|
|
178
179
|
const rightLit = stringLiteral(b)
|
|
@@ -269,142 +270,6 @@ function extractRefinements(cond, out, sense = true) {
|
|
|
269
270
|
return out
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
/** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
|
|
273
|
-
* Conservative over-reject: if unsure, treat as written.
|
|
274
|
-
* `let`/`const` declarations are NOT reassignments — only the initializer expressions
|
|
275
|
-
* inside them are scanned. (Treating `let g = ...` as a write of `g` would defeat A3.) */
|
|
276
|
-
export function isReassigned(body, name) {
|
|
277
|
-
if (!Array.isArray(body)) return false
|
|
278
|
-
const op = body[0]
|
|
279
|
-
if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%='
|
|
280
|
-
|| op === '&=' || op === '|=' || op === '^=' || op === '<<=' || op === '>>=' || op === '>>>='
|
|
281
|
-
|| op === '||=' || op === '&&=' || op === '??=') {
|
|
282
|
-
if (body[1] === name) return true
|
|
283
|
-
}
|
|
284
|
-
if ((op === '++' || op === '--') && body[1] === name) return true
|
|
285
|
-
if (op === 'let' || op === 'const') {
|
|
286
|
-
// Each decl item is either a bare name (string) or `['=', pattern, init]`.
|
|
287
|
-
// Only the init expression can contain real reassignments — recurse into it only.
|
|
288
|
-
for (let i = 1; i < body.length; i++) {
|
|
289
|
-
const d = body[i]
|
|
290
|
-
if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
|
|
291
|
-
}
|
|
292
|
-
return false
|
|
293
|
-
}
|
|
294
|
-
for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
|
|
295
|
-
return false
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Does `body` contain a `continue` that targets THIS loop?
|
|
299
|
-
* A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
|
|
300
|
-
function hasOwnContinue(body) {
|
|
301
|
-
if (!Array.isArray(body)) return false
|
|
302
|
-
const op = body[0]
|
|
303
|
-
if (op === 'continue') return true
|
|
304
|
-
if (op === 'for' || op === 'while' || op === 'do') return false
|
|
305
|
-
for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
|
|
306
|
-
return false
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function hasOwnBreakOrContinue(body) {
|
|
310
|
-
if (!Array.isArray(body)) return false
|
|
311
|
-
const op = body[0]
|
|
312
|
-
if (op === 'break' || op === 'continue') return true
|
|
313
|
-
if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
|
|
314
|
-
for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
|
|
315
|
-
return false
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function containsNestedClosure(body) {
|
|
319
|
-
if (!Array.isArray(body)) return false
|
|
320
|
-
if (body[0] === '=>') return true
|
|
321
|
-
for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
|
|
322
|
-
return false
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function containsNestedLoop(body) {
|
|
326
|
-
if (!Array.isArray(body)) return false
|
|
327
|
-
const op = body[0]
|
|
328
|
-
if (op === 'for' || op === 'while' || op === 'do') return true
|
|
329
|
-
if (op === '=>') return false
|
|
330
|
-
for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
|
|
331
|
-
return false
|
|
332
|
-
}
|
|
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
|
-
|
|
347
|
-
function containsDeclOf(body, name) {
|
|
348
|
-
if (!Array.isArray(body)) return false
|
|
349
|
-
const op = body[0]
|
|
350
|
-
if (op === '=>') return false
|
|
351
|
-
if (op === 'let' || op === 'const') {
|
|
352
|
-
for (let i = 1; i < body.length; i++) {
|
|
353
|
-
const d = body[i]
|
|
354
|
-
if (d === name) return true
|
|
355
|
-
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
|
|
359
|
-
return false
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function intLiteralValue(expr) {
|
|
363
|
-
let v = null
|
|
364
|
-
if (typeof expr === 'number') v = expr
|
|
365
|
-
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
366
|
-
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
367
|
-
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
368
|
-
return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function cloneWithSubst(node, name, value) {
|
|
372
|
-
if (node === name) return [null, value]
|
|
373
|
-
if (!Array.isArray(node)) return node
|
|
374
|
-
if (node[0] === '=>') return node
|
|
375
|
-
return node.map(x => cloneWithSubst(x, name, value))
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const MAX_SMALL_FOR_UNROLL = 8
|
|
379
|
-
const MAX_NESTED_FOR_UNROLL = 64
|
|
380
|
-
|
|
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) {
|
|
390
|
-
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
391
|
-
const decl = init[1]
|
|
392
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
393
|
-
const name = decl[1]
|
|
394
|
-
const start = intLiteralValue(decl[2])
|
|
395
|
-
if (start !== 0) return null
|
|
396
|
-
|
|
397
|
-
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
398
|
-
const end = intLiteralValue(cond[2])
|
|
399
|
-
if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
|
|
400
|
-
|
|
401
|
-
const stepOk = Array.isArray(step) && (
|
|
402
|
-
(step[0] === '++' && step[1] === name) ||
|
|
403
|
-
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
404
|
-
)
|
|
405
|
-
return stepOk ? end : null
|
|
406
|
-
}
|
|
407
|
-
|
|
408
273
|
function unrollSmallConstFor(init, cond, step, body) {
|
|
409
274
|
const end = smallConstForTripCount(init, cond, step)
|
|
410
275
|
if (end == null) return null
|
|
@@ -422,21 +287,23 @@ function unrollSmallConstFor(init, cond, step, body) {
|
|
|
422
287
|
return out
|
|
423
288
|
}
|
|
424
289
|
|
|
425
|
-
|
|
426
|
-
* Used for early-return refinement: after `if (!guard) return`, `guard` holds for the rest. */
|
|
427
|
-
function isTerminator(body) {
|
|
290
|
+
function canThrow(body, seen = new Set()) {
|
|
428
291
|
if (!Array.isArray(body)) return false
|
|
429
292
|
const op = body[0]
|
|
430
|
-
if (op === '
|
|
431
|
-
|
|
432
|
-
if (op === '
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
293
|
+
if (op === 'throw') return true
|
|
294
|
+
if (op === '=>') return false
|
|
295
|
+
if (op === '()') {
|
|
296
|
+
const callee = body[1]
|
|
297
|
+
if (typeof callee === 'string') {
|
|
298
|
+
const bodyName = ctx.func.directClosures?.get(callee)
|
|
299
|
+
const f = ctx.func.map?.get(bodyName || callee)
|
|
300
|
+
if (f?.body && !f.raw && !seen.has(f.name)) {
|
|
301
|
+
seen.add(f.name)
|
|
302
|
+
if (canThrow(f.body, seen)) return true
|
|
303
|
+
}
|
|
437
304
|
}
|
|
438
|
-
return false
|
|
439
305
|
}
|
|
306
|
+
for (let i = 1; i < body.length; i++) if (canThrow(body[i], seen)) return true
|
|
440
307
|
return false
|
|
441
308
|
}
|
|
442
309
|
|
|
@@ -867,6 +734,20 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
867
734
|
inc('__str_cmp')
|
|
868
735
|
return typed([`i32.${i32op}`, ['call', '$__str_cmp', asI64(va), asI64(vb)], ['i32.const', 0]], 'i32')
|
|
869
736
|
}
|
|
737
|
+
if (vta === VAL.DATE || vtb === VAL.DATE) {
|
|
738
|
+
const dateNum = (node, v, vt) => {
|
|
739
|
+
if (vt !== VAL.DATE) return toNumF64(node, v)
|
|
740
|
+
const ptr = v.ptrKind === VAL.DATE
|
|
741
|
+
? v
|
|
742
|
+
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
743
|
+
return typed(['f64.load', ptr], 'f64')
|
|
744
|
+
}
|
|
745
|
+
return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
|
|
746
|
+
}
|
|
747
|
+
if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
|
|
748
|
+
return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
|
|
749
|
+
if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
|
|
750
|
+
return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
|
|
870
751
|
const ai = intConstValue(a), bi = intConstValue(b)
|
|
871
752
|
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
872
753
|
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
@@ -874,6 +755,33 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
874
755
|
? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
875
756
|
}
|
|
876
757
|
|
|
758
|
+
function needsRelationalToNumber(expr, vt) {
|
|
759
|
+
if (vt === VAL.STRING) return true
|
|
760
|
+
if (vt != null) return false
|
|
761
|
+
return mayReadBoxedValue(expr)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function needsLooseEqualityToNumber(expr, vt) {
|
|
765
|
+
if (vt === VAL.STRING) return true
|
|
766
|
+
if (vt != null) return false
|
|
767
|
+
return mayReadBoxedValue(expr)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function looseNumberEq(numIR, otherNode, otherIR, negate = false) {
|
|
771
|
+
const t = temp('eq')
|
|
772
|
+
const other = typed(['local.get', `$${t}`], 'f64')
|
|
773
|
+
const cmp = ['f64.eq', asF64(numIR), toNumF64(otherNode, other)]
|
|
774
|
+
return typed(['block', ['result', 'i32'],
|
|
775
|
+
['local.set', `$${t}`, asF64(otherIR)],
|
|
776
|
+
['if', ['result', 'i32'], isNullish(other),
|
|
777
|
+
['then', ['i32.const', negate ? 1 : 0]],
|
|
778
|
+
['else', negate ? ['i32.eqz', cmp] : cmp]]], 'i32')
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function mayReadBoxedValue(expr) {
|
|
782
|
+
return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
|
|
783
|
+
}
|
|
784
|
+
|
|
877
785
|
function intConstValue(expr) {
|
|
878
786
|
if (typeof expr === 'number' && Number.isInteger(expr)) return expr
|
|
879
787
|
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
|
|
@@ -942,10 +850,7 @@ export const emitter = {
|
|
|
942
850
|
';': (...args) => {
|
|
943
851
|
const out = []
|
|
944
852
|
for (const a of args) {
|
|
945
|
-
|
|
946
|
-
if (r == null) continue
|
|
947
|
-
out.push(...flat(r))
|
|
948
|
-
if (r?.type && r.type !== 'void') out.push('drop')
|
|
853
|
+
out.push(...emitFlat(a))
|
|
949
854
|
}
|
|
950
855
|
return out
|
|
951
856
|
},
|
|
@@ -989,6 +894,8 @@ export const emitter = {
|
|
|
989
894
|
},
|
|
990
895
|
|
|
991
896
|
'catch': (body, errName, handler) => {
|
|
897
|
+
if (!canThrow(body)) return emitFlat(body)
|
|
898
|
+
|
|
992
899
|
ctx.runtime.throws = true
|
|
993
900
|
const id = ctx.func.uniq++
|
|
994
901
|
ctx.func.locals.set(errName, 'f64')
|
|
@@ -1007,6 +914,14 @@ export const emitter = {
|
|
|
1007
914
|
},
|
|
1008
915
|
|
|
1009
916
|
'finally': (body, cleanup) => {
|
|
917
|
+
if (!canThrow(body)) {
|
|
918
|
+
const parentStack = ctx.func.finallyStack || []
|
|
919
|
+
const activeStack = parentStack.concat([cleanup])
|
|
920
|
+
const bodyIR = withFinallyStack(activeStack, () => emitFlat(body))
|
|
921
|
+
const cleanupIR = isTerminator(body) ? [] : withFinallyStack(parentStack, () => emitFlat(cleanup))
|
|
922
|
+
return [...bodyIR, ...cleanupIR]
|
|
923
|
+
}
|
|
924
|
+
|
|
1010
925
|
ctx.runtime.throws = true
|
|
1011
926
|
const id = ctx.func.uniq++
|
|
1012
927
|
const errLocal = temp('err')
|
|
@@ -1036,13 +951,14 @@ export const emitter = {
|
|
|
1036
951
|
|
|
1037
952
|
'return': expr => {
|
|
1038
953
|
const finalizers = emitFinalizers()
|
|
954
|
+
const finalizerBlock = () => [['block', ...finalizers]]
|
|
1039
955
|
if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
|
|
1040
956
|
const vals = expr.slice(1).map(e => asF64(emit(e)))
|
|
1041
957
|
if (finalizers.length === 0) return typed(['return', ...vals], 'void')
|
|
1042
958
|
const names = vals.map(() => temp('ret'))
|
|
1043
959
|
return [
|
|
1044
960
|
...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
|
|
1045
|
-
...
|
|
961
|
+
...finalizerBlock(),
|
|
1046
962
|
typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
|
|
1047
963
|
]
|
|
1048
964
|
}
|
|
@@ -1058,7 +974,7 @@ export const emitter = {
|
|
|
1058
974
|
const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
|
|
1059
975
|
return [
|
|
1060
976
|
['local.set', `$${name}`, ir],
|
|
1061
|
-
...
|
|
977
|
+
...finalizerBlock(),
|
|
1062
978
|
typed(['return', ['local.get', `$${name}`]], 'void'),
|
|
1063
979
|
]
|
|
1064
980
|
}
|
|
@@ -1330,6 +1246,8 @@ export const emitter = {
|
|
|
1330
1246
|
|
|
1331
1247
|
// Compound assignments: read-modify-write with type coercion
|
|
1332
1248
|
'+=': (name, val) => {
|
|
1249
|
+
// Complex LHS (obj.prop, arr[i]) → desugar to side-effect-safe `name = name + val`
|
|
1250
|
+
if (typeof name !== 'string') return emit(['=', name, ['+', name, val]])
|
|
1333
1251
|
// String concatenation: desugar to name = name + val (+ handler knows about strings).
|
|
1334
1252
|
// Also desugar when either side has unknown type — the `+` operator picks runtime
|
|
1335
1253
|
// string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
|
|
@@ -1342,17 +1260,24 @@ export const emitter = {
|
|
|
1342
1260
|
},
|
|
1343
1261
|
...Object.fromEntries([
|
|
1344
1262
|
['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
|
|
1345
|
-
].map(([op, fn]) => [op, (name, val) =>
|
|
1346
|
-
(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1263
|
+
].map(([op, fn]) => [op, (name, val) => {
|
|
1264
|
+
if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
|
|
1265
|
+
return compoundAssign(name, val,
|
|
1266
|
+
(a, b) => typed([`f64.${fn}`, a, b], 'f64'),
|
|
1267
|
+
fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
|
|
1268
|
+
)
|
|
1269
|
+
}])),
|
|
1270
|
+
'%=': (name, val) => {
|
|
1271
|
+
if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
|
|
1272
|
+
return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
|
|
1273
|
+
},
|
|
1350
1274
|
|
|
1351
1275
|
// Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
|
|
1352
1276
|
...Object.fromEntries([
|
|
1353
1277
|
['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
|
|
1354
1278
|
['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
|
|
1355
1279
|
].map(([op, fn]) => [op, (name, val) => {
|
|
1280
|
+
if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
|
|
1356
1281
|
if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
|
|
1357
1282
|
const void_ = _expect === 'void'
|
|
1358
1283
|
const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
|
|
@@ -1542,6 +1467,8 @@ export const emitter = {
|
|
|
1542
1467
|
// Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
|
|
1543
1468
|
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1544
1469
|
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1470
|
+
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
|
|
1471
|
+
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
|
|
1545
1472
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
1546
1473
|
// Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
|
|
1547
1474
|
// JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
|
|
@@ -1569,6 +1496,8 @@ export const emitter = {
|
|
|
1569
1496
|
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
|
|
1570
1497
|
const vta = resolveValType(a, valTypeOf, lookupValType)
|
|
1571
1498
|
const vtb = resolveValType(b, valTypeOf, lookupValType)
|
|
1499
|
+
if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
|
|
1500
|
+
if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
|
|
1572
1501
|
if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
1573
1502
|
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1574
1503
|
return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
@@ -2490,7 +2419,10 @@ function liftOptionalChain(node) {
|
|
|
2490
2419
|
*/
|
|
2491
2420
|
export function emit(node, expect) {
|
|
2492
2421
|
_expect = expect || null
|
|
2493
|
-
if (Array.isArray(node)
|
|
2422
|
+
if (Array.isArray(node)) {
|
|
2423
|
+
ctx.error.node = node
|
|
2424
|
+
if (node.loc != null) ctx.error.loc = node.loc
|
|
2425
|
+
}
|
|
2494
2426
|
if (node == null) return null
|
|
2495
2427
|
if (node === true) return typed(['i32.const', 1], 'i32')
|
|
2496
2428
|
if (node === false) return typed(['i32.const', 0], 'i32')
|
package/src/host.js
CHANGED
|
@@ -607,6 +607,18 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
607
607
|
imports.env.now = (clock) =>
|
|
608
608
|
clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
|
|
609
609
|
}
|
|
610
|
+
if (envFns.has('parseFloat') && !imports.env.parseFloat) {
|
|
611
|
+
imports.env.parseFloat = (valBig) => {
|
|
612
|
+
const s = state.mem.read(i64ToF64(valBig))
|
|
613
|
+
return parseFloat(s)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (envFns.has('parseInt') && !imports.env.parseInt) {
|
|
617
|
+
imports.env.parseInt = (valBig, radix) => {
|
|
618
|
+
const s = state.mem.read(i64ToF64(valBig))
|
|
619
|
+
return parseInt(s, radix || undefined)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
610
622
|
// host: 'js' timer wiring. Wasm calls env.setTimeout/clearTimeout; we drive
|
|
611
623
|
// callbacks back via the exported __invoke_closure trampoline (state.invoke).
|
|
612
624
|
// Each id maps to a cancel thunk so set/clear share state without tagging.
|
package/src/ir.js
CHANGED
|
@@ -347,6 +347,12 @@ export function toNumF64(node, v) {
|
|
|
347
347
|
if (v.type === 'i32' || isLit(v)) return asF64(v)
|
|
348
348
|
const vt = keyValType(node)
|
|
349
349
|
if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
|
|
350
|
+
if (vt === VAL.DATE) {
|
|
351
|
+
const ptr = v.ptrKind === VAL.DATE
|
|
352
|
+
? v
|
|
353
|
+
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
354
|
+
return typed(['f64.load', ptr], 'f64')
|
|
355
|
+
}
|
|
350
356
|
// intCertain locals: every reachable def is integer-valued, so the binding
|
|
351
357
|
// never carries a NaN-boxed pointer — skip the __to_num wrapper.
|
|
352
358
|
if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
|
|
@@ -682,6 +688,19 @@ export const flat = ir => {
|
|
|
682
688
|
* Reconstruct arguments with spreads inserted at correct positions.
|
|
683
689
|
* Example: normal=[a, c], spreads=[{pos:1, expr:arr}] → [a, __spread(arr), c]
|
|
684
690
|
*/
|
|
691
|
+
/** Find the index of the first body-content child in a (func ...) WAT node.
|
|
692
|
+
* Skips $name, (export …), (import …), (type …), (param …), (result …), (local …). */
|
|
693
|
+
export function findBodyStart(fn) {
|
|
694
|
+
for (let i = 2; i < fn.length; i++) {
|
|
695
|
+
const c = fn[i]
|
|
696
|
+
if (!Array.isArray(c)) continue
|
|
697
|
+
if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' ||
|
|
698
|
+
c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
|
|
699
|
+
return i
|
|
700
|
+
}
|
|
701
|
+
return fn.length
|
|
702
|
+
}
|
|
703
|
+
|
|
685
704
|
export function reconstructArgsWithSpreads(normal, spreads) {
|
|
686
705
|
const combined = []
|
|
687
706
|
let normalIdx = 0
|