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/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 an address expression `(i32.add base (i32.shl (local.get IND) (i32.const K)))`,
393
- * with optional outer `(local.tee $A ...)`. Stride 1 fallback: `(i32.add base (local.get IND))`.
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?: string, viaLocal?: string } or null.
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
- // case 1: (i32.add base (i32.shl (local.get ind) (i32.const K)))
415
- if (isArr(b) && b[0] === 'i32.shl' && b.length === 3 && isLocalGet(b[1], ind)) {
416
- const k = constNum(b[2])
417
- if (k != null && k >= 0 && k <= 3) return { strideLog2: k, base: a, teeName }
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
- // case 2: (i32.add base (local.get ind)) — stride 1
420
- if (isLocalGet(b, ind)) return { strideLog2: 0, base: a, teeName }
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
- }
@@ -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
- }