jz 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +251 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +551 -128
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +540 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +662 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +786 -94
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +822 -150
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/resolve.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-graph resolver — flattens a file's relative-import graph into the
|
|
3
|
+
* { code, modules } pair that `compile()` expects.
|
|
4
|
+
*
|
|
5
|
+
* Every specifier is canonicalized to an absolute path so the same physical
|
|
6
|
+
* file always produces one module instance. Two relative specifiers from
|
|
7
|
+
* different importers (e.g. `'../../parse.js'` from a feature module and
|
|
8
|
+
* `'./parse.js'` from the entry) would otherwise hit prepare.js as separate
|
|
9
|
+
* modules with separate exports / mangling prefixes — and any module-level
|
|
10
|
+
* state (like a shared `lookup` registry) would split in two.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs'
|
|
14
|
+
import { dirname, resolve, join } from 'path'
|
|
15
|
+
import { execFileSync } from 'child_process'
|
|
16
|
+
|
|
17
|
+
// Matches both `import` and `export ... from` statements (statement position).
|
|
18
|
+
// Lazy `[^'"]*?` consumes whatever sits between the keyword and the first quote.
|
|
19
|
+
const importRe = /^\s*(?:import|export)\s+[^'"]*?['"]([^'"]+)['"]/gm
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} entryFile - absolute or cwd-relative path to the entry module.
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {boolean} [opts.resolveNode] - resolve bare specifiers via Node resolution.
|
|
25
|
+
* @returns {{ code: string, modules: Record<string,string> }}
|
|
26
|
+
* `code` is the entry rewritten to canonical keys; `modules` maps every
|
|
27
|
+
* reachable absolute path to its (also-rewritten) source.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveModuleGraph(entryFile, { resolveNode = false } = {}) {
|
|
30
|
+
const dir = dirname(resolve(entryFile))
|
|
31
|
+
const code = readFileSync(resolve(entryFile), 'utf8')
|
|
32
|
+
const modules = {} // keyed by canonical absolute path
|
|
33
|
+
const seenPaths = new Set()
|
|
34
|
+
const pkgImports = {} // pkg.imports spec → absolute path
|
|
35
|
+
|
|
36
|
+
const pkgFile = join(dir, 'package.json')
|
|
37
|
+
if (existsSync(pkgFile)) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'))
|
|
40
|
+
if (pkg.imports) for (const [spec, path] of Object.entries(pkg.imports))
|
|
41
|
+
pkgImports[spec] = resolve(dir, path)
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolveBareModule = (specifier, fromDir) => execFileSync(
|
|
46
|
+
process.execPath,
|
|
47
|
+
['--input-type=module', '-e', 'process.stdout.write(import.meta.resolve(process.argv[1]))', specifier],
|
|
48
|
+
{ cwd: fromDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
49
|
+
).trim()
|
|
50
|
+
const resolveAbsPath = (specifier, fromDir) => {
|
|
51
|
+
if (pkgImports[specifier]) return pkgImports[specifier]
|
|
52
|
+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
53
|
+
const full = resolve(fromDir, specifier)
|
|
54
|
+
if (existsSync(full)) return full
|
|
55
|
+
if (existsSync(full + '.js')) return full + '.js'
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
if (resolveNode) {
|
|
59
|
+
try {
|
|
60
|
+
const resolved = resolveBareModule(specifier, fromDir)
|
|
61
|
+
if (resolved.startsWith('file:')) return new URL(resolved).pathname
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
const rewriteImports = (src, fromDir) => src.replace(importRe, (match, spec) => {
|
|
67
|
+
const abs = resolveAbsPath(spec, fromDir)
|
|
68
|
+
if (!abs) return match
|
|
69
|
+
const q = match[match.lastIndexOf(spec) - 1]
|
|
70
|
+
return match.slice(0, match.lastIndexOf(spec) - 1) + q + abs + q
|
|
71
|
+
})
|
|
72
|
+
const resolveModule = (specifier, fromDir) => {
|
|
73
|
+
const abs = resolveAbsPath(specifier, fromDir)
|
|
74
|
+
if (!abs || seenPaths.has(abs)) return
|
|
75
|
+
seenPaths.add(abs)
|
|
76
|
+
let src; try { src = readFileSync(abs, 'utf8') } catch { return }
|
|
77
|
+
modules[abs] = rewriteImports(src, dirname(abs))
|
|
78
|
+
let m; importRe.lastIndex = 0
|
|
79
|
+
while ((m = importRe.exec(src)) !== null) resolveModule(m[1], dirname(abs))
|
|
80
|
+
}
|
|
81
|
+
let m; importRe.lastIndex = 0
|
|
82
|
+
while ((m = importRe.exec(code)) !== null) resolveModule(m[1], dir)
|
|
83
|
+
|
|
84
|
+
return { code: rewriteImports(code, dir), modules }
|
|
85
|
+
}
|
package/src/vectorize.js
CHANGED
|
@@ -389,18 +389,52 @@ function firstAccess(node, name) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
* Match
|
|
393
|
-
*
|
|
392
|
+
* Match the index-offset operand of a lane address — the part that scales the
|
|
393
|
+
* induction variable inside `(i32.add base OFFSET)`. OFFSET is one of:
|
|
394
|
+
* (i32.shl (local.get IND) (i32.const K)) → strideLog2 K
|
|
395
|
+
* (local.get IND) → strideLog2 0
|
|
396
|
+
* (local.tee $T <either of the above>) → records $T
|
|
397
|
+
* (local.get $T) where $T is a recorded offset-tee → that tee's strideLog2
|
|
398
|
+
*
|
|
399
|
+
* The tee'd form arises from CSE: a map loop `b[i] = f(a[i])` over two distinct
|
|
400
|
+
* base pointers shares one `i << K` offset (`(local.tee $T (i32.shl i K))` in
|
|
401
|
+
* the first address, `(local.get $T)` in the second). `offsetTees` (Map
|
|
402
|
+
* name→strideLog2) carries that across calls.
|
|
403
|
+
*
|
|
404
|
+
* Returns { strideLog2, teeName?: string } or null.
|
|
405
|
+
*/
|
|
406
|
+
function matchLaneOffset(off, ind, offsetTees) {
|
|
407
|
+
if (isArr(off) && off[0] === 'local.get' && typeof off[1] === 'string' &&
|
|
408
|
+
offsetTees && offsetTees.has(off[1])) {
|
|
409
|
+
return { strideLog2: offsetTees.get(off[1]), teeName: null }
|
|
410
|
+
}
|
|
411
|
+
let teeName = null
|
|
412
|
+
let n = off
|
|
413
|
+
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) { teeName = n[1]; n = n[2] }
|
|
414
|
+
// (i32.shl (local.get ind) (i32.const K))
|
|
415
|
+
if (isArr(n) && n[0] === 'i32.shl' && n.length === 3 && isLocalGet(n[1], ind)) {
|
|
416
|
+
const k = constNum(n[2])
|
|
417
|
+
if (k != null && k >= 0 && k <= 3) return { strideLog2: k, teeName }
|
|
418
|
+
}
|
|
419
|
+
// (local.get ind) — stride 1
|
|
420
|
+
if (isLocalGet(n, ind)) return { strideLog2: 0, teeName }
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Match an address expression `(i32.add base OFFSET)`, with optional outer
|
|
426
|
+
* `(local.tee $A ...)`. OFFSET is matched by matchLaneOffset (which also
|
|
427
|
+
* accepts a CSE'd `(local.tee $T (i32.shl ind K))` / `(local.get $T)` pair).
|
|
394
428
|
* Also accepts `(local.get $A)` when $A is a previously-recorded address tee.
|
|
395
429
|
*
|
|
396
|
-
* Returns { strideLog2, base, teeName
|
|
430
|
+
* Returns { strideLog2, base, teeName?, offsetTeeName?, viaLocal? } or null.
|
|
397
431
|
* `strideLog2` = K for i32.shl form, 0 for plain add form.
|
|
398
432
|
* `base` is the loop-invariant base subtree.
|
|
399
433
|
*/
|
|
400
|
-
function matchLaneAddr(addr, ind, addrLocals) {
|
|
434
|
+
function matchLaneAddr(addr, ind, addrLocals, offsetTees) {
|
|
401
435
|
let teeName = null
|
|
402
436
|
let n = addr
|
|
403
|
-
// (local.get $A) where $A holds a previously-tee'd lane-address.
|
|
437
|
+
// (local.get $A) where $A holds a previously-tee'd FULL lane-address.
|
|
404
438
|
if (isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' && addrLocals && addrLocals.has(n[1])) {
|
|
405
439
|
const e = addrLocals.get(n[1])
|
|
406
440
|
return { strideLog2: e.strideLog2, base: e.base, teeName: null, viaLocal: n[1] }
|
|
@@ -411,14 +445,40 @@ function matchLaneAddr(addr, ind, addrLocals) {
|
|
|
411
445
|
}
|
|
412
446
|
if (!isArr(n) || n[0] !== 'i32.add' || n.length !== 3) return null
|
|
413
447
|
const a = n[1], b = n[2]
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
448
|
+
const off = matchLaneOffset(b, ind, offsetTees)
|
|
449
|
+
if (!off) return null
|
|
450
|
+
return { strideLog2: off.strideLog2, base: a, teeName, offsetTeeName: off.teeName }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A scalar i32 local that is ONLY ever assigned a lane offset — `(i32.shl ind K)`
|
|
455
|
+
* (or bare `ind` for stride 0) — is a CSE'd offset shared across base pointers.
|
|
456
|
+
* Returns the consistent strideLog2, or null if any write to it diverges.
|
|
457
|
+
* This soundness check backs every `(local.get $T)` resolved via `offsetTees`.
|
|
458
|
+
*/
|
|
459
|
+
function _offsetLocalStride(body, name, ind) {
|
|
460
|
+
let stride = null, found = false, ok = true
|
|
461
|
+
function walk(n) {
|
|
462
|
+
if (!isArr(n)) return
|
|
463
|
+
if ((n[0] === 'local.tee' || n[0] === 'local.set') && n[1] === name && n.length === 3) {
|
|
464
|
+
found = true
|
|
465
|
+
const v = n[2]
|
|
466
|
+
let k = null
|
|
467
|
+
if (isArr(v) && v[0] === 'i32.shl' && v.length === 3 && isLocalGet(v[1], ind)) {
|
|
468
|
+
k = constNum(v[2])
|
|
469
|
+
if (k == null || k < 0 || k > 3) ok = false
|
|
470
|
+
} else if (isLocalGet(v, ind)) {
|
|
471
|
+
k = 0
|
|
472
|
+
} else ok = false
|
|
473
|
+
if (k != null) {
|
|
474
|
+
if (stride == null) stride = k
|
|
475
|
+
else if (stride !== k) ok = false
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
418
479
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return null
|
|
480
|
+
for (const s of body) walk(s)
|
|
481
|
+
return found && ok ? stride : null
|
|
422
482
|
}
|
|
423
483
|
|
|
424
484
|
// ---- Recognize a (block (loop)) pair --------------------------------------
|
|
@@ -486,6 +546,9 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
486
546
|
// both validates the load's address AND records NAME so the matching store's
|
|
487
547
|
// `(local.get NAME)` is accepted as the same lane address.
|
|
488
548
|
const addrLocals = new Map()
|
|
549
|
+
// Offset tees: name → strideLog2. A CSE'd `i << K` shared across base
|
|
550
|
+
// pointers (map loops over distinct arrays). Soundness re-checked post-scan.
|
|
551
|
+
const offsetTees = new Map()
|
|
489
552
|
|
|
490
553
|
function scanForLoadsStores(node, parent, pi) {
|
|
491
554
|
if (!isArr(node)) return true
|
|
@@ -497,10 +560,11 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
497
560
|
} else if (LOAD_OPS[op] !== laneType) {
|
|
498
561
|
return false
|
|
499
562
|
}
|
|
500
|
-
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
563
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
501
564
|
if (!m) return false
|
|
502
565
|
if ((1 << m.strideLog2) !== stride) return false
|
|
503
566
|
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
567
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
504
568
|
loadStoreSites.push({ parent, idx: pi, kind: 'load' })
|
|
505
569
|
return true
|
|
506
570
|
}
|
|
@@ -508,10 +572,11 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
508
572
|
const sty = STORE_OPS[op]
|
|
509
573
|
if (laneType != null && sty !== laneType) return false
|
|
510
574
|
if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
|
|
511
|
-
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
575
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
512
576
|
if (!m) return false
|
|
513
577
|
if ((1 << m.strideLog2) !== stride) return false
|
|
514
578
|
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
579
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
515
580
|
loadStoreSites.push({ parent, idx: pi, kind: 'store' })
|
|
516
581
|
// Recurse into VALUE child (idx 2) — it's data, not address.
|
|
517
582
|
if (!scanForLoadsStores(node[2], node, 2)) return false
|
|
@@ -521,10 +586,13 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
521
586
|
// `(local.set $a (i32.add base (i32.shl i 2)))` as a standalone stmt) —
|
|
522
587
|
// record so a later `(local.get $a)` resolves.
|
|
523
588
|
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string' && node.length === 3) {
|
|
524
|
-
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals)
|
|
589
|
+
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals, offsetTees)
|
|
525
590
|
if (valM && valM.teeName) {
|
|
526
591
|
addrLocals.set(valM.teeName, { strideLog2: valM.strideLog2, base: valM.base })
|
|
527
592
|
}
|
|
593
|
+
// Standalone offset compute: `(local.set $t (i32.shl i K))`.
|
|
594
|
+
const offM = matchLaneOffset(node[2], incVar, offsetTees)
|
|
595
|
+
if (offM) offsetTees.set(node[1], offM.strideLog2)
|
|
528
596
|
}
|
|
529
597
|
// Recurse into all children
|
|
530
598
|
for (let i = 1; i < node.length; i++) {
|
|
@@ -538,6 +606,12 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
538
606
|
if (!laneType) return null // no memory ops — vectorizing buys nothing
|
|
539
607
|
if (loadStoreSites.length === 0) return null
|
|
540
608
|
|
|
609
|
+
// Soundness gate for offset-tee resolution: every `(local.get $T)` we
|
|
610
|
+
// accepted as `i << K` is only valid if EVERY write of $T is that offset.
|
|
611
|
+
for (const [name, k] of offsetTees) {
|
|
612
|
+
if (_offsetLocalStride(body, name, incVar) !== k) return null
|
|
613
|
+
}
|
|
614
|
+
|
|
541
615
|
// Classify all locals referenced in body.
|
|
542
616
|
// - induction var (incVar): exempt
|
|
543
617
|
// - bound local (if any): must be invariant
|
|
@@ -571,7 +645,7 @@ function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
571
645
|
// Discriminate lane-data vs address-tee. Address tees hold i32 addresses,
|
|
572
646
|
// not vector data. We classify by checking the local's declared type.
|
|
573
647
|
const decl = fnLocals.get(name)
|
|
574
|
-
if (decl === 'i32' && _isAddressLocal(body, name, incVar)) {
|
|
648
|
+
if (decl === 'i32' && (offsetTees.has(name) || _isAddressLocal(body, name, incVar))) {
|
|
575
649
|
localKind.set(name, 'addr')
|
|
576
650
|
} else {
|
|
577
651
|
localKind.set(name, 'lane')
|
|
@@ -710,16 +784,18 @@ function tryReduceVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
710
784
|
const laneType = reduceEntry.laneType
|
|
711
785
|
const stride = LANE_INFO[laneType].stride
|
|
712
786
|
const addrLocals = new Map()
|
|
787
|
+
const offsetTees = new Map()
|
|
713
788
|
let loadCount = 0
|
|
714
789
|
function scanExpr(node) {
|
|
715
790
|
if (!isArr(node)) return true
|
|
716
791
|
const op = node[0]
|
|
717
792
|
if (LOAD_OPS[op]) {
|
|
718
793
|
if (LOAD_OPS[op] !== laneType) return false
|
|
719
|
-
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
794
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
720
795
|
if (!m) return false
|
|
721
796
|
if ((1 << m.strideLog2) !== stride) return false
|
|
722
797
|
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
798
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
723
799
|
loadCount++
|
|
724
800
|
return true
|
|
725
801
|
}
|
|
@@ -731,6 +807,10 @@ function tryReduceVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
731
807
|
}
|
|
732
808
|
if (!scanExpr(exprNode)) return null
|
|
733
809
|
if (loadCount === 0) return null
|
|
810
|
+
// Soundness gate for offset-tee resolution (see tryVectorize).
|
|
811
|
+
for (const [name, k] of offsetTees) {
|
|
812
|
+
if (_offsetLocalStride([exprNode], name, incVar) !== k) return null
|
|
813
|
+
}
|
|
734
814
|
|
|
735
815
|
// Classify locals referenced in EXPR. Anything not the induction var or an
|
|
736
816
|
// address-tee is invariant (we forbade local.set/tee in scanExpr).
|
|
@@ -744,10 +824,11 @@ function tryReduceVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
744
824
|
const localKind = new Map()
|
|
745
825
|
for (const name of referenced) {
|
|
746
826
|
if (name === incVar) continue
|
|
747
|
-
if (addrLocals.has(name)) { localKind.set(name, 'addr'); continue }
|
|
827
|
+
if (addrLocals.has(name) || offsetTees.has(name)) { localKind.set(name, 'addr'); continue }
|
|
748
828
|
localKind.set(name, 'invariant')
|
|
749
829
|
}
|
|
750
830
|
for (const name of addrLocals.keys()) localKind.set(name, 'addr')
|
|
831
|
+
for (const name of offsetTees.keys()) localKind.set(name, 'addr')
|
|
751
832
|
|
|
752
833
|
const ctx = { laneType, incVar, localKind, newLanedLocals: new Map(), fail: false, failReason: null }
|
|
753
834
|
const liftedExpr = liftExprV(exprNode, ctx)
|
package/src/ast.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST analysis predicates — pure functions over jz AST arrays.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from emit.js to reduce the God File and enable reuse.
|
|
5
|
-
* These functions walk the jz AST (array-of-atoms format) and answer
|
|
6
|
-
* structural questions without emitting any IR.
|
|
7
|
-
*
|
|
8
|
-
* # Convention
|
|
9
|
-
* AST nodes are either:
|
|
10
|
-
* - strings (identifiers / bare names)
|
|
11
|
-
* - numbers (numeric literals)
|
|
12
|
-
* - arrays where [0] is the operator tag: ['+', a, b], ['let', ...], ['=>', params, body], etc.
|
|
13
|
-
* - [null, value] for parenthesized/boxed literals
|
|
14
|
-
*
|
|
15
|
-
* @module ast
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { ASSIGN_OPS, intLiteralValue } from './analyze.js'
|
|
19
|
-
import { ctx } from './ctx.js'
|
|
20
|
-
|
|
21
|
-
/** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
|
|
22
|
-
* Conservative over-reject: if unsure, treat as written.
|
|
23
|
-
* `let`/`const` declarations are NOT reassignments — only the initializer expressions
|
|
24
|
-
* inside them are scanned. */
|
|
25
|
-
export function isReassigned(body, name) {
|
|
26
|
-
if (!Array.isArray(body)) return false
|
|
27
|
-
const op = body[0]
|
|
28
|
-
if (ASSIGN_OPS.has(op) && body[1] === name) return true
|
|
29
|
-
if ((op === '++' || op === '--') && body[1] === name) return true
|
|
30
|
-
if (op === 'let' || op === 'const') {
|
|
31
|
-
for (let i = 1; i < body.length; i++) {
|
|
32
|
-
const d = body[i]
|
|
33
|
-
if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
|
|
34
|
-
}
|
|
35
|
-
return false
|
|
36
|
-
}
|
|
37
|
-
for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Does `body` contain a `continue` that targets THIS loop?
|
|
42
|
-
* A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
|
|
43
|
-
export function hasOwnContinue(body) {
|
|
44
|
-
if (!Array.isArray(body)) return false
|
|
45
|
-
const op = body[0]
|
|
46
|
-
if (op === 'continue') return true
|
|
47
|
-
if (op === 'for' || op === 'while' || op === 'do') return false
|
|
48
|
-
for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function hasOwnBreakOrContinue(body) {
|
|
53
|
-
if (!Array.isArray(body)) return false
|
|
54
|
-
const op = body[0]
|
|
55
|
-
if (op === 'break' || op === 'continue') return true
|
|
56
|
-
if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
|
|
57
|
-
for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function containsNestedClosure(body) {
|
|
62
|
-
if (!Array.isArray(body)) return false
|
|
63
|
-
if (body[0] === '=>') return true
|
|
64
|
-
for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function containsNestedLoop(body) {
|
|
69
|
-
if (!Array.isArray(body)) return false
|
|
70
|
-
const op = body[0]
|
|
71
|
-
if (op === 'for' || op === 'while' || op === 'do') return true
|
|
72
|
-
if (op === '=>') return false
|
|
73
|
-
for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Recursive loop size estimator — product of trip counts for nested `for (let i=0; i<N; i++)` loops. */
|
|
78
|
-
export function nestedSmallLoopBudget(body) {
|
|
79
|
-
if (!Array.isArray(body)) return 1
|
|
80
|
-
if (body[0] === '=>') return 1
|
|
81
|
-
if (body[0] === 'for') {
|
|
82
|
-
const [, init, cond, step, loopBody] = body
|
|
83
|
-
const n = smallConstForTripCount(init, cond, step)
|
|
84
|
-
return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
|
|
85
|
-
}
|
|
86
|
-
let max = 1
|
|
87
|
-
for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
|
|
88
|
-
return max
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function containsDeclOf(body, name) {
|
|
92
|
-
if (!Array.isArray(body)) return false
|
|
93
|
-
const op = body[0]
|
|
94
|
-
if (op === '=>') return false
|
|
95
|
-
if (op === 'let' || op === 'const') {
|
|
96
|
-
for (let i = 1; i < body.length; i++) {
|
|
97
|
-
const d = body[i]
|
|
98
|
-
if (d === name) return true
|
|
99
|
-
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
|
|
103
|
-
return false
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Clone AST node, substituting bare-name matches with [null, value]. Skips into closures. */
|
|
107
|
-
export function cloneWithSubst(node, name, value) {
|
|
108
|
-
if (node === name) return [null, value]
|
|
109
|
-
if (!Array.isArray(node)) return node
|
|
110
|
-
if (node[0] === '=>') return node
|
|
111
|
-
return node.map(x => cloneWithSubst(x, name, value))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const MAX_SMALL_FOR_UNROLL = 8
|
|
115
|
-
export const MAX_NESTED_FOR_UNROLL = 64
|
|
116
|
-
|
|
117
|
-
/** Does `body` access a typed-array element by string name known to the type system? */
|
|
118
|
-
export function containsKnownTypedArrayIndex(body) {
|
|
119
|
-
if (!Array.isArray(body)) return false
|
|
120
|
-
if (body[0] === '=>') return false
|
|
121
|
-
if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
|
|
122
|
-
for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
|
|
123
|
-
return false
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Analyze `for (let i=0; i<N; i++)` trip count. Returns N if structurally matches, else null. */
|
|
127
|
-
export function smallConstForTripCount(init, cond, step) {
|
|
128
|
-
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
129
|
-
const decl = init[1]
|
|
130
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
131
|
-
const name = decl[1]
|
|
132
|
-
const start = intLiteralValue(decl[2])
|
|
133
|
-
if (start !== 0) return null
|
|
134
|
-
|
|
135
|
-
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
136
|
-
const end = intLiteralValue(cond[2])
|
|
137
|
-
if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
|
|
138
|
-
|
|
139
|
-
const stepOk = Array.isArray(step) && (
|
|
140
|
-
(step[0] === '++' && step[1] === name) ||
|
|
141
|
-
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
142
|
-
)
|
|
143
|
-
return stepOk ? end : null
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Does `body` always exit the enclosing scope (return / throw / break / continue)? */
|
|
147
|
-
export function isTerminator(body) {
|
|
148
|
-
if (!Array.isArray(body)) return false
|
|
149
|
-
const op = body[0]
|
|
150
|
-
if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
|
|
151
|
-
if (op === '{}' || op === ';') {
|
|
152
|
-
for (let i = body.length - 1; i >= 1; i--) {
|
|
153
|
-
const s = body[i]
|
|
154
|
-
if (s == null) continue
|
|
155
|
-
return isTerminator(s)
|
|
156
|
-
}
|
|
157
|
-
return false
|
|
158
|
-
}
|
|
159
|
-
return false
|
|
160
|
-
}
|
package/src/auto-config.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-detect optimization tuning from source characteristics.
|
|
3
|
-
*
|
|
4
|
-
* Scans the prepared AST + ctx.func.list to infer program properties
|
|
5
|
-
* and returns suggested optimization overrides. When the user does not
|
|
6
|
-
* explicitly configure individual passes, these suggestions are merged
|
|
7
|
-
* in before resolveOptimize() so the compiler self-tunes.
|
|
8
|
-
*
|
|
9
|
-
* @module auto-config
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { ctx } from './ctx.js'
|
|
13
|
-
|
|
14
|
-
const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
|
|
15
|
-
const TYPED_CTORS = new Set([
|
|
16
|
-
'new.Float32Array', 'new.Float64Array', 'new.Int8Array', 'new.Int16Array',
|
|
17
|
-
'new.Int32Array', 'new.Uint8Array', 'new.Uint16Array', 'new.Uint32Array',
|
|
18
|
-
'new.Uint8ClampedArray',
|
|
19
|
-
])
|
|
20
|
-
|
|
21
|
-
function nodeSize(node) {
|
|
22
|
-
if (!Array.isArray(node)) return 1
|
|
23
|
-
let n = 1
|
|
24
|
-
for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
|
|
25
|
-
return n
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function scanNode(node, stats, loopDepth) {
|
|
29
|
-
if (!Array.isArray(node)) return
|
|
30
|
-
const op = node[0]
|
|
31
|
-
|
|
32
|
-
if (LOOP_OPS.has(op)) {
|
|
33
|
-
stats.loopCount++
|
|
34
|
-
const d = loopDepth + 1
|
|
35
|
-
if (d > stats.maxLoopDepth) stats.maxLoopDepth = d
|
|
36
|
-
for (let i = 1; i < node.length; i++) scanNode(node[i], stats, d)
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (op === '()') {
|
|
41
|
-
stats.callSites++
|
|
42
|
-
const callee = node[1]
|
|
43
|
-
if (typeof callee === 'string' && TYPED_CTORS.has(callee)) {
|
|
44
|
-
stats.typedArrayCount++
|
|
45
|
-
const args = node[2]
|
|
46
|
-
const argList = args == null ? [] : (Array.isArray(args) && args[0] === ',') ? args.slice(1) : [args]
|
|
47
|
-
const lenLit = typeof argList[0] === 'number' ? argList[0] : null
|
|
48
|
-
if (lenLit != null && lenLit > stats.maxTypedArrayLen) stats.maxTypedArrayLen = lenLit
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (op === 'str') stats.stringLiteralCount++
|
|
53
|
-
if (op === '=>') stats.closureCount++
|
|
54
|
-
|
|
55
|
-
for (let i = 1; i < node.length; i++) scanNode(node[i], stats, loopDepth)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function scanStats(ast, code) {
|
|
59
|
-
const stats = {
|
|
60
|
-
sourceChars: code?.length || 0,
|
|
61
|
-
funcCount: 0,
|
|
62
|
-
maxFuncBodySize: 0,
|
|
63
|
-
loopCount: 0,
|
|
64
|
-
maxLoopDepth: 0,
|
|
65
|
-
typedArrayCount: 0,
|
|
66
|
-
maxTypedArrayLen: 0,
|
|
67
|
-
stringLiteralCount: 0,
|
|
68
|
-
closureCount: 0,
|
|
69
|
-
callSites: 0,
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (ctx.func?.list) {
|
|
73
|
-
stats.funcCount = ctx.func.list.length
|
|
74
|
-
for (const f of ctx.func.list) {
|
|
75
|
-
if (f.body) {
|
|
76
|
-
const sz = nodeSize(f.body)
|
|
77
|
-
if (sz > stats.maxFuncBodySize) stats.maxFuncBodySize = sz
|
|
78
|
-
scanNode(f.body, stats, 0)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (ast) scanNode(ast, stats, 0)
|
|
83
|
-
return stats
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Detect optimization config from source characteristics.
|
|
88
|
-
* Returns an object of pass overrides; empty object means "use defaults".
|
|
89
|
-
*/
|
|
90
|
-
export function detectOptimizeConfig(ast, code) {
|
|
91
|
-
const s = scanStats(ast, code)
|
|
92
|
-
const cfg = {}
|
|
93
|
-
|
|
94
|
-
// Machine-generated or large code: watr's WAT-level CSE/DCE/inline fights
|
|
95
|
-
// jz's already-optimized IR and inflates output. Disable it automatically.
|
|
96
|
-
const isLarge = s.sourceChars > 4000 || s.funcCount > 40 || s.maxFuncBodySize > 300
|
|
97
|
-
const isMachineLike = s.callSites > 300 && s.stringLiteralCount < 10
|
|
98
|
-
if (isLarge || isMachineLike) cfg.watr = false
|
|
99
|
-
|
|
100
|
-
// Typed-array heavy: tighten scalarization thresholds when we see large
|
|
101
|
-
// fixed-size arrays; keep defaults for small/dynamic ones.
|
|
102
|
-
if (s.typedArrayCount > 0 && s.maxTypedArrayLen > 0) {
|
|
103
|
-
cfg.scalarTypedArrayLen = Math.min(32, Math.max(8, s.maxTypedArrayLen + 4))
|
|
104
|
-
cfg.scalarTypedLoopUnroll = s.maxLoopDepth > 1 ? 8 : 16
|
|
105
|
-
cfg.scalarTypedNestedUnroll = s.maxLoopDepth > 1 ? 32 : 128
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// String-heavy: ensure pool sorting is on (already default, but explicit).
|
|
109
|
-
if (s.stringLiteralCount > 30) {
|
|
110
|
-
cfg.sortStrPoolByFreq = true
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Closure-heavy: ptr hoists pay off.
|
|
114
|
-
if (s.closureCount > 4) {
|
|
115
|
-
cfg.hoistPtrType = true
|
|
116
|
-
cfg.hoistInvariantPtrOffset = true
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return cfg
|
|
120
|
-
}
|