jz 0.6.0 → 0.7.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 +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/src/optimize/index.js
CHANGED
|
@@ -59,12 +59,12 @@ const FALSE_BITS = atomNanHex(4)
|
|
|
59
59
|
* 3 — level 2 + larger array/hash initial caps + `hoistConstantPool` off
|
|
60
60
|
* (inline `f64.const` over mutable globals); trades size for speed.
|
|
61
61
|
*
|
|
62
|
-
* String
|
|
63
|
-
* knobs; watr is on for
|
|
64
|
-
* 'size'
|
|
65
|
-
*
|
|
66
|
-
* '
|
|
67
|
-
*
|
|
62
|
+
* String presets (the size↔speed tradeoff lives entirely in the unroll/scalar
|
|
63
|
+
* knobs; watr is on for both):
|
|
64
|
+
* 'size' — loop/const unroll + lane vectorization off, tight scalar-replacement
|
|
65
|
+
* caps. Smallest wasm.
|
|
66
|
+
* 'speed' — full nested unroll + lane vectorization (= level 3).
|
|
67
|
+
* The default (level 2) has no string name — omit `optimize` or pass `2`.
|
|
68
68
|
*
|
|
69
69
|
* # Two-layer contract (this file vs src/wat/optimize.js)
|
|
70
70
|
* Both layers walk the same S-expression IR; the boundary is KNOWLEDGE, not
|
|
@@ -143,9 +143,9 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
143
143
|
// closures). Inline `f64.const` is the minimal lowering: V8 CSEs identical
|
|
144
144
|
// constants for free. Measured −3% on jessie parse for +14% binary — exactly
|
|
145
145
|
// the size↔speed trade 'speed' exists to make.
|
|
146
|
-
3: Object.freeze({ ...ALL_ON, hoistConstantPool: false, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
147
|
-
// '
|
|
148
|
-
|
|
146
|
+
3: Object.freeze({ ...ALL_ON, hoistConstantPool: false, arrayMinCap: 16, hashSmallInitCap: 8, reduceUnroll: true, relaxedSimd: true, inlineFns: true }),
|
|
147
|
+
// 'size' tightens scalar/unroll caps; 'speed' = level 3. There is no 'balanced'
|
|
148
|
+
// preset — it was a pure synonym for the default level 2 (omit `optimize` or pass 2).
|
|
149
149
|
size: Object.freeze({
|
|
150
150
|
...ALL_ON,
|
|
151
151
|
smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false, splitCharScan: false,
|
|
@@ -154,7 +154,16 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
154
154
|
scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
|
|
155
155
|
}),
|
|
156
156
|
// 'speed' === level 3: full watr (inlining on) + L3 cap/hash tuning, pool off.
|
|
157
|
-
|
|
157
|
+
// reduceUnroll: vectorize reductions with N independent accumulators (ILP/latency
|
|
158
|
+
// hiding, ~3x on dot/FIR sums) — a size↔speed trade like the pool-off above, so
|
|
159
|
+
// speed-only; level 2 / balanced / size keep the single-accumulator reduce.
|
|
160
|
+
// relaxedSimd: fold f64x2 dot-pairs to f64x2.relaxed_madd (single fused VFMADD,
|
|
161
|
+
// one rounding) — faster + more accurate, but the fused result diverges bit-for-bit
|
|
162
|
+
// from the non-fused JS/native reference (bench `fma` parity class). speed-only.
|
|
163
|
+
// (The stencil + outer-strip vectorizers are NOT level-gated here: they're bit-exact pure wins
|
|
164
|
+
// like the base lane vectorizer, so they run whenever it does — default-on at level 2+ via
|
|
165
|
+
// `cfg.experimentalStencil !== false` at the call site, not a speed-only size/precision trade.)
|
|
166
|
+
speed: Object.freeze({ ...ALL_ON, hoistConstantPool: false, arrayMinCap: 16, hashSmallInitCap: 8, reduceUnroll: true, relaxedSimd: true, inlineFns: true }),
|
|
158
167
|
})
|
|
159
168
|
|
|
160
169
|
/**
|
|
@@ -163,7 +172,7 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
163
172
|
* resolveOptimize(undefined | true) → level 2 stable defaults
|
|
164
173
|
* resolveOptimize(false | 0) → all off
|
|
165
174
|
* resolveOptimize(1 | 2 | 3) → preset for that level
|
|
166
|
-
* resolveOptimize('size' | 'speed'
|
|
175
|
+
* resolveOptimize('size' | 'speed') → named preset ('speed' = level 3)
|
|
167
176
|
* resolveOptimize({ level: 1, watr: true }) → level 1 base, with watr forced on
|
|
168
177
|
* resolveOptimize({ level: 'size', vectorizeLaneLocal: true }) → 'size' base, override
|
|
169
178
|
* resolveOptimize({ hoistAddrBase: false }) → level 2 base, hoistAddrBase off
|
|
@@ -433,6 +442,24 @@ export function hoistAddrBase(fn) {
|
|
|
433
442
|
// loop-invariant __jss_length in the same loop condition CAN hoist).
|
|
434
443
|
const SAFE_OFFSET_CALLS = new Set(['$__ptr_offset', '$__ptr_type', '$__ptr_aux', '$__len', '$__jss_length', '$__jss_charCodeAt'])
|
|
435
444
|
|
|
445
|
+
// Calls that don't modify EXISTING heap memory: they may allocate (bump the heap
|
|
446
|
+
// pointer) or do tag dispatch, but they never write to an address a hoisted
|
|
447
|
+
// __typed_idx/__str_idx element read would revisit. Their presence must not
|
|
448
|
+
// block readonly-mem-call LICM (else any `s += unknown` — which dispatches via
|
|
449
|
+
// __is_str_key/__str_concat — would pin every invariant array element in-loop:
|
|
450
|
+
// the jagged-array `grid[i][j]` deopt).
|
|
451
|
+
const NON_MUTATING_CALLS = new Set(['$__is_str_key', '$__str_concat', '$__to_num', '$__to_str', '$__str_byteLen'])
|
|
452
|
+
|
|
453
|
+
// Read-only HEAP-MEMORY calls: like SAFE_OFFSET_CALLS but they read element
|
|
454
|
+
// storage that a direct f64.store/i32.store in the loop could alias. Safe to
|
|
455
|
+
// hoist only when the loop has no mutating call AND no direct store at all (we
|
|
456
|
+
// can't do alias analysis at WAT level). __typed_idx/__str_idx read arr[i] /
|
|
457
|
+
// s[i]; plain-array element writes go through calls (caught by hasUnsafeCall),
|
|
458
|
+
// and typed-array writes are direct stores (caught by hasDirectStore) — so the
|
|
459
|
+
// guard covers both. This is what lets LICM hoist `grid[i]` out of a read-only
|
|
460
|
+
// `for(j) { ... grid[i][j] ... }` inner loop (the jagged-array deopt).
|
|
461
|
+
const READONLY_MEM_CALLS = new Set(['$__typed_idx', '$__str_idx'])
|
|
462
|
+
|
|
436
463
|
export function hoistInvariantPtrOffset(fn) {
|
|
437
464
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
438
465
|
const bodyStart = findBodyStart(fn)
|
|
@@ -550,6 +577,22 @@ const HARD_OPS = new Set([
|
|
|
550
577
|
])
|
|
551
578
|
const hasHardOp = (n) => Array.isArray(n) && (HARD_OPS.has(n[0]) || n.some((c, i) => i > 0 && hasHardOp(c)))
|
|
552
579
|
|
|
580
|
+
// The inline typed-array base decode `(i32.wrap_i64 (i64.and (i64.reinterpret_f64
|
|
581
|
+
// (local|global X)) 0xFFFFFFFF))` — what `typedBase` emits for a NaN-boxed pointer.
|
|
582
|
+
// V8's wasm tier does NOT reliably LICM this i64 reinterpret chain, and it carries no
|
|
583
|
+
// HARD_OP, so without this it stays per-element inside the loop. It is the typed-read
|
|
584
|
+
// equivalent of the `__ptr_offset` call (a HARD_OP) that hoistGlobalPtrOffset hoists at
|
|
585
|
+
// function scope; admitting it here also covers a pointer reassigned ELSEWHERE in the
|
|
586
|
+
// function (the ping-pong double-buffer `a = b` in wireworld / any CA), where the base
|
|
587
|
+
// is invariant within each loop but not function-wide.
|
|
588
|
+
const isPtrBaseDecode = (n) =>
|
|
589
|
+
Array.isArray(n) && n[0] === 'i32.wrap_i64' && n.length === 2 &&
|
|
590
|
+
Array.isArray(n[1]) && n[1][0] === 'i64.and' && n[1].length === 3 &&
|
|
591
|
+
Array.isArray(n[1][2]) && n[1][2][0] === 'i64.const' &&
|
|
592
|
+
(typeof n[1][2][1] === 'string' ? Number(n[1][2][1]) : n[1][2][1]) === LAYOUT.OFFSET_MASK &&
|
|
593
|
+
Array.isArray(n[1][1]) && n[1][1][0] === 'i64.reinterpret_f64' && n[1][1].length === 2 &&
|
|
594
|
+
Array.isArray(n[1][1][1]) && (n[1][1][1][0] === 'local.get' || n[1][1][1][0] === 'global.get')
|
|
595
|
+
|
|
553
596
|
const PURE_LICM_OPS = new Set([
|
|
554
597
|
'f64.add', 'f64.sub', 'f64.mul', 'f64.div', 'f64.neg', 'f64.abs', 'f64.sqrt',
|
|
555
598
|
'f64.min', 'f64.max', 'f64.ceil', 'f64.floor', 'f64.trunc', 'f64.nearest', 'f64.copysign',
|
|
@@ -613,7 +656,27 @@ export function hoistInvariantLoop(fn) {
|
|
|
613
656
|
if (!Array.isArray(node)) return null
|
|
614
657
|
const op = node[0]
|
|
615
658
|
if (op === 'select') return resultType(node[1])
|
|
616
|
-
if (op === '
|
|
659
|
+
if (op === 'if') {
|
|
660
|
+
// (if (result T) cond (then ...) (else ...)) — type is the result clause.
|
|
661
|
+
for (let i = 1; i < node.length; i++) {
|
|
662
|
+
const c = node[i]
|
|
663
|
+
if (Array.isArray(c) && c[0] === 'result') return c[1]
|
|
664
|
+
}
|
|
665
|
+
return null
|
|
666
|
+
}
|
|
667
|
+
if (op === 'block') {
|
|
668
|
+
for (let i = 1; i < node.length; i++) {
|
|
669
|
+
const c = node[i]
|
|
670
|
+
if (Array.isArray(c) && c[0] === 'result') return c[1]
|
|
671
|
+
}
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
if (op === 'call') {
|
|
675
|
+
// SAFE_OFFSET_CALLS all return i32; READONLY_MEM_CALLS return f64 (NaN-boxed element)
|
|
676
|
+
if (SAFE_OFFSET_CALLS.has(node[1])) return 'i32'
|
|
677
|
+
if (READONLY_MEM_CALLS.has(node[1])) return 'f64'
|
|
678
|
+
return null
|
|
679
|
+
}
|
|
617
680
|
if (op === 'local.get' || op === 'local.tee') return localTypes.get(node[1]) ?? null
|
|
618
681
|
const dot = op.indexOf('.')
|
|
619
682
|
if (dot < 0) return null
|
|
@@ -644,23 +707,27 @@ export function hoistInvariantLoop(fn) {
|
|
|
644
707
|
const newLocals = []
|
|
645
708
|
const refcount = buildRefcount(fn)
|
|
646
709
|
|
|
647
|
-
const processLoop = (loopNode) => {
|
|
710
|
+
const processLoop = (loopNode, nested) => {
|
|
648
711
|
// Inner loops first (bottom-up) — an inner hoist creates a local.get the
|
|
649
|
-
// outer level can hoist further.
|
|
712
|
+
// outer level can hoist further. Children run in a nested context.
|
|
650
713
|
for (let i = 1; i < loopNode.length; i++)
|
|
651
|
-
if (Array.isArray(loopNode[i])) processNode(loopNode[i], loopNode, i)
|
|
714
|
+
if (Array.isArray(loopNode[i])) processNode(loopNode[i], loopNode, i, true)
|
|
652
715
|
|
|
653
716
|
// The loop's effect summary (scans nested loops too — conservative).
|
|
654
717
|
const locals = new Set(), globals = new Set(), storedCells = new Set()
|
|
655
|
-
let hasUnsafeCall = false, hasAnyCall = false
|
|
718
|
+
let hasUnsafeCall = false, hasAnyCall = false, hasDirectStore = false, hasV128 = false
|
|
656
719
|
const scan = (node) => {
|
|
657
720
|
if (!Array.isArray(node)) return
|
|
658
721
|
const op = node[0]
|
|
722
|
+
// A vectorized loop (lane/v128 ops) is already register-tight and hand-tuned;
|
|
723
|
+
// extra scalar hoisting there only adds spill pressure — keep it conservative.
|
|
724
|
+
if (op.startsWith('v128.') || /^[if]\d+x\d+\./.test(op)) hasV128 = true
|
|
659
725
|
if (op === 'local.set' || op === 'local.tee') { if (typeof node[1] === 'string') locals.add(node[1]); for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
660
726
|
if (op === 'global.set') { if (typeof node[1] === 'string') globals.add(node[1]); for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
661
|
-
if (op === 'call') { hasAnyCall = true; if (!SAFE_OFFSET_CALLS.has(node[1])) hasUnsafeCall = true; for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
727
|
+
if (op === 'call') { hasAnyCall = true; if (!SAFE_OFFSET_CALLS.has(node[1]) && !READONLY_MEM_CALLS.has(node[1]) && !NON_MUTATING_CALLS.has(node[1])) hasUnsafeCall = true; for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
662
728
|
if (op === 'call_ref' || op === 'call_indirect') { hasAnyCall = hasUnsafeCall = true; for (let i = 1; i < node.length; i++) scan(node[i]); return }
|
|
663
729
|
if ((op === 'f64.store' || op === 'i32.store') && node.length >= 3) {
|
|
730
|
+
hasDirectStore = true
|
|
664
731
|
const a = node[1]
|
|
665
732
|
if (Array.isArray(a) && a[0] === 'local.get' && typeof a[1] === 'string' && a[1].startsWith(CELL_PREFIX)) storedCells.add(a[1])
|
|
666
733
|
}
|
|
@@ -731,7 +798,27 @@ export function hoistInvariantLoop(fn) {
|
|
|
731
798
|
return Array.isArray(a) && a[0] === 'local.get' && typeof a[1] === 'string' && a[1].startsWith(CELL_PREFIX)
|
|
732
799
|
&& !hasAnyCall && !storedCells.has(a[1]) && (bound.has(a[1]) || !locals.has(a[1]))
|
|
733
800
|
}
|
|
734
|
-
if (op === 'call')
|
|
801
|
+
if (op === 'call') {
|
|
802
|
+
if (SAFE_OFFSET_CALLS.has(node[1]))
|
|
803
|
+
return !hasUnsafeCall && node.slice(2).every(c => pureGiven(c, bound))
|
|
804
|
+
// Read-only heap reads: additionally require no direct store (alias-safe).
|
|
805
|
+
if (READONLY_MEM_CALLS.has(node[1]))
|
|
806
|
+
return !hasUnsafeCall && !hasDirectStore && node.slice(2).every(c => pureGiven(c, bound))
|
|
807
|
+
return false
|
|
808
|
+
}
|
|
809
|
+
// A value-producing `if` whose condition and both arms are pure is itself
|
|
810
|
+
// pure — the tag-dispatch idiom `(if (result f64) tag-check (then read-A)
|
|
811
|
+
// (else read-B))` that wraps __typed_idx/__str_idx element access.
|
|
812
|
+
if (op === 'if') {
|
|
813
|
+
for (let i = 1; i < node.length; i++) {
|
|
814
|
+
const c = node[i]
|
|
815
|
+
if (!Array.isArray(c)) continue
|
|
816
|
+
if (c[0] === 'result') continue
|
|
817
|
+
if (c[0] === 'then' || c[0] === 'else') { if (!c.slice(1).every(x => pureGiven(x, bound))) return false }
|
|
818
|
+
else if (!pureGiven(c, bound)) return false // the condition
|
|
819
|
+
}
|
|
820
|
+
return true
|
|
821
|
+
}
|
|
735
822
|
if (PURE_LICM_OPS.has(op)) return node.slice(1).every(c => pureGiven(c, bound))
|
|
736
823
|
return false
|
|
737
824
|
}
|
|
@@ -745,9 +832,14 @@ export function hoistInvariantLoop(fn) {
|
|
|
745
832
|
// Every local the subtree writes must be private to it (no other use in the
|
|
746
833
|
// loop) — else moving the write to the pre-header changes another reader.
|
|
747
834
|
for (const b of bound) if (localCount.get(b) !== countsOf(node).get(b)) return false
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
|
|
835
|
+
// Top-level loops: only hoist what V8's wasm tier won't — a HARD_OP or the
|
|
836
|
+
// inline typed-array base decode — and leave plain pure arithmetic to V8's own
|
|
837
|
+
// LICM (which handles single-level loops well). NESTED (inner) loops are
|
|
838
|
+
// different: V8's wasm tier under-hoists invariants out of them (a nested
|
|
839
|
+
// rasterizer/convolution recomputes triangle/row-invariant subexpressions every
|
|
840
|
+
// iteration), so hoist any pure-invariant subtree there. Soundness is unchanged —
|
|
841
|
+
// `pureGiven` already proves the subtree is loop-invariant and side-effect-free.
|
|
842
|
+
return ((nested && !hasV128) || hasHardOp(node) || isPtrBaseDecode(node)) && pureGiven(node, bound)
|
|
751
843
|
}
|
|
752
844
|
|
|
753
845
|
// Maximal extraction: take the largest hoistable subtree; don't descend into
|
|
@@ -780,17 +872,17 @@ export function hoistInvariantLoop(fn) {
|
|
|
780
872
|
return snaps
|
|
781
873
|
}
|
|
782
874
|
|
|
783
|
-
const processNode = (node, parent, idx) => {
|
|
875
|
+
const processNode = (node, parent, idx, nested = false) => {
|
|
784
876
|
if (!Array.isArray(node)) return
|
|
785
877
|
if (node[0] === 'loop') {
|
|
786
|
-
const snaps = processLoop(node)
|
|
878
|
+
const snaps = processLoop(node, nested)
|
|
787
879
|
if (snaps.length) parent.splice(idx, 0, ...snaps)
|
|
788
880
|
return
|
|
789
881
|
}
|
|
790
|
-
for (let i = 0; i < node.length; i++) processNode(node[i], node, i)
|
|
882
|
+
for (let i = 0; i < node.length; i++) processNode(node[i], node, i, nested)
|
|
791
883
|
}
|
|
792
884
|
|
|
793
|
-
for (let i = bodyStart; i < fn.length; i++) processNode(fn[i], fn, i)
|
|
885
|
+
for (let i = bodyStart; i < fn.length; i++) processNode(fn[i], fn, i, false)
|
|
794
886
|
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
795
887
|
}
|
|
796
888
|
|
|
@@ -1768,23 +1860,52 @@ export function hoistGlobalPtrOffset(fn, stablePtrGlobals, reachableWrites) {
|
|
|
1768
1860
|
const bodyStart = findBodyStart(fn)
|
|
1769
1861
|
if (bodyStart < 0) return
|
|
1770
1862
|
|
|
1771
|
-
// `(
|
|
1772
|
-
const
|
|
1773
|
-
Array.isArray(n) && n[0] === '
|
|
1774
|
-
&& Array.isArray(n[
|
|
1775
|
-
|
|
1776
|
-
|
|
1863
|
+
// `(i64.reinterpret_f64 (global.get $G))` → G, or null.
|
|
1864
|
+
const reintGlobal = (n) =>
|
|
1865
|
+
Array.isArray(n) && n[0] === 'i64.reinterpret_f64'
|
|
1866
|
+
&& Array.isArray(n[1]) && n[1][0] === 'global.get' && typeof n[1][1] === 'string'
|
|
1867
|
+
? n[1][1] : null
|
|
1868
|
+
// A stable-pointee global's byte-base reaches us in two interchangeable shapes:
|
|
1869
|
+
// • forwarding-aware `(call $__ptr_offset (i64.reinterpret_f64 (global.get $G)))`
|
|
1870
|
+
// • inline typed read `(i32.wrap_i64 (i64.and (i64.reinterpret_f64 (global.get $G)) MASK))`
|
|
1871
|
+
// The inline form is what typed-array reads emit (a fixed-size typed array never
|
|
1872
|
+
// relocates, so they skip __ptr_offset's forwarding follow — see module/typedarray.js
|
|
1873
|
+
// `typedBase`). For a never-forwarding pointee both yield the identical offset, so
|
|
1874
|
+
// either site hoists to the one `__ptr_offset` entry snapshot. Matching only the
|
|
1875
|
+
// call form left typed-array globals re-decoding the NaN-box per element in stencil
|
|
1876
|
+
// sweeps (watercolor's pressure solve: 5 reads/cell × millions of cells). → G, or null.
|
|
1877
|
+
const siteGlobal = (n) => {
|
|
1878
|
+
if (!Array.isArray(n)) return null
|
|
1879
|
+
if (n[0] === 'call' && n[1] === '$__ptr_offset' && n.length === 3) return reintGlobal(n[2])
|
|
1880
|
+
if (n[0] === 'i32.wrap_i64' && n.length === 2 && Array.isArray(n[1]) && n[1][0] === 'i64.and' && n[1].length === 3) {
|
|
1881
|
+
const mask = n[1][2]
|
|
1882
|
+
if (Array.isArray(mask) && mask[0] === 'i64.const'
|
|
1883
|
+
&& (typeof mask[1] === 'string' ? Number(mask[1]) : mask[1]) === LAYOUT.OFFSET_MASK)
|
|
1884
|
+
return reintGlobal(n[1][1])
|
|
1885
|
+
}
|
|
1886
|
+
return null
|
|
1887
|
+
}
|
|
1777
1888
|
|
|
1778
1889
|
// Per-global: static site count AND whether any site sits inside a loop. A
|
|
1779
1890
|
// single in-loop site is a per-ITERATION resolve (lenia's convolution reads
|
|
1780
1891
|
// each of kdx/kdy/kw at one site × ~14M taps/frame), so loop placement beats
|
|
1781
1892
|
// site count as the hoist criterion.
|
|
1782
1893
|
const counts = new Map(), inLoop = new Set(), ownWrites = new Set(), ownCallees = new Set()
|
|
1894
|
+
// Globals seen via the `__ptr_offset` call form (vs. only the inline typed mask).
|
|
1895
|
+
// The snapshot reuses an EXISTING form so it never resurrects a treeshaken helper:
|
|
1896
|
+
// a typed-array-only module emits no `__ptr_offset` call, so snapping one in would
|
|
1897
|
+
// reference a function that isn't in the module.
|
|
1898
|
+
const ptrOffsetForm = new Set()
|
|
1783
1899
|
let hasIndirect = false
|
|
1784
1900
|
const scan = (n, loopDepth) => {
|
|
1785
1901
|
if (!Array.isArray(n)) return
|
|
1786
1902
|
const g = siteGlobal(n)
|
|
1787
|
-
if (g != null) {
|
|
1903
|
+
if (g != null) {
|
|
1904
|
+
counts.set(g, (counts.get(g) || 0) + 1)
|
|
1905
|
+
if (loopDepth > 0) inLoop.add(g)
|
|
1906
|
+
if (n[0] === 'call') ptrOffsetForm.add(g)
|
|
1907
|
+
return
|
|
1908
|
+
}
|
|
1788
1909
|
if (n[0] === 'global.set' && typeof n[1] === 'string') ownWrites.add(n[1])
|
|
1789
1910
|
else if ((n[0] === 'call' || n[0] === 'return_call') && typeof n[1] === 'string') ownCallees.add(n[1])
|
|
1790
1911
|
else if (n[0] === 'call_indirect' || n[0] === 'call_ref' || n[0] === 'return_call_indirect') hasIndirect = true
|
|
@@ -1833,7 +1954,13 @@ export function hoistGlobalPtrOffset(fn, stablePtrGlobals, reachableWrites) {
|
|
|
1833
1954
|
const decls = [], snaps = []
|
|
1834
1955
|
for (const [g, name] of chosen) {
|
|
1835
1956
|
decls.push(['local', name, 'i32'])
|
|
1836
|
-
|
|
1957
|
+
// Match an existing site's form so we never reference a treeshaken helper.
|
|
1958
|
+
// For a never-forwarding pointee both forms compute the same offset, so the
|
|
1959
|
+
// inline mask is a safe (and call-free) snapshot when no __ptr_offset site exists.
|
|
1960
|
+
const snap = ptrOffsetForm.has(g)
|
|
1961
|
+
? ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['global.get', g]]]
|
|
1962
|
+
: ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ['global.get', g]], ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
1963
|
+
snaps.push(['local.set', name, snap])
|
|
1837
1964
|
}
|
|
1838
1965
|
fn.splice(bodyStart, 0, ...decls, ...snaps)
|
|
1839
1966
|
}
|
|
@@ -2374,6 +2501,146 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
|
|
|
2374
2501
|
}
|
|
2375
2502
|
}
|
|
2376
2503
|
|
|
2504
|
+
/**
|
|
2505
|
+
* Fold dead string-dispatch blocks when the tested operand is a proven-f64 local.
|
|
2506
|
+
*
|
|
2507
|
+
* jz's `+` emitter produces, for every binary addition whose right operand has an
|
|
2508
|
+
* unresolved valType, the pattern:
|
|
2509
|
+
*
|
|
2510
|
+
* (block (result f64)
|
|
2511
|
+
* (local.set $B EXPR_A)
|
|
2512
|
+
* (if (result f64)
|
|
2513
|
+
* (call $__is_str_key (i64.reinterpret_f64 (local.tee $C (local.get $P))))
|
|
2514
|
+
* (then (call $__str_concat …))
|
|
2515
|
+
* (else (f64.add (local.get $B) (local.get $C)))))
|
|
2516
|
+
*
|
|
2517
|
+
* When $P is a proven-f64 local (an f64 param, or an f64-typed local provably set
|
|
2518
|
+
* only from f64 arithmetic) it can never hold a string-key NaN-box, so the
|
|
2519
|
+
* `$__is_str_key` test is provably false and the `then` branch is dead.
|
|
2520
|
+
* Replace the whole block with `(f64.add EXPR_A (local.get $P))`.
|
|
2521
|
+
*
|
|
2522
|
+
* SOUND: f64 params can never hold a string-key NaN-box by construction (jz
|
|
2523
|
+
* only allows strings in f64 slots via explicit mkptr boxing, never a bare
|
|
2524
|
+
* param). This fold is additive/gated (only runs when vectorizeLaneLocal is on)
|
|
2525
|
+
* and only removes provably-dead string-dispatch overhead.
|
|
2526
|
+
*
|
|
2527
|
+
* Called in the 'post' phase of optimizeFunc, before vectorizeLaneLocal, so the
|
|
2528
|
+
* cleaned IR is what the vectorizer pattern-matches.
|
|
2529
|
+
*/
|
|
2530
|
+
export function foldStrDispatchF64(fn) {
|
|
2531
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
2532
|
+
const bodyStart = findBodyStart(fn)
|
|
2533
|
+
if (bodyStart < 0) return
|
|
2534
|
+
|
|
2535
|
+
// Collect all f64 params — provably never hold a string-key NaN-box.
|
|
2536
|
+
const rawF64 = new Set()
|
|
2537
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
2538
|
+
const d = fn[i]
|
|
2539
|
+
if (Array.isArray(d) && d[0] === 'param' && typeof d[1] === 'string' && d[2] === 'f64')
|
|
2540
|
+
rawF64.add(d[1])
|
|
2541
|
+
}
|
|
2542
|
+
if (!rawF64.size) return // no f64 params → nothing to fold
|
|
2543
|
+
|
|
2544
|
+
// Transitively extend rawF64: an f64 local set only via f64 arithmetic over rawF64
|
|
2545
|
+
// members is itself provably non-string. One forward pass suffices for DAG-shaped
|
|
2546
|
+
// straight-line code (the common case); a fixed-point loop covers rare mutual defs.
|
|
2547
|
+
// Collect local types first.
|
|
2548
|
+
const localTypeMap = new Map()
|
|
2549
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
2550
|
+
const d = fn[i]
|
|
2551
|
+
if (Array.isArray(d) && (d[0] === 'param' || d[0] === 'local') && typeof d[1] === 'string')
|
|
2552
|
+
localTypeMap.set(d[1], d[2])
|
|
2553
|
+
}
|
|
2554
|
+
// An expression is rawF64-valued if it only uses ops that stay in f64 and
|
|
2555
|
+
// reads only rawF64 locals (or f64.const). Stops early — we only need the
|
|
2556
|
+
// closed set for the pattern's $P operand.
|
|
2557
|
+
const isRawF64Expr = (n) => {
|
|
2558
|
+
if (!Array.isArray(n)) return false
|
|
2559
|
+
const op = n[0]
|
|
2560
|
+
if (op === 'f64.const') return true
|
|
2561
|
+
if (op === 'local.get' && typeof n[1] === 'string') return rawF64.has(n[1])
|
|
2562
|
+
if (op === 'local.tee' && typeof n[1] === 'string') return rawF64.has(n[1]) && isRawF64Expr(n[2])
|
|
2563
|
+
if (op === 'f64.add' || op === 'f64.sub' || op === 'f64.mul' || op === 'f64.div' ||
|
|
2564
|
+
op === 'f64.neg' || op === 'f64.abs' || op === 'f64.sqrt') {
|
|
2565
|
+
return n.slice(1).every(isRawF64Expr)
|
|
2566
|
+
}
|
|
2567
|
+
return false
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// Single forward pass: a local.set $v EXPR where EXPR is rawF64-valued makes $v rawF64.
|
|
2571
|
+
// Repeat until stable (handles ordering edge cases in non-DAG code).
|
|
2572
|
+
let changed = true
|
|
2573
|
+
while (changed) {
|
|
2574
|
+
changed = false
|
|
2575
|
+
const scan = (node) => {
|
|
2576
|
+
if (!Array.isArray(node)) return
|
|
2577
|
+
if ((node[0] === 'local.set' || node[0] === 'local.tee') && typeof node[1] === 'string' &&
|
|
2578
|
+
localTypeMap.get(node[1]) === 'f64' && !rawF64.has(node[1]) && isRawF64Expr(node[2])) {
|
|
2579
|
+
rawF64.add(node[1]); changed = true
|
|
2580
|
+
}
|
|
2581
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
2582
|
+
}
|
|
2583
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Pattern-match and fold in-place (bottom-up recursive walk so nested blocks resolve).
|
|
2587
|
+
const foldNode = (node) => {
|
|
2588
|
+
if (!Array.isArray(node)) return node
|
|
2589
|
+
// Recurse children first (bottom-up).
|
|
2590
|
+
for (let i = 0; i < node.length; i++) {
|
|
2591
|
+
const c = node[i]
|
|
2592
|
+
if (Array.isArray(c)) node[i] = foldNode(c)
|
|
2593
|
+
}
|
|
2594
|
+
// Match:
|
|
2595
|
+
// ['block', ['result','f64'],
|
|
2596
|
+
// ['local.set', $B, EXPR_A],
|
|
2597
|
+
// ['if', ['result','f64'],
|
|
2598
|
+
// ['call','$__is_str_key', ['i64.reinterpret_f64', ['local.tee',$C,['local.get',$P]]]],
|
|
2599
|
+
// ['then', ['call','$__str_concat',...]],
|
|
2600
|
+
// ['else', ['f64.add', ['local.get',$B], ['local.get',$C]]]]]
|
|
2601
|
+
if (node[0] !== 'block') return node
|
|
2602
|
+
if (!Array.isArray(node[1]) || node[1][0] !== 'result' || node[1][1] !== 'f64') return node
|
|
2603
|
+
if (node.length !== 4) return node
|
|
2604
|
+
const setStmt = node[2], ifStmt = node[3]
|
|
2605
|
+
if (!Array.isArray(setStmt) || setStmt[0] !== 'local.set' || typeof setStmt[1] !== 'string') return node
|
|
2606
|
+
const B = setStmt[1], exprA = setStmt[2]
|
|
2607
|
+
if (!Array.isArray(ifStmt) || ifStmt[0] !== 'if') return node
|
|
2608
|
+
// if must have: (result f64), cond, then, else — total 5 elements
|
|
2609
|
+
if (ifStmt.length !== 5) return node
|
|
2610
|
+
if (!Array.isArray(ifStmt[1]) || ifStmt[1][0] !== 'result' || ifStmt[1][1] !== 'f64') return node
|
|
2611
|
+
const cond = ifStmt[2], thenB = ifStmt[3], elseB = ifStmt[4]
|
|
2612
|
+
// cond: ['call','$__is_str_key',['i64.reinterpret_f64',['local.tee',$C,['local.get',$P]]]]
|
|
2613
|
+
if (!Array.isArray(cond) || cond[0] !== 'call' || cond[1] !== '$__is_str_key' || cond.length !== 3) return node
|
|
2614
|
+
const reinterpArg = cond[2]
|
|
2615
|
+
if (!Array.isArray(reinterpArg) || reinterpArg[0] !== 'i64.reinterpret_f64' || reinterpArg.length !== 2) return node
|
|
2616
|
+
const teeNode = reinterpArg[1]
|
|
2617
|
+
if (!Array.isArray(teeNode) || teeNode[0] !== 'local.tee' || typeof teeNode[1] !== 'string' || teeNode.length !== 3) return node
|
|
2618
|
+
const C = teeNode[1]
|
|
2619
|
+
const getP = teeNode[2]
|
|
2620
|
+
if (!Array.isArray(getP) || getP[0] !== 'local.get' || typeof getP[1] !== 'string') return node
|
|
2621
|
+
const P = getP[1]
|
|
2622
|
+
// $P must be a proven f64 local (never a string-key NaN-box)
|
|
2623
|
+
if (!rawF64.has(P)) return node
|
|
2624
|
+
// then: ['then', ['call','$__str_concat',...]]
|
|
2625
|
+
if (!Array.isArray(thenB) || thenB[0] !== 'then') return node
|
|
2626
|
+
// else: ['else', ['f64.add', ['local.get',$B], ['local.get',$C]]]
|
|
2627
|
+
if (!Array.isArray(elseB) || elseB[0] !== 'else' || elseB.length !== 2) return node
|
|
2628
|
+
const addExpr = elseB[1]
|
|
2629
|
+
if (!Array.isArray(addExpr) || addExpr[0] !== 'f64.add' || addExpr.length !== 3) return node
|
|
2630
|
+
// The two operands of f64.add must be local.get $B and local.get $C (in either order)
|
|
2631
|
+
const [lhsAdd, rhsAdd] = [addExpr[1], addExpr[2]]
|
|
2632
|
+
const lhsIsB = Array.isArray(lhsAdd) && lhsAdd[0] === 'local.get' && lhsAdd[1] === B
|
|
2633
|
+
const rhsIsC = Array.isArray(rhsAdd) && rhsAdd[0] === 'local.get' && rhsAdd[1] === C
|
|
2634
|
+
const lhsIsC = Array.isArray(lhsAdd) && lhsAdd[0] === 'local.get' && lhsAdd[1] === C
|
|
2635
|
+
const rhsIsB = Array.isArray(rhsAdd) && rhsAdd[0] === 'local.get' && rhsAdd[1] === B
|
|
2636
|
+
if (!((lhsIsB && rhsIsC) || (lhsIsC && rhsIsB))) return node
|
|
2637
|
+
// Match confirmed. Fold to: (f64.add EXPR_A (local.get $P))
|
|
2638
|
+
return ['f64.add', exprA, ['local.get', P]]
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = foldNode(fn[i])
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2377
2644
|
/**
|
|
2378
2645
|
* Run all per-function IR optimizations on a single function node.
|
|
2379
2646
|
* hoistPtrType runs first — it introduces new locals (`$__ptN`) that the fused
|
|
@@ -2432,7 +2699,11 @@ export function optimizeFunc(fn, cfg, globalTypes, volatileGlobals, phase = 'pre
|
|
|
2432
2699
|
if (cfg && cfg.vectorizeLaneLocal === true) {
|
|
2433
2700
|
const fullWatr = cfg.watr === true || typeof cfg.watr === 'object'
|
|
2434
2701
|
const runVectorizer = (fullWatr && phase === 'post') || (!fullWatr && phase !== 'post')
|
|
2435
|
-
|
|
2702
|
+
// Phase 1: fold dead string-dispatch blocks on proven-f64 locals BEFORE
|
|
2703
|
+
// the vectorizer pattern-matches — dead __is_str_key calls in $fbm-style
|
|
2704
|
+
// functions (param f64 + op f64) block liftPPC from recognizing them as pure.
|
|
2705
|
+
if (runVectorizer) foldStrDispatchF64(fn)
|
|
2706
|
+
if (runVectorizer) vectorizeLaneLocal(fn, cfg.reduceUnroll === true, cfg.relaxedSimd === true, cfg.blurMultiPixel !== false, cfg.whyNotSimd === true, cfg.experimentalStencil !== false, cfg.experimentalOuterStrip !== false, cfg._pureFuncMap || null, cfg.experimentalToneMap !== false)
|
|
2436
2707
|
}
|
|
2437
2708
|
if (!cfg || cfg.sortLocalsByUse !== false) sortLocalsByUse(fn, cfg && cfg.fusedRewrite !== false ? counts : null)
|
|
2438
2709
|
// An optimizer pass that emits a malformed local — the class that otherwise dies
|
|
@@ -2836,6 +3107,13 @@ export function treeshake(funcSections, allModuleNodes, opts) {
|
|
|
2836
3107
|
for (let i = 2; i < fn.length; i++)
|
|
2837
3108
|
if (Array.isArray(fn[i]) && fn[i][0] === 'export') { addRoot(name); break }
|
|
2838
3109
|
|
|
3110
|
+
// When user funcs are NOT being reclaimed (O0/O1 keep declared-but-uncalled ones), they
|
|
3111
|
+
// all survive — so they're roots for the *internal*-func reachability below. Otherwise an
|
|
3112
|
+
// unreachable user func that's kept would still call a `__helper`, yet that helper would be
|
|
3113
|
+
// pruned as unreached-from-exports, leaving a dangling `call $__helper`.
|
|
3114
|
+
if (!removeDead && opts && opts.userFuncs)
|
|
3115
|
+
for (const name of opts.userFuncs) addRoot(name)
|
|
3116
|
+
|
|
2839
3117
|
const findRoots = (node) => {
|
|
2840
3118
|
if (!Array.isArray(node)) return
|
|
2841
3119
|
if (node[0] === 'start' && typeof node[1] === 'string') addRoot(node[1])
|
|
@@ -2863,24 +3141,43 @@ export function treeshake(funcSections, allModuleNodes, opts) {
|
|
|
2863
3141
|
for (const fn of allFuncs) if (typeof fn[1] !== 'string') visitCalls(fn)
|
|
2864
3142
|
while (stack.length) visitCalls(funcByName.get(stack.pop()))
|
|
2865
3143
|
|
|
3144
|
+
// Compiler-internal funcs (stdlib helpers, allocator wrappers — everything not in the
|
|
3145
|
+
// user's own `ctx.func.list`) carry no source meaning, so an unreachable one is reclaimed
|
|
3146
|
+
// at EVERY opt level: it's never a live-coding aid, just over-production (e.g. `s + '!'`
|
|
3147
|
+
// pulls the alloc trio's `__alloc_hdr`, which string concat never calls, and a dead-branch
|
|
3148
|
+
// dep like `__str_len`). User funcs are reclaimed only when DCE is on, so O0/O1 keep a
|
|
3149
|
+
// declared-but-uncalled user function. Absent the set, fall back to gating everything.
|
|
3150
|
+
const userFuncs = opts && opts.userFuncs
|
|
3151
|
+
const isUserFunc = (name) => userFuncs ? userFuncs.has(name) : true
|
|
2866
3152
|
let removed = 0
|
|
2867
|
-
if (removeDead) {
|
|
3153
|
+
if (removeDead || userFuncs) {
|
|
2868
3154
|
for (const { arr } of funcSections) {
|
|
2869
3155
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
2870
3156
|
const n = arr[i]
|
|
2871
|
-
if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string' && !reachable.has(n[1])
|
|
3157
|
+
if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string' && !reachable.has(n[1]) &&
|
|
3158
|
+
(removeDead || !isUserFunc(n[1]))) {
|
|
2872
3159
|
arr.splice(i, 1); removed++
|
|
2873
3160
|
}
|
|
2874
3161
|
}
|
|
2875
3162
|
}
|
|
2876
3163
|
}
|
|
2877
3164
|
|
|
2878
|
-
// Dead-global elimination:
|
|
2879
|
-
//
|
|
2880
|
-
//
|
|
2881
|
-
//
|
|
2882
|
-
//
|
|
2883
|
-
|
|
3165
|
+
// Dead-global elimination: drop `(global $g …)` decls that nothing references
|
|
3166
|
+
// (a `global.get`/`global.set` in a remaining func, a kept global's init expr, a
|
|
3167
|
+
// data/elem offset, or an `(export … (global $g))`). Imported globals live in
|
|
3168
|
+
// `allModuleNodes`, not in `opts.globals`, so they're never touched. Fixpoint: a
|
|
3169
|
+
// kept global's init may reference another global.
|
|
3170
|
+
//
|
|
3171
|
+
// Compiler-internal globals (support state the user never wrote — e.g. core's
|
|
3172
|
+
// `__heap_start` or the math module's `rng_state`, declared eagerly but read
|
|
3173
|
+
// only by specific fast paths) are reclaimed at *every* level: leaving an
|
|
3174
|
+
// unreferenced one in the output is pure noise, never a live-coding aid. User
|
|
3175
|
+
// globals are reclaimed only when DCE is on, so O0/O1 still preserve declared-
|
|
3176
|
+
// but-unused user bindings. `userGlobals` (names sans `$`) draws the line; absent
|
|
3177
|
+
// it, fall back to the `$__` reserved-prefix heuristic.
|
|
3178
|
+
const userGlobals = opts && opts.userGlobals
|
|
3179
|
+
const isUserGlobal = (name) => userGlobals ? userGlobals.has(name.slice(1)) : !name.startsWith('$__')
|
|
3180
|
+
const globals = opts && Array.isArray(opts.globals) ? opts.globals : null
|
|
2884
3181
|
if (globals) {
|
|
2885
3182
|
const collectGlobalRefs = (node, refd) => {
|
|
2886
3183
|
if (!Array.isArray(node)) return
|
|
@@ -2897,9 +3194,11 @@ export function treeshake(funcSections, allModuleNodes, opts) {
|
|
|
2897
3194
|
for (const g of globals) collectGlobalRefs(g, refd)
|
|
2898
3195
|
for (let i = globals.length - 1; i >= 0; i--) {
|
|
2899
3196
|
const g = globals[i]
|
|
2900
|
-
if (Array.isArray(g)
|
|
2901
|
-
|
|
2902
|
-
|
|
3197
|
+
if (!Array.isArray(g) || g[0] !== 'global' || typeof g[1] !== 'string' || refd.has(g[1])) continue
|
|
3198
|
+
// An inline `(export …)` on the decl pins it — it's part of the module's
|
|
3199
|
+
// JS-host surface (e.g. `__heap`), referenced from outside the wasm.
|
|
3200
|
+
if (g.some(c => Array.isArray(c) && c[0] === 'export')) continue
|
|
3201
|
+
if (removeDead || !isUserGlobal(g[1])) { globals.splice(i, 1); changed = true }
|
|
2903
3202
|
}
|
|
2904
3203
|
}
|
|
2905
3204
|
}
|