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/plan.js
CHANGED
|
@@ -24,17 +24,39 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { ctx } from './ctx.js'
|
|
27
|
-
import { T, VAL, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts } from './analyze.js'
|
|
27
|
+
import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts, extractParams } from './analyze.js'
|
|
28
28
|
import { MAX_CLOSURE_ARITY } from './ir.js'
|
|
29
29
|
import narrowSignatures, { specializeBimorphicTyped, refineDynKeys } from './narrow.js'
|
|
30
30
|
|
|
31
|
-
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
32
31
|
const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
|
|
33
32
|
const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// Fixed-size typed arrays eligible for scalar replacement, mapped to the element
|
|
34
|
+
// store-coercion kind ('' = none, i.e. Float64Array's f64-identity). Excluded:
|
|
35
|
+
// Float32Array — store coercion is `Math.fround`, needs the `math` module pulled at plan time
|
|
36
|
+
// Uint32Array — element range [0, 2^32) exceeds what jz keeps as f64 after `x >>> 0` (i32-narrowed)
|
|
37
|
+
// Uint8ClampedArray — round-half-to-even clamp
|
|
38
|
+
// Coerced (truthy) types are scalarized only when fully local — any escape (passed
|
|
39
|
+
// to a call, `.buffer`/view aliasing, etc.) keeps the real allocation, since the
|
|
40
|
+
// mirror/fence path can't track writes through an alias that outlives the fence.
|
|
41
|
+
const SCALAR_TYPED_COERCE = {
|
|
42
|
+
'new.Float64Array': '',
|
|
43
|
+
'new.Int32Array': 'i32',
|
|
44
|
+
'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
|
|
45
|
+
'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
|
|
46
|
+
}
|
|
47
|
+
// AST for the store coercion a typed-array element does on write (`arr[i] = v`).
|
|
48
|
+
// All expressible with operators jz already lowers post-plan (no module deps).
|
|
49
|
+
const coerceAST = (kind, expr) => {
|
|
50
|
+
if (kind === 'i32') return ['|', expr, [null, 0]]
|
|
51
|
+
if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
|
|
52
|
+
if (kind === 'u16') return ['&', expr, [null, 0xffff]]
|
|
53
|
+
if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
|
|
54
|
+
if (kind === 'u8') return ['&', expr, [null, 0xff]]
|
|
55
|
+
return expr
|
|
56
|
+
}
|
|
57
|
+
const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 32
|
|
58
|
+
const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
|
|
59
|
+
const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
|
|
38
60
|
|
|
39
61
|
const isSeq = node => Array.isArray(node) && node[0] === ';'
|
|
40
62
|
const blockStmts = body => {
|
|
@@ -165,12 +187,14 @@ const scalarObjectProps = (expr) => {
|
|
|
165
187
|
return props
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
const
|
|
169
|
-
|
|
190
|
+
const fixedScalarTypedArray = (expr) => {
|
|
191
|
+
const ctor = typedElemCtor(expr)
|
|
192
|
+
if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
|
|
170
193
|
const args = callArgs(expr)
|
|
171
194
|
if (!args || args.length !== 1) return null
|
|
172
195
|
const len = constIntExpr(args[0])
|
|
173
|
-
return len != null && len >= 0 && len <=
|
|
196
|
+
return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
|
|
197
|
+
? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
@@ -254,7 +278,10 @@ const typedArraySlotIndex = (node, len) => {
|
|
|
254
278
|
return idx != null && idx >= 0 && idx < len ? idx : null
|
|
255
279
|
}
|
|
256
280
|
|
|
257
|
-
|
|
281
|
+
// `coerce` truthy ⇒ the array's element type truncates on store (Int*/Uint* views),
|
|
282
|
+
// so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
|
|
283
|
+
// reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
|
|
284
|
+
const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
|
|
258
285
|
if (typeof node === 'string') return node !== name
|
|
259
286
|
if (!Array.isArray(node)) return true
|
|
260
287
|
const op = node[0]
|
|
@@ -262,17 +289,18 @@ const safeScalarTypedArrayUse = (node, name, len) => {
|
|
|
262
289
|
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
263
290
|
if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
|
|
264
291
|
if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
|
|
265
|
-
return typedArraySlotIndex(node[1][2], len) != null
|
|
292
|
+
return !coerce && typedArraySlotIndex(node[1][2], len) != null
|
|
266
293
|
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
267
294
|
if (node[1] === name) return false
|
|
268
295
|
if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
296
|
+
if (coerce && op !== '=') return false
|
|
269
297
|
if (typedArraySlotIndex(node[1][2], len) == null) return false
|
|
270
|
-
for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
|
|
298
|
+
for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
271
299
|
return true
|
|
272
300
|
}
|
|
273
301
|
}
|
|
274
302
|
if (op === '...' && node[1] === name) return false
|
|
275
|
-
for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
|
|
303
|
+
for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
276
304
|
return true
|
|
277
305
|
}
|
|
278
306
|
|
|
@@ -297,8 +325,11 @@ const rewriteScalarTypedArrayUses = (node, arrays) => {
|
|
|
297
325
|
return slot ? [op, slot] : node
|
|
298
326
|
}
|
|
299
327
|
if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
|
|
300
|
-
const
|
|
301
|
-
|
|
328
|
+
const entry = arrays.get(node[1][1])
|
|
329
|
+
const slot = slotFor(node[1][2], entry)
|
|
330
|
+
if (!slot) return node
|
|
331
|
+
const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
|
|
332
|
+
return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
|
|
302
333
|
}
|
|
303
334
|
return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
|
|
304
335
|
}
|
|
@@ -329,6 +360,24 @@ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
|
|
|
329
360
|
return out
|
|
330
361
|
}
|
|
331
362
|
|
|
363
|
+
const hasScalarTypedArrayRead = (node, name) => {
|
|
364
|
+
if (!Array.isArray(node)) return false
|
|
365
|
+
const op = node[0]
|
|
366
|
+
const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
|
|
367
|
+
if ((op === '++' || op === '--') && isTarget(node[1])) return true
|
|
368
|
+
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
369
|
+
if (isTarget(node[1])) {
|
|
370
|
+
if (op !== '=') return true
|
|
371
|
+
for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (op === '[]' && node[1] === name) return true
|
|
376
|
+
if (op === '=>') return false
|
|
377
|
+
for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
332
381
|
const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
333
382
|
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
334
383
|
let changed = false
|
|
@@ -345,26 +394,27 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
|
345
394
|
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
346
395
|
const decl = stmt[1]
|
|
347
396
|
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
397
|
+
const fixed = fixedScalarTypedArray(decl[2])
|
|
398
|
+
if (fixed == null) continue
|
|
399
|
+
const { len, coerce } = fixed
|
|
350
400
|
let hasSafeUse = false, hasUnsafeUse = false
|
|
351
401
|
for (let j = 0; j < stmts.length; j++) {
|
|
352
402
|
if (j === i) continue
|
|
353
403
|
if (!mentionsName(stmts[j], decl[1])) continue
|
|
354
|
-
const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len)
|
|
404
|
+
const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
|
|
355
405
|
hasSafeUse ||= safe
|
|
356
406
|
hasUnsafeUse ||= !safe
|
|
357
407
|
}
|
|
358
|
-
if (!hasSafeUse
|
|
359
|
-
if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, mirrored: false })
|
|
360
|
-
else mirrored.set(decl[1], { index: i, len, mirrored: true })
|
|
408
|
+
if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
|
|
409
|
+
if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
|
|
410
|
+
else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
|
|
361
411
|
}
|
|
362
412
|
if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
363
413
|
|
|
364
414
|
const arrays = new Map()
|
|
365
415
|
for (const [name, c] of [...candidates, ...mirrored]) {
|
|
366
416
|
const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
|
|
367
|
-
arrays.set(name, { len: c.len, slots, mirrored: c.mirrored })
|
|
417
|
+
arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
|
|
368
418
|
}
|
|
369
419
|
|
|
370
420
|
const out = []
|
|
@@ -386,7 +436,7 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
|
386
436
|
}
|
|
387
437
|
const unsafe = []
|
|
388
438
|
for (const [name, arr] of arrays) {
|
|
389
|
-
if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len)) unsafe.push([name, arr])
|
|
439
|
+
if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
|
|
390
440
|
}
|
|
391
441
|
if (unsafe.length) {
|
|
392
442
|
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
|
|
@@ -446,7 +496,7 @@ function smallScalarTypedForTrip(init, cond, step) {
|
|
|
446
496
|
if (constIntExpr(decl[2]) !== 0) return null
|
|
447
497
|
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
448
498
|
const end = constIntExpr(cond[2])
|
|
449
|
-
if (end == null || end < 0 || end >
|
|
499
|
+
if (end == null || end < 0 || end > maxScalarTypedLoopUnroll()) return null
|
|
450
500
|
const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
|
|
451
501
|
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
|
|
452
502
|
return stepOk ? { name, end } : null
|
|
@@ -482,7 +532,7 @@ const unrollTypedArrayLoops = (node, names) => {
|
|
|
482
532
|
}
|
|
483
533
|
if (node[0] === 'for') {
|
|
484
534
|
const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
|
|
485
|
-
if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <=
|
|
535
|
+
if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
|
|
486
536
|
!hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
|
|
487
537
|
const out = [';']
|
|
488
538
|
for (let i = 0; i < trip.end; i++) {
|
|
@@ -511,8 +561,8 @@ const fixedTypedArraysInBody = (body) => {
|
|
|
511
561
|
for (let i = 1; i < node.length; i++) {
|
|
512
562
|
const d = node[i]
|
|
513
563
|
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
564
|
+
const fixed = fixedScalarTypedArray(d[2])
|
|
565
|
+
if (fixed != null) out.set(d[1], fixed)
|
|
516
566
|
}
|
|
517
567
|
}
|
|
518
568
|
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
@@ -528,15 +578,15 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
|
|
|
528
578
|
const cands = new Map()
|
|
529
579
|
for (let i = 0; i < params.length; i++) {
|
|
530
580
|
const pname = params[i].name
|
|
531
|
-
let len = null, ok = true
|
|
581
|
+
let len = null, coerce = null, ok = true
|
|
532
582
|
for (const site of sites) {
|
|
533
583
|
const arg = site.argList[i]
|
|
534
584
|
const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
|
|
535
585
|
if (!fixed) { ok = false; break }
|
|
536
|
-
if (len == null) len = fixed.len
|
|
537
|
-
else if (len !== fixed.len) { ok = false; break }
|
|
586
|
+
if (len == null) { len = fixed.len; coerce = fixed.coerce }
|
|
587
|
+
else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
|
|
538
588
|
}
|
|
539
|
-
if (ok && len != null) cands.set(pname, { len })
|
|
589
|
+
if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
|
|
540
590
|
}
|
|
541
591
|
if (!cands.size) return cands
|
|
542
592
|
for (const site of sites) {
|
|
@@ -552,12 +602,14 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
|
|
|
552
602
|
}
|
|
553
603
|
|
|
554
604
|
const scalarizeTypedArrayParams = (func, paramCands) => {
|
|
555
|
-
for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len)) paramCands.delete(name)
|
|
605
|
+
for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
|
|
606
|
+
for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
|
|
556
607
|
if (!paramCands.size) return { body: func.body, changed: false }
|
|
557
608
|
const arrays = new Map()
|
|
558
609
|
for (const [name, c] of paramCands) {
|
|
559
610
|
arrays.set(name, {
|
|
560
611
|
len: c.len,
|
|
612
|
+
coerce: c.coerce,
|
|
561
613
|
slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
|
|
562
614
|
})
|
|
563
615
|
}
|
|
@@ -920,6 +972,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
920
972
|
if (cfg && cfg.sourceInline === false) return false
|
|
921
973
|
|
|
922
974
|
const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
|
|
975
|
+
const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
|
|
923
976
|
const sitesByCallee = new Map()
|
|
924
977
|
for (const cs of programFacts.callSites) {
|
|
925
978
|
const list = sitesByCallee.get(cs.callee)
|
|
@@ -942,8 +995,22 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
942
995
|
return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
|
|
943
996
|
}))
|
|
944
997
|
}
|
|
945
|
-
const
|
|
946
|
-
|
|
998
|
+
const hasFullyFixedTypedArraySites = (func, sites) => {
|
|
999
|
+
const params = func.sig?.params || []
|
|
1000
|
+
if (!sites?.length) return false
|
|
1001
|
+
let sawTypedArg = false
|
|
1002
|
+
for (const site of sites) {
|
|
1003
|
+
const typed = typedByFunc.get(site.callerFunc)
|
|
1004
|
+
const fixed = fixedByFunc.get(site.callerFunc)
|
|
1005
|
+
for (let i = 0; i < params.length; i++) {
|
|
1006
|
+
const arg = site.argList[i]
|
|
1007
|
+
if (typeof arg !== 'string' || !typed?.has(arg)) continue
|
|
1008
|
+
sawTypedArg = true
|
|
1009
|
+
if (!fixed?.has(arg)) return false
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return sawTypedArg
|
|
1013
|
+
}
|
|
947
1014
|
|
|
948
1015
|
const candidates = new Map()
|
|
949
1016
|
for (const func of ctx.func.list) {
|
|
@@ -951,6 +1018,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
951
1018
|
if (func.defaults && Object.keys(func.defaults).length) continue
|
|
952
1019
|
const sites = sitesByCallee.get(func.name)
|
|
953
1020
|
const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
|
|
1021
|
+
const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
|
|
954
1022
|
if (!sites || sites.length < 1 || (!fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
|
|
955
1023
|
const stmts = blockStmts(func.body)
|
|
956
1024
|
// Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
|
|
@@ -980,7 +1048,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
980
1048
|
// loop carries most of the cost. Inlining them into a host that V8 can't
|
|
981
1049
|
// tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
|
|
982
1050
|
// Keep them as standalone functions so V8 wasm tier-up can warm them.
|
|
983
|
-
if (loopDepth(func.body, 0) >= 2 && !
|
|
1051
|
+
if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
|
|
984
1052
|
// Factory functions that allocate pointers (`new TypedArray`, `new Array`,
|
|
985
1053
|
// object/array literals returned) break downstream pointer-ABI specialization
|
|
986
1054
|
// when inlined: narrow.js can't trace the post-inline alias chain back to a
|
|
@@ -1036,6 +1104,146 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1036
1104
|
return changed
|
|
1037
1105
|
}
|
|
1038
1106
|
|
|
1107
|
+
// === Inline non-escaping local lambdas ===
|
|
1108
|
+
// `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
|
|
1109
|
+
// site. A non-escaping lambda's captured free vars are still in lexical scope at
|
|
1110
|
+
// the call site, so splicing the body in place preserves capture-by-reference
|
|
1111
|
+
// semantics while eliminating the closure object (no env pointer, no NaN-box, no
|
|
1112
|
+
// call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
|
|
1113
|
+
|
|
1114
|
+
// True iff `name` appears textually anywhere in `node` (descending into nested
|
|
1115
|
+
// arrows; `.prop` / `:key` positions are literal names, not refs — skipped to
|
|
1116
|
+
// match cloneWithSubst's structure).
|
|
1117
|
+
const referencesName = (node, name) => {
|
|
1118
|
+
if (typeof node === 'string') return node === name
|
|
1119
|
+
if (!Array.isArray(node)) return false
|
|
1120
|
+
const op = node[0]
|
|
1121
|
+
if (op === 'str') return false
|
|
1122
|
+
if (op === '.' || op === '?.') return referencesName(node[1], name)
|
|
1123
|
+
if (op === ':') return referencesName(node[2], name)
|
|
1124
|
+
for (let i = 1; i < node.length; i++) if (referencesName(node[i], name)) return true
|
|
1125
|
+
return false
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// True iff every textual reference to `name` in `node` is the callee of a
|
|
1129
|
+
// `name(...)` call (i.e. the binding never escapes — never read as a value,
|
|
1130
|
+
// reassigned, captured by a nested lambda, or shadowed).
|
|
1131
|
+
const onlyCalledNotReferenced = (node, name) => {
|
|
1132
|
+
if (typeof node === 'string') return node !== name
|
|
1133
|
+
if (!Array.isArray(node)) return true
|
|
1134
|
+
const op = node[0]
|
|
1135
|
+
if (op === 'str') return true
|
|
1136
|
+
// A nested lambda touching `name` at all (capture or shadowing param) → bail.
|
|
1137
|
+
if (op === '=>') return !referencesName(node[1], name) && !referencesName(node[2], name)
|
|
1138
|
+
if (op === '()' && node[1] === name) {
|
|
1139
|
+
for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
1140
|
+
return true
|
|
1141
|
+
}
|
|
1142
|
+
if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
|
|
1143
|
+
if (op === ':') return onlyCalledNotReferenced(node[2], name)
|
|
1144
|
+
for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
1145
|
+
return true
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const bodyStmtList = body =>
|
|
1149
|
+
Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
|
|
1150
|
+
: Array.isArray(body) && body[0] === ';' ? body.slice(1)
|
|
1151
|
+
: body == null ? [] : [body]
|
|
1152
|
+
|
|
1153
|
+
const removeStmts = (body, set) => {
|
|
1154
|
+
if (!Array.isArray(body)) return set.has(body) ? null : body
|
|
1155
|
+
if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
|
|
1156
|
+
if (body[0] === ';') {
|
|
1157
|
+
const kept = body.slice(1).filter(s => !set.has(s))
|
|
1158
|
+
return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
|
|
1159
|
+
}
|
|
1160
|
+
return set.has(body) ? null : body
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
|
|
1164
|
+
// `return` (trailing, if a block), no throw/break/continue, no param mutation,
|
|
1165
|
+
// no nested lambda.
|
|
1166
|
+
const inlinableLambdaBody = (abody, params) => {
|
|
1167
|
+
if (scanBody(abody, n => n[0] === '=>')) return false
|
|
1168
|
+
if (scanBody(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
|
|
1169
|
+
let returns = 0
|
|
1170
|
+
scanBody(abody, n => { if (n[0] === 'return') returns++; return false })
|
|
1171
|
+
if (returns > 1) return false
|
|
1172
|
+
if (returns === 1) {
|
|
1173
|
+
const stmts = blockStmts(abody)
|
|
1174
|
+
if (!stmts || !stmts.length) return false
|
|
1175
|
+
const last = stmts[stmts.length - 1]
|
|
1176
|
+
if (!Array.isArray(last) || last[0] !== 'return') return false
|
|
1177
|
+
}
|
|
1178
|
+
return !mutatesAny(abody, new Set(params))
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const inlineLocalLambdasInBody = (getBody, setBody) => {
|
|
1182
|
+
const body = getBody()
|
|
1183
|
+
const stmts = bodyStmtList(body)
|
|
1184
|
+
if (stmts.length < 2) return false
|
|
1185
|
+
|
|
1186
|
+
// Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
|
|
1187
|
+
const decls = new Map()
|
|
1188
|
+
for (const stmt of stmts) {
|
|
1189
|
+
if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
|
|
1190
|
+
const d = stmt[1]
|
|
1191
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1192
|
+
const arrow = d[2]
|
|
1193
|
+
if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
|
|
1194
|
+
const params = extractParams(arrow[1])
|
|
1195
|
+
if (!params.every(p => typeof p === 'string')) continue
|
|
1196
|
+
if (!inlinableLambdaBody(arrow[2], params)) continue
|
|
1197
|
+
decls.set(d[1], { stmt, arrow, params })
|
|
1198
|
+
}
|
|
1199
|
+
if (!decls.size) return false
|
|
1200
|
+
|
|
1201
|
+
// Drop any candidate whose body references another (or its own) candidate —
|
|
1202
|
+
// single-level inlining can't resolve such chains, and a still-referenced
|
|
1203
|
+
// candidate's decl can't be removed.
|
|
1204
|
+
for (let changed = true; changed;) {
|
|
1205
|
+
changed = false
|
|
1206
|
+
for (const [name, info] of decls) {
|
|
1207
|
+
if ([...decls.keys()].some(c => referencesName(info.arrow[2], c))) { decls.delete(name); changed = true }
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Every other reference to the name must be a `name(...)` call.
|
|
1211
|
+
for (const [name, info] of [...decls]) {
|
|
1212
|
+
if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
|
|
1213
|
+
}
|
|
1214
|
+
if (!decls.size) return false
|
|
1215
|
+
|
|
1216
|
+
const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
|
|
1217
|
+
const stmtCands = new Map(), exprCands = new Map()
|
|
1218
|
+
for (const [name, info] of decls)
|
|
1219
|
+
(Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
|
|
1220
|
+
|
|
1221
|
+
let out = body, didChange = false
|
|
1222
|
+
if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
|
|
1223
|
+
if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
|
|
1224
|
+
if (!didChange) return false
|
|
1225
|
+
|
|
1226
|
+
// Remove decls of candidates that are now fully consumed.
|
|
1227
|
+
const newStmts = bodyStmtList(out)
|
|
1228
|
+
const dead = new Set()
|
|
1229
|
+
for (const [name, info] of decls) {
|
|
1230
|
+
if (!newStmts.some(s => s !== info.stmt && referencesName(s, name))) dead.add(info.stmt)
|
|
1231
|
+
}
|
|
1232
|
+
if (dead.size) out = removeStmts(out, dead) ?? [';']
|
|
1233
|
+
|
|
1234
|
+
setBody(out)
|
|
1235
|
+
return true
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const inlineLocalLambdas = () => {
|
|
1239
|
+
let changed = false
|
|
1240
|
+
for (const func of ctx.func.list) {
|
|
1241
|
+
if (!func.body || func.raw) continue
|
|
1242
|
+
if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
|
|
1243
|
+
}
|
|
1244
|
+
return changed
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1039
1247
|
const restIndexExpr = (idx, restParams) => {
|
|
1040
1248
|
const k = intLit(idx)
|
|
1041
1249
|
if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
|
|
@@ -1193,11 +1401,23 @@ const materializeAutoBoxSchemas = (programFacts) => {
|
|
|
1193
1401
|
|
|
1194
1402
|
const resolveClosureWidth = (programFacts) => {
|
|
1195
1403
|
if (!ctx.closure.make) return
|
|
1196
|
-
const { hasSpread, hasRest, maxCall, maxDef } = programFacts
|
|
1404
|
+
const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
|
|
1197
1405
|
const floor = ctx.closure.floor ?? 0
|
|
1406
|
+
// A top-level function used as a first-class value gets a boundary trampoline
|
|
1407
|
+
// that forwards $__a0..$__a{arity-1} into it (emit.js). The uniform closure
|
|
1408
|
+
// ABI must therefore be at least as wide as any table-resident function's
|
|
1409
|
+
// fixed arity — maxDef only counts surviving `=>` literals, so lifted/hoisted
|
|
1410
|
+
// function definitions slip past it (their bodies are walked, their param
|
|
1411
|
+
// lists aren't). Without this, e.g. an arity-3 function used only via a
|
|
1412
|
+
// 1-arg indirect call emits `(local.get $__a2)` against a 2-param trampoline.
|
|
1413
|
+
let maxValueArity = 0
|
|
1414
|
+
if (valueUsed) for (const name of valueUsed) {
|
|
1415
|
+
const n = ctx.func.map.get(name)?.sig?.params?.length ?? 0
|
|
1416
|
+
if (n > maxValueArity) maxValueArity = n
|
|
1417
|
+
}
|
|
1198
1418
|
ctx.closure.width = (hasSpread && hasRest)
|
|
1199
1419
|
? MAX_CLOSURE_ARITY
|
|
1200
|
-
: Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), floor))
|
|
1420
|
+
: Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), maxValueArity, floor))
|
|
1201
1421
|
}
|
|
1202
1422
|
|
|
1203
1423
|
const canSkipWholeProgramNarrowing = (programFacts) =>
|
|
@@ -1214,6 +1434,7 @@ export default function plan(ast) {
|
|
|
1214
1434
|
|
|
1215
1435
|
let programFacts = collectProgramFacts(ast)
|
|
1216
1436
|
if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
|
|
1437
|
+
if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
|
|
1217
1438
|
if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
|
|
1218
1439
|
if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
|
|
1219
1440
|
if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
|
package/src/prepare.js
CHANGED
|
@@ -656,7 +656,10 @@ function expandDestruct(pattern, source, out, decls = null) {
|
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
if (Array.isArray(item) && item[0] === ':') {
|
|
659
|
-
|
|
659
|
+
const key = item[1]
|
|
660
|
+
const computedKey = Array.isArray(key) && key[0] === '[]' && key.length === 2 ? key[1] : null
|
|
661
|
+
if (computedKey) includeForArrayAccess()
|
|
662
|
+
pushPatternAssign(item[2], computedKey ? ['[]', source, computedKey] : ['.', source, key], out, decls)
|
|
660
663
|
continue
|
|
661
664
|
}
|
|
662
665
|
}
|
|
@@ -690,7 +693,20 @@ function prepDecl(op, ...inits) {
|
|
|
690
693
|
err('destructuring assignment after declaration must be a separate statement')
|
|
691
694
|
}
|
|
692
695
|
|
|
693
|
-
if (!Array.isArray(i) || i[0] !== '=') {
|
|
696
|
+
if (!Array.isArray(i) || i[0] !== '=') {
|
|
697
|
+
let declName = i
|
|
698
|
+
if (depth === 0 && typeof declName === 'string') {
|
|
699
|
+
if (ctx.module.currentPrefix) {
|
|
700
|
+
declName = `${ctx.module.currentPrefix}$${declName}`
|
|
701
|
+
ctx.scope.chain[i] = declName
|
|
702
|
+
}
|
|
703
|
+
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
704
|
+
ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
|
|
705
|
+
ctx.scope.userGlobals.add(declName)
|
|
706
|
+
}
|
|
707
|
+
rest.push(declName)
|
|
708
|
+
continue
|
|
709
|
+
}
|
|
694
710
|
const [, name, init] = i, normed = prep(init)
|
|
695
711
|
|
|
696
712
|
if (isDestructPattern(name)) {
|
|
@@ -1475,6 +1491,7 @@ const handlers = {
|
|
|
1475
1491
|
'new'(ctor, ...args) {
|
|
1476
1492
|
let name = ctor, ctorArgs = args
|
|
1477
1493
|
if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
|
|
1494
|
+
if (name === 'Date' && ctorArgs.length === 1 && ctorArgs[0] == null) ctorArgs = []
|
|
1478
1495
|
// Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
|
|
1479
1496
|
if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
|
|
1480
1497
|
ctorArgs = ctorArgs[0].slice(1)
|
|
@@ -1743,6 +1760,10 @@ function prepareModule(specifier, source) {
|
|
|
1743
1760
|
if (!Array.isArray(node)) return typeof node === 'string' && !skip?.has(node) && moduleExports.has(node) ? moduleExports.get(node) : node
|
|
1744
1761
|
if (node[0] === 'str' || node[0] == null || node[0] === '`' || node[0] === '//') return node
|
|
1745
1762
|
if (node[0] === ':') { node[2] = walk(node[2], skip); return node }
|
|
1763
|
+
// Static member access: `obj.prop` — only the receiver is a reference; the
|
|
1764
|
+
// property name is a literal key and must not be renamed even if it collides
|
|
1765
|
+
// with a module-scoped binding (e.g. `IMM.reftype` where `const reftype` exists).
|
|
1766
|
+
if (node[0] === '.' || node[0] === '?.') { node[1] = walk(node[1], skip); return node }
|
|
1746
1767
|
if (node[0] === '=>') {
|
|
1747
1768
|
node[2] = walk(node[2], collectParamNames(extractParams(node[1]), new Set(skip)))
|
|
1748
1769
|
return node
|
package/src/vectorize.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findBodyStart } from './ir.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Lane-local SIMD-128 vectorizer.
|
|
3
5
|
*
|
|
@@ -41,18 +43,7 @@
|
|
|
41
43
|
* `i < BOUND` guard handles the tail.
|
|
42
44
|
*/
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
// child in a (func ...) form — past type/param/result/local declarations.
|
|
46
|
-
function findBodyStart(fn) {
|
|
47
|
-
for (let i = 2; i < fn.length; i++) {
|
|
48
|
-
const c = fn[i]
|
|
49
|
-
if (!Array.isArray(c)) continue
|
|
50
|
-
if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' ||
|
|
51
|
-
c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
|
|
52
|
-
return i
|
|
53
|
-
}
|
|
54
|
-
return fn.length
|
|
55
|
-
}
|
|
46
|
+
|
|
56
47
|
|
|
57
48
|
const isArr = Array.isArray
|
|
58
49
|
|