jz 0.8.1 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +299 -44
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +892 -32
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
|
@@ -533,7 +533,11 @@ const slpStorePairsIn = (node, fnLocals, freshIdRef, newLocalDecls, getCounts) =
|
|
|
533
533
|
for (let i = 0; i < node.length; i++) {
|
|
534
534
|
const u0 = slpUnitAt(node, i, getCounts)
|
|
535
535
|
if (!u0) continue
|
|
536
|
-
|
|
536
|
+
// u1's MATCH index is its store's index, which for the flat tee'd shape is one PAST
|
|
537
|
+
// its span's lo (the tee'd `local.set` precedes it) — try both hi+1 (inline/block-wrapped,
|
|
538
|
+
// where lo===hi) and hi+2 (flat tee'd, where the store sits at lo+1) and keep whichever
|
|
539
|
+
// yields a unit that actually starts right after u0.
|
|
540
|
+
const u1 = slpUnitAt(node, u0.hi + 1, getCounts) || slpUnitAt(node, u0.hi + 2, getCounts)
|
|
537
541
|
if (!u1 || u1.lo !== u0.hi + 1) continue
|
|
538
542
|
if (u1.off - u0.off !== 8 || !slpSameBase(u0.addr, u1.addr)) continue
|
|
539
543
|
// RAW hazard: the high store's value must not read the low store's target — the pack
|
|
@@ -916,21 +920,61 @@ function firstAccess(node, name) {
|
|
|
916
920
|
*
|
|
917
921
|
* Returns { strideLog2, teeName?: string } or null.
|
|
918
922
|
*/
|
|
919
|
-
|
|
923
|
+
// The per-iteration element count P (≥2) of an array-of-structs index `P*ind`, or null. Accepts the
|
|
924
|
+
// `(i32.mul P ind)`/`(i32.mul ind P)` form (non-power-of-2 P — RGB's 3) AND the strength-reduced
|
|
925
|
+
// power-of-2 form `(i32.shl ind S)` = ind·2^S (a `const j = 2*i`/`4*i` folds to a shift — complex/
|
|
926
|
+
// RGBA). P is the ELEMENT stride, not a byte offset.
|
|
927
|
+
function matchConstMulIV(node, ind) {
|
|
928
|
+
if (!isArr(node) || node.length !== 3) return null
|
|
929
|
+
if (node[0] === 'i32.mul') {
|
|
930
|
+
let p = null
|
|
931
|
+
if (isLocalGet(node[1], ind)) p = constNum(node[2])
|
|
932
|
+
else if (isLocalGet(node[2], ind)) p = constNum(node[1])
|
|
933
|
+
return (p != null && p >= 2 && p <= 64) ? p : null
|
|
934
|
+
}
|
|
935
|
+
if (node[0] === 'i32.shl' && isLocalGet(node[1], ind)) {
|
|
936
|
+
const s = constNum(node[2])
|
|
937
|
+
if (s != null && s >= 1 && s <= 6) return 1 << s // ind·2^S, stride 2..64
|
|
938
|
+
}
|
|
939
|
+
return null
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function matchLaneOffset(off, ind, offsetTees, allowAos, aosPix, idxTees) {
|
|
920
943
|
if (isArr(off) && off[0] === 'local.get' && typeof off[1] === 'string' &&
|
|
921
944
|
offsetTees && offsetTees.has(off[1])) {
|
|
922
|
-
return { strideLog2: offsetTees.get(off[1]), teeName: null }
|
|
945
|
+
return { strideLog2: offsetTees.get(off[1]), pixelStride: (aosPix && aosPix.get(off[1])) || 1, teeName: null }
|
|
923
946
|
}
|
|
924
947
|
let teeName = null
|
|
925
948
|
let n = off
|
|
926
949
|
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) { teeName = n[1]; n = n[2] }
|
|
927
|
-
// (i32.shl
|
|
928
|
-
if (isArr(n) && n[0] === 'i32.shl' && n.length === 3
|
|
950
|
+
// (i32.shl <ind-scaled> (i32.const K))
|
|
951
|
+
if (isArr(n) && n[0] === 'i32.shl' && n.length === 3) {
|
|
929
952
|
const k = constNum(n[2])
|
|
930
|
-
if (k != null && k >= 0 && k <= 3)
|
|
953
|
+
if (k != null && k >= 0 && k <= 3) {
|
|
954
|
+
if (isLocalGet(n[1], ind)) return { strideLog2: k, pixelStride: 1, teeName }
|
|
955
|
+
// AoS (array-of-structs / interleaved channels), P elements per iteration, element
|
|
956
|
+
// byte-size 1<<K. Only under allowAos (tryVectorize gathers/scatters the lanes); every
|
|
957
|
+
// other recognizer returns null here. Two shapes: the folded `(i32.mul P ind)`, and a
|
|
958
|
+
// pixel-index local `(local.get $J)` with $J = P*ind (the `const j = P*i`, via idxTees).
|
|
959
|
+
if (allowAos) {
|
|
960
|
+
const p = matchConstMulIV(n[1], ind)
|
|
961
|
+
if (p != null) return { strideLog2: k, pixelStride: p, teeName }
|
|
962
|
+
if (isArr(n[1]) && n[1][0] === 'local.get' && idxTees && idxTees.has(n[1][1])) {
|
|
963
|
+
const pj = idxTees.get(n[1][1])
|
|
964
|
+
if (pj > 1) return { strideLog2: k, pixelStride: pj, teeName }
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// POWER-OF-2 AoS stride: `a[P*i]` (P = 2,4,8 — complex/RGBA/…) folds `(P*i)<<3` to a single
|
|
969
|
+
// `(i32.shl ind K)` with K = 3 + log2(P) > 3. The excess shift over the f64 element (3) is the
|
|
970
|
+
// pixel stride. f64 lane only — scanForLoadsStores' `1<<strideLog2 === elemSize` gate keeps
|
|
971
|
+
// strideLog2=3 (=8 bytes), so a folded narrower-lane stride can't be mis-accepted here.
|
|
972
|
+
else if (allowAos && k != null && k >= 4 && k <= 9 && isLocalGet(n[1], ind)) {
|
|
973
|
+
return { strideLog2: 3, pixelStride: 1 << (k - 3), teeName }
|
|
974
|
+
}
|
|
931
975
|
}
|
|
932
976
|
// (local.get ind) — stride 1
|
|
933
|
-
if (isLocalGet(n, ind)) return { strideLog2: 0, teeName }
|
|
977
|
+
if (isLocalGet(n, ind)) return { strideLog2: 0, pixelStride: 1, teeName }
|
|
934
978
|
return null
|
|
935
979
|
}
|
|
936
980
|
|
|
@@ -944,13 +988,13 @@ function matchLaneOffset(off, ind, offsetTees) {
|
|
|
944
988
|
* `strideLog2` = K for i32.shl form, 0 for plain add form.
|
|
945
989
|
* `base` is the loop-invariant base subtree.
|
|
946
990
|
*/
|
|
947
|
-
function matchLaneAddr(addr, ind, addrLocals, offsetTees) {
|
|
991
|
+
function matchLaneAddr(addr, ind, addrLocals, offsetTees, allowAos, aosPix, idxTees) {
|
|
948
992
|
let teeName = null
|
|
949
993
|
let n = addr
|
|
950
994
|
// (local.get $A) where $A holds a previously-tee'd FULL lane-address.
|
|
951
995
|
if (isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' && addrLocals && addrLocals.has(n[1])) {
|
|
952
996
|
const e = addrLocals.get(n[1])
|
|
953
|
-
return { strideLog2: e.strideLog2, base: e.base, teeName: null, viaLocal: n[1] }
|
|
997
|
+
return { strideLog2: e.strideLog2, pixelStride: e.pixelStride || 1, base: e.base, teeName: null, viaLocal: n[1] }
|
|
954
998
|
}
|
|
955
999
|
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) {
|
|
956
1000
|
teeName = n[1]
|
|
@@ -958,9 +1002,9 @@ function matchLaneAddr(addr, ind, addrLocals, offsetTees) {
|
|
|
958
1002
|
}
|
|
959
1003
|
if (!isArr(n) || n[0] !== 'i32.add' || n.length !== 3) return null
|
|
960
1004
|
const a = n[1], b = n[2]
|
|
961
|
-
const off = matchLaneOffset(b, ind, offsetTees)
|
|
1005
|
+
const off = matchLaneOffset(b, ind, offsetTees, allowAos, aosPix, idxTees)
|
|
962
1006
|
if (!off) return null
|
|
963
|
-
return { strideLog2: off.strideLog2, base: a, teeName, offsetTeeName: off.teeName }
|
|
1007
|
+
return { strideLog2: off.strideLog2, pixelStride: off.pixelStride || 1, base: a, teeName, offsetTeeName: off.teeName }
|
|
964
1008
|
}
|
|
965
1009
|
|
|
966
1010
|
/**
|
|
@@ -969,17 +1013,30 @@ function matchLaneAddr(addr, ind, addrLocals, offsetTees) {
|
|
|
969
1013
|
* Returns the consistent strideLog2, or null if any write to it diverges.
|
|
970
1014
|
* This soundness check backs every `(local.get $T)` resolved via `offsetTees`.
|
|
971
1015
|
*/
|
|
972
|
-
function _offsetLocalStride(body, name, ind) {
|
|
973
|
-
let stride = null, found = false, ok = true
|
|
1016
|
+
function _offsetLocalStride(body, name, ind, allowAos, idxTees) {
|
|
1017
|
+
let stride = null, found = false, ok = true, pix = null
|
|
974
1018
|
function walk(n) {
|
|
975
1019
|
if (!isArr(n)) return
|
|
976
1020
|
if ((n[0] === 'local.tee' || n[0] === 'local.set') && n[1] === name && n.length === 3) {
|
|
977
1021
|
found = true
|
|
978
1022
|
const v = n[2]
|
|
979
1023
|
let k = null
|
|
980
|
-
if (isArr(v) && v[0] === 'i32.shl' && v.length === 3
|
|
981
|
-
|
|
982
|
-
|
|
1024
|
+
if (isArr(v) && v[0] === 'i32.shl' && v.length === 3) {
|
|
1025
|
+
const kk = constNum(v[2])
|
|
1026
|
+
// Power-of-2 AoS offset local `(i32.shl ind K)` with K>3 — matches matchLaneOffset's
|
|
1027
|
+
// power-of-2 arm: element shift 3, pixel stride 2^(K-3). Verify all writes share one P.
|
|
1028
|
+
if (allowAos && isLocalGet(v[1], ind) && kk != null && kk >= 4 && kk <= 9) {
|
|
1029
|
+
k = 3; const p = 1 << (kk - 3); if (pix == null) pix = p; else if (pix !== p) ok = false
|
|
1030
|
+
}
|
|
1031
|
+
else if (isLocalGet(v[1], ind)) { k = kk; if (k == null || k < 0 || k > 3) ok = false }
|
|
1032
|
+
else if (allowAos && kk != null && kk >= 0 && kk <= 3) {
|
|
1033
|
+
// AoS offset local (i32.shl <P·ind> K) — folded `(i32.mul P ind)` or a pixel-index
|
|
1034
|
+
// local `(local.get $J)` with $J = P·ind (idxTees). Verify every write shares one P.
|
|
1035
|
+
let p = matchConstMulIV(v[1], ind)
|
|
1036
|
+
if (p == null && isArr(v[1]) && v[1][0] === 'local.get' && idxTees && idxTees.get(v[1][1]) > 1) p = idxTees.get(v[1][1])
|
|
1037
|
+
if (p != null) { k = kk; if (pix == null) pix = p; else if (pix !== p) ok = false }
|
|
1038
|
+
else ok = false
|
|
1039
|
+
} else ok = false
|
|
983
1040
|
} else if (isLocalGet(v, ind)) {
|
|
984
1041
|
k = 0
|
|
985
1042
|
} else ok = false
|
|
@@ -994,6 +1051,16 @@ function _offsetLocalStride(body, name, ind) {
|
|
|
994
1051
|
return found && ok ? stride : null
|
|
995
1052
|
}
|
|
996
1053
|
|
|
1054
|
+
// True if the tree contains any branch or return — control flow that a flattened value-block
|
|
1055
|
+
// lift can't preserve (an early `br`/`return` out of the block changes which value is produced).
|
|
1056
|
+
const hasBranchOrReturn = (node) => {
|
|
1057
|
+
if (!isArr(node)) return false
|
|
1058
|
+
const op = node[0]
|
|
1059
|
+
if (op === 'br' || op === 'br_if' || op === 'br_table' || op === 'return') return true
|
|
1060
|
+
for (let i = 1; i < node.length; i++) if (hasBranchOrReturn(node[i])) return true
|
|
1061
|
+
return false
|
|
1062
|
+
}
|
|
1063
|
+
|
|
997
1064
|
// True if any node in the tree writes a global. When false for a loop body,
|
|
998
1065
|
// every `global.get` inside it is loop-invariant — safe to splat for SIMD.
|
|
999
1066
|
const hasGlobalSet = (node) => {
|
|
@@ -1044,6 +1111,74 @@ const hasSideEffect = (node) => {
|
|
|
1044
1111
|
* a non-`$__li` preamble, an impure value, or any array content AFTER the loop
|
|
1045
1112
|
* bails. When false, ANY non-loop array content in the block bails.
|
|
1046
1113
|
*/
|
|
1114
|
+
// A transparent block — no label (first child isn't a `$label` string) and no result — is
|
|
1115
|
+
// pure statement grouping: wasm locals are function-scoped, and an unlabeled resultless block
|
|
1116
|
+
// is neither a branch target nor a value producer, so it can ONLY appear in statement position
|
|
1117
|
+
// (a resultless block in value position is a type error). jz emits one per source statement
|
|
1118
|
+
// group; watr's mergeBlocks/vacuum flattens them post-hoc. The vectorizer is jz LOWERING
|
|
1119
|
+
// (pre-watr), so it normalizes them itself IN PLACE — splicing each transparent block's
|
|
1120
|
+
// children into its parent's statement list — so every recognizer (scaffold-consuming AND
|
|
1121
|
+
// raw-node) reads the flat statement lists they were tuned against. Post-order: children are
|
|
1122
|
+
// already flat when a block is spliced up. A labeled block (branch target) or result-carrying
|
|
1123
|
+
// block (value producer) is kept — including the `(block $brk (loop …))` SIMD scaffold itself.
|
|
1124
|
+
function normalizeTransparentBlocks(node) {
|
|
1125
|
+
if (!isArr(node)) return
|
|
1126
|
+
for (let i = 1; i < node.length; i++) normalizeTransparentBlocks(node[i])
|
|
1127
|
+
for (let i = node.length - 1; i >= 1; i--) {
|
|
1128
|
+
const c = node[i]
|
|
1129
|
+
if (isArr(c) && c[0] === 'block' &&
|
|
1130
|
+
!(typeof c[1] === 'string' && c[1].startsWith('$')) &&
|
|
1131
|
+
!(isArr(c[1]) && c[1][0] === 'result'))
|
|
1132
|
+
node.splice(i, 1, ...c.slice(1))
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Fold the arithmetic identities watr's `identity` pass removes but jz emits raw — most
|
|
1137
|
+
// importantly `i<<0` (a byte-stride address for a u8/i8 array: `base + (i << 0)`), which the
|
|
1138
|
+
// vectorizer's address matchers, tuned on watr's folded IR, read as bare `i`. Also the trivial
|
|
1139
|
+
// `x±0`, `x|0`, `x^0`, `x<<0/>>0`, `x*1`. In place, bottom-up; returns the (possibly folded)
|
|
1140
|
+
// node so a parent can rebind. Pure syntactic identities — always sound, watr-equivalent.
|
|
1141
|
+
function foldVecIdentities(node) {
|
|
1142
|
+
if (!isArr(node)) return node
|
|
1143
|
+
for (let i = 1; i < node.length; i++) node[i] = foldVecIdentities(node[i])
|
|
1144
|
+
if (node.length !== 3) return node
|
|
1145
|
+
const op = node[0], a = node[1], b = node[2]
|
|
1146
|
+
const ci = (n) => isArr(n) && (n[0] === 'i32.const' || n[0] === 'i64.const') ? Number(n[1]) : NaN
|
|
1147
|
+
const rb = ci(b), ra = ci(a)
|
|
1148
|
+
switch (op) {
|
|
1149
|
+
case 'i32.shl': case 'i32.shr_s': case 'i32.shr_u':
|
|
1150
|
+
case 'i64.shl': case 'i64.shr_s': case 'i64.shr_u':
|
|
1151
|
+
return rb === 0 ? a : node // x << 0 = x (right-identity only)
|
|
1152
|
+
case 'i32.add': case 'i32.or': case 'i32.xor':
|
|
1153
|
+
case 'i64.add': case 'i64.or': case 'i64.xor':
|
|
1154
|
+
return rb === 0 ? a : (ra === 0 ? b : node) // x±0, x|0, x^0 (either side)
|
|
1155
|
+
case 'i32.sub': case 'i64.sub':
|
|
1156
|
+
return rb === 0 ? a : node // x - 0 = x
|
|
1157
|
+
case 'i32.mul': case 'i64.mul':
|
|
1158
|
+
return rb === 1 ? a : (ra === 1 ? b : node) // x*1
|
|
1159
|
+
default: return node
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Canonicalize jz's `if COND (then (br L))` break-idiom to watr's `br_if L COND` — the shape the
|
|
1164
|
+
// loop-scan recognizers (byte-scan, divergent-escape) match. watr's `brif` pass does this
|
|
1165
|
+
// post-hoc; the pre-watr vectorizer needs it now. Only the statement-form (no result), no-else,
|
|
1166
|
+
// single-`br`-then shape becomes a br_if — anything richer is left untouched for watr. In place,
|
|
1167
|
+
// top-down (a converted br_if has no nested `if` to revisit).
|
|
1168
|
+
function canonicalizeIfBr(node) {
|
|
1169
|
+
if (!isArr(node)) return
|
|
1170
|
+
for (let i = 1; i < node.length; i++) {
|
|
1171
|
+
const c = node[i]
|
|
1172
|
+
if (isArr(c) && c[0] === 'if' && c.length === 3 &&
|
|
1173
|
+
!(isArr(c[1]) && c[1][0] === 'result') &&
|
|
1174
|
+
isArr(c[2]) && c[2][0] === 'then' && c[2].length === 2 &&
|
|
1175
|
+
isArr(c[2][1]) && c[2][1][0] === 'br' && c[2][1].length === 2 && typeof c[2][1][1] === 'string')
|
|
1176
|
+
// `(br L)` only — a value-carrying `(br L v)` would lose its operand under br_if's 2-arg form.
|
|
1177
|
+
node[i] = ['br_if', c[2][1][1], c[1]]
|
|
1178
|
+
else canonicalizeIfBr(c)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1047
1182
|
function matchBlockLoop(blockNode, opts = {}) {
|
|
1048
1183
|
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
1049
1184
|
const allowPreamble = !!opts.allowPreamble
|
|
@@ -1104,7 +1239,7 @@ function matchBlockLoop(blockNode, opts = {}) {
|
|
|
1104
1239
|
* Try to vectorize the inner loop. Returns the replacement node array
|
|
1105
1240
|
* (synthetic outer block) or null on no match.
|
|
1106
1241
|
*/
|
|
1107
|
-
function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
1242
|
+
function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
|
|
1108
1243
|
// Consumes the shared scaffold descriptor (matchBlockLoop, computed once by the
|
|
1109
1244
|
// dispatch). The LICM `$__li` preamble is cloned ahead of the SIMD block; each
|
|
1110
1245
|
// set is pure & loop-invariant, so the kept scalar tail harmlessly re-runs it.
|
|
@@ -1125,6 +1260,28 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1125
1260
|
// Offset tees: name → strideLog2. A CSE'd `i << K` shared across base
|
|
1126
1261
|
// pointers (map loops over distinct arrays). Soundness re-checked post-scan.
|
|
1127
1262
|
const offsetTees = new Map()
|
|
1263
|
+
// AoS (array-of-structs) de-interleave: this recognizer alone accepts a pixel-stride
|
|
1264
|
+
// access `base[P*i + c]` (interleaved RGB/vec3/complex). allowAos enables it in every
|
|
1265
|
+
// shared matcher; aosPix carries P for a CSE'd offset tee; aosPixelStride is the loop's
|
|
1266
|
+
// single P (1 = plain stride-1, unchanged). Set once, verified equal across all sites.
|
|
1267
|
+
const allowAos = true
|
|
1268
|
+
const aosPix = new Map()
|
|
1269
|
+
let aosPixelStride = 1
|
|
1270
|
+
// Pixel-INDEX locals: `$J = P*i` (the `const j = 3*i` of an AoS loop, kept as its own local
|
|
1271
|
+
// pre-watr). A channel address is then `base + ((local.get $J) << K)`; idxTees lets
|
|
1272
|
+
// matchLaneOffset resolve $J → pixel-stride P. Value -1 marks an inconsistent local (bail).
|
|
1273
|
+
const idxTees = new Map()
|
|
1274
|
+
{
|
|
1275
|
+
const walk = (n) => {
|
|
1276
|
+
if (!isArr(n)) return
|
|
1277
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string' && n.length === 3) {
|
|
1278
|
+
const p = matchConstMulIV(n[2], incVar)
|
|
1279
|
+
if (p != null) idxTees.set(n[1], idxTees.has(n[1]) && idxTees.get(n[1]) !== p ? -1 : p)
|
|
1280
|
+
}
|
|
1281
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
1282
|
+
}
|
|
1283
|
+
for (const s of body) walk(s)
|
|
1284
|
+
}
|
|
1128
1285
|
|
|
1129
1286
|
// The compute/lane type is the WIDEST FLOAT among all loads+stores. A narrower
|
|
1130
1287
|
// float/int LOAD is then a widening read (INT_WIDEN_F32 / f32→f64), a narrower
|
|
@@ -1146,6 +1303,26 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1146
1303
|
for (const s of body) scanFloatWidth(s)
|
|
1147
1304
|
if (preFloat) { laneType = preFloat; stride = LANE_INFO[preFloat].stride }
|
|
1148
1305
|
|
|
1306
|
+
// Record a memory site's pixel stride. An AoS stride (P>1) is f64-lane only (the gather/scatter
|
|
1307
|
+
// lifts 2 f64 lanes). Strides are collected for the post-scan uniformity gate: EVERY site must
|
|
1308
|
+
// share one stride, else the loop mixes stride-1 and stride-P accesses (e.g. AoS-struct loads
|
|
1309
|
+
// feeding stride-1 array stores) and a single gather/scatter delta would corrupt the odd sites.
|
|
1310
|
+
const siteStrides = []
|
|
1311
|
+
const recordAos = (m) => {
|
|
1312
|
+
const ps = m.pixelStride || 1
|
|
1313
|
+
if (ps > 1) {
|
|
1314
|
+
if (laneType !== 'f64') return false
|
|
1315
|
+
if (aosPixelStride === 1) aosPixelStride = ps
|
|
1316
|
+
if (m.offsetTeeName) aosPix.set(m.offsetTeeName, ps)
|
|
1317
|
+
}
|
|
1318
|
+
siteStrides.push(ps)
|
|
1319
|
+
return true
|
|
1320
|
+
}
|
|
1321
|
+
// The real address is node[1], unless a folded `offset=N` memarg precedes it (node[1] is the
|
|
1322
|
+
// string `offset=N`, node[2] the address) — the AoS channels `d[j+1]`,`d[j+2]` and stencil
|
|
1323
|
+
// neighbours arrive this way.
|
|
1324
|
+
const memAddr = (node) => (typeof node[1] === 'string' && node[1].startsWith('offset=')) ? node[2] : node[1]
|
|
1325
|
+
|
|
1149
1326
|
function scanForLoadsStores(node, parent, pi) {
|
|
1150
1327
|
if (!isArr(node)) return true
|
|
1151
1328
|
const op = node[0]
|
|
@@ -1162,10 +1339,11 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1162
1339
|
} else if (lt !== laneType && !widenInt) {
|
|
1163
1340
|
return false
|
|
1164
1341
|
}
|
|
1165
|
-
const m = matchLaneAddr(node
|
|
1342
|
+
const m = matchLaneAddr(memAddr(node), incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
|
|
1166
1343
|
if (!m) return false
|
|
1167
1344
|
if ((1 << m.strideLog2) !== (widenInt ? LANE_INFO[lt].stride : stride)) return false
|
|
1168
|
-
if (m
|
|
1345
|
+
if (!recordAos(m)) return false
|
|
1346
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, pixelStride: m.pixelStride, base: m.base })
|
|
1169
1347
|
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
1170
1348
|
loadStoreSites.push({ parent, idx: pi, kind: 'load' })
|
|
1171
1349
|
return true
|
|
@@ -1178,27 +1356,30 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1178
1356
|
const narrowing = laneType != null && sty !== laneType && isNarrowStore(laneType, sty)
|
|
1179
1357
|
if (laneType != null && sty !== laneType && !narrowing) return false
|
|
1180
1358
|
if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
|
|
1181
|
-
const
|
|
1359
|
+
const memarg = typeof node[1] === 'string' && node[1].startsWith('offset=')
|
|
1360
|
+
const m = matchLaneAddr(memAddr(node), incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
|
|
1182
1361
|
if (!m) return false
|
|
1183
1362
|
if ((1 << m.strideLog2) !== (narrowing ? LANE_INFO[sty].stride : stride)) return false
|
|
1184
|
-
if (m
|
|
1363
|
+
if (!recordAos(m)) return false
|
|
1364
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, pixelStride: m.pixelStride, base: m.base })
|
|
1185
1365
|
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
1186
1366
|
loadStoreSites.push({ parent, idx: pi, kind: 'store' })
|
|
1187
|
-
// Recurse into VALUE child (idx 2) — it's data, not address.
|
|
1188
|
-
|
|
1367
|
+
// Recurse into VALUE child (idx 2, or 3 past an offset= memarg) — it's data, not address.
|
|
1368
|
+
const valIdx = memarg ? 3 : 2
|
|
1369
|
+
if (!scanForLoadsStores(node[valIdx], node, valIdx)) return false
|
|
1189
1370
|
return true
|
|
1190
1371
|
}
|
|
1191
1372
|
// local.set/tee of an address local outside a load/store context (e.g.
|
|
1192
1373
|
// `(local.set $a (i32.add base (i32.shl i 2)))` as a standalone stmt) —
|
|
1193
1374
|
// record so a later `(local.get $a)` resolves.
|
|
1194
1375
|
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string' && node.length === 3) {
|
|
1195
|
-
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals, offsetTees)
|
|
1376
|
+
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
|
|
1196
1377
|
if (valM && valM.teeName) {
|
|
1197
|
-
addrLocals.set(valM.teeName, { strideLog2: valM.strideLog2, base: valM.base })
|
|
1378
|
+
addrLocals.set(valM.teeName, { strideLog2: valM.strideLog2, pixelStride: valM.pixelStride, base: valM.base })
|
|
1198
1379
|
}
|
|
1199
|
-
// Standalone offset compute: `(local.set $t (i32.shl i K))
|
|
1200
|
-
const offM = matchLaneOffset(node[2], incVar, offsetTees)
|
|
1201
|
-
if (offM) offsetTees.set(node[1], offM.strideLog2)
|
|
1380
|
+
// Standalone offset compute: `(local.set $t (i32.shl i K))` (or AoS `(i32.shl (mul P i) K)`).
|
|
1381
|
+
const offM = matchLaneOffset(node[2], incVar, offsetTees, allowAos, aosPix, idxTees)
|
|
1382
|
+
if (offM) { offsetTees.set(node[1], offM.strideLog2); if (offM.pixelStride > 1) aosPix.set(node[1], offM.pixelStride) }
|
|
1202
1383
|
}
|
|
1203
1384
|
// Recurse into all children
|
|
1204
1385
|
for (let i = 1; i < node.length; i++) {
|
|
@@ -1212,11 +1393,14 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1212
1393
|
if (body.some(hasGlobalSet)) return null // a global write breaks the "global.get is invariant" splat
|
|
1213
1394
|
if (!laneType) return null // no memory ops — vectorizing buys nothing
|
|
1214
1395
|
if (loadStoreSites.length === 0) return null
|
|
1396
|
+
// Uniform stride gate: an AoS loop must have EVERY load/store at the same pixel stride. A mix of
|
|
1397
|
+
// stride-1 and stride-P sites can't share one lift stride — bail (stays scalar, always correct).
|
|
1398
|
+
if (aosPixelStride > 1 && siteStrides.some(s => s !== aosPixelStride)) return null
|
|
1215
1399
|
|
|
1216
1400
|
// Soundness gate for offset-tee resolution: every `(local.get $T)` we
|
|
1217
1401
|
// accepted as `i << K` is only valid if EVERY write of $T is that offset.
|
|
1218
1402
|
for (const [name, k] of offsetTees) {
|
|
1219
|
-
if (_offsetLocalStride(body, name, incVar) !== k) return null
|
|
1403
|
+
if (_offsetLocalStride(body, name, incVar, allowAos, idxTees) !== k) return null
|
|
1220
1404
|
}
|
|
1221
1405
|
|
|
1222
1406
|
// Classify all locals referenced in body.
|
|
@@ -1252,7 +1436,7 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1252
1436
|
// Discriminate lane-data vs address-tee. Address tees hold i32 addresses,
|
|
1253
1437
|
// not vector data. We classify by checking the local's declared type.
|
|
1254
1438
|
const decl = fnLocals.get(name)
|
|
1255
|
-
if (decl === 'i32' && (offsetTees.has(name) || _isAddressLocal(body, name, incVar))) {
|
|
1439
|
+
if (decl === 'i32' && (addrLocals.has(name) || offsetTees.has(name) || _isAddressLocal(body, name, incVar) || _isPixelIndexLocal(body, name, incVar))) {
|
|
1256
1440
|
localKind.set(name, 'addr')
|
|
1257
1441
|
} else {
|
|
1258
1442
|
localKind.set(name, 'lane')
|
|
@@ -1285,10 +1469,43 @@ function tryVectorize(bl, fnLocals, freshIdRef) {
|
|
|
1285
1469
|
if (dropped.size) body2 = inlined.filter(s => !dropped.has(s))
|
|
1286
1470
|
}
|
|
1287
1471
|
|
|
1472
|
+
// A signum ternary (`a<0?-1:1`) is commonly CSE'd into its own lane-local just before its
|
|
1473
|
+
// sole use (`set $s (select (i32.const -1)(i32.const 1) COND); … f64.convert_i32_s($s) …`),
|
|
1474
|
+
// hiding it from liftExprV's `f64.convert_i32_s(select …)` fusion (below), which only matches
|
|
1475
|
+
// the select INLINE. Post-watr this local hop would already be copy-propagated away; pre-watr
|
|
1476
|
+
// it survives. When such a lane-local is read exactly once, and that read is inside a
|
|
1477
|
+
// `f64.convert_i32_s`, inline the select back — same "sink a single-use def into its sole
|
|
1478
|
+
// specialized consumer" trick as the narrowing-store case above.
|
|
1479
|
+
{
|
|
1480
|
+
const getCount2 = new Map()
|
|
1481
|
+
const countGets2 = (n) => { if (!isArr(n)) return; if (n[0] === 'local.get' && typeof n[1] === 'string') getCount2.set(n[1], (getCount2.get(n[1]) || 0) + 1); for (let i = 1; i < n.length; i++) countGets2(n[i]) }
|
|
1482
|
+
for (const s of body2) countGets2(s)
|
|
1483
|
+
const dropDefs = new Set()
|
|
1484
|
+
const inlineSign = (n) => {
|
|
1485
|
+
if (!isArr(n)) return n
|
|
1486
|
+
if (n[0] === 'f64.convert_i32_s' && n.length === 2 && isArr(n[1]) && n[1][0] === 'local.get' &&
|
|
1487
|
+
typeof n[1][1] === 'string' && getCount2.get(n[1][1]) === 1) {
|
|
1488
|
+
const nm = n[1][1]
|
|
1489
|
+
const def = body2.find(x => isArr(x) && x[0] === 'local.set' && x[1] === nm && x.length === 3)
|
|
1490
|
+
if (def && isArr(def[2]) && def[2][0] === 'select' && def[2].length === 4 && isI32Const(def[2][1]) && isI32Const(def[2][2])) {
|
|
1491
|
+
dropDefs.add(def)
|
|
1492
|
+
return ['f64.convert_i32_s', def[2]]
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return n.map((c, i) => i === 0 ? c : inlineSign(c))
|
|
1496
|
+
}
|
|
1497
|
+
const inlined2 = body2.map(inlineSign)
|
|
1498
|
+
// Two-pass: inlineSign allocates a fresh array for every node it visits (even unchanged
|
|
1499
|
+
// ones), so filtering `inlined2` by `dropDefs.has(...)` would fail on reference identity —
|
|
1500
|
+
// `dropDefs` holds references into the PRE-map `body2`, so the drop-filter must run against
|
|
1501
|
+
// `body2` (matching indices into `inlined2`), not against the mapped output.
|
|
1502
|
+
if (dropDefs.size) body2 = body2.map((s, i) => dropDefs.has(s) ? null : inlined2[i]).filter(s => s != null)
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1288
1505
|
// Build lifted body. If anything fails to lift, bail.
|
|
1289
1506
|
const newLanedLocals = new Map() // origName → laneName (bare string; see getOrAllocLanedLocal)
|
|
1290
1507
|
const extraLocals = [] // canon temps allocated during lift
|
|
1291
|
-
const ctx = { laneType, incVar, rampVar: null, rampTemp: null, widenLoads: false, localKind, fnLocals, newLanedLocals, extraLocals, freshIdRef, fail: false, failReason: null }
|
|
1508
|
+
const ctx = { laneType, incVar, rampVar: null, rampTemp: null, widenLoads: false, localKind, fnLocals, newLanedLocals, extraLocals, freshIdRef, fail: false, failReason: null, aosPixelStride, pureFuncMap, inlineDepth: 0, constLocals }
|
|
1292
1509
|
const lifted = []
|
|
1293
1510
|
for (const s of body2) {
|
|
1294
1511
|
const r = liftStmt(s, ctx)
|
|
@@ -2175,6 +2392,24 @@ function _isAddressLocal(body, name, ind) {
|
|
|
2175
2392
|
return foundTee && onlyAsAddrTee
|
|
2176
2393
|
}
|
|
2177
2394
|
|
|
2395
|
+
// A pixel-INDEX local — every write is `(i32.mul P ind)` — is the `const j = P*i` of an
|
|
2396
|
+
// AoS loop (feeds channel addresses `base + ((j+c)<<K)`). Classified as 'addr' so the lift
|
|
2397
|
+
// keeps it a recomputed scalar i32, never a v128 lane. (Only tryVectorize consults this.)
|
|
2398
|
+
function _isPixelIndexLocal(body, name, ind) {
|
|
2399
|
+
let found = false, ok = true
|
|
2400
|
+
function walk(n) {
|
|
2401
|
+
if (!isArr(n)) return
|
|
2402
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && n[1] === name && n.length === 3) {
|
|
2403
|
+
found = true
|
|
2404
|
+
if (matchConstMulIV(n[2], ind) == null) ok = false
|
|
2405
|
+
return
|
|
2406
|
+
}
|
|
2407
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
2408
|
+
}
|
|
2409
|
+
for (const s of body) walk(s)
|
|
2410
|
+
return found && ok
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2178
2413
|
// ---- Lifter ----------------------------------------------------------------
|
|
2179
2414
|
|
|
2180
2415
|
// Returns the v128 lane-local NAME (a string) for `name`, allocating once. We store the bare
|
|
@@ -2190,6 +2425,217 @@ function getOrAllocLanedLocal(name, newLanedLocals) {
|
|
|
2190
2425
|
return laneName
|
|
2191
2426
|
}
|
|
2192
2427
|
|
|
2428
|
+
// AoS de-interleave gather/scatter (ctx.aosPixelStride P > 1). The SIMD block steps the IV
|
|
2429
|
+
// by `lanes`, so a scalar address `A` points at pixel i, channel c; pixel i+1's same channel
|
|
2430
|
+
// is P elements = P*elemSize bytes further — reachable as a static load/store `offset`.
|
|
2431
|
+
// aosAddrPair yields two address forms that evaluate `A` exactly ONCE (teeing when needed).
|
|
2432
|
+
function aosAddrPair(addr, ctx) {
|
|
2433
|
+
if (isArr(addr) && addr[0] === 'local.get') return { a0: addr, a1: addr } // live local — read twice, free
|
|
2434
|
+
if (isArr(addr) && addr[0] === 'local.tee' && addr.length === 3) return { a0: addr, a1: ['local.get', addr[1]] }
|
|
2435
|
+
const g = `$__aosa${ctx.freshIdRef.next++}` // bare expr — tee into a scratch
|
|
2436
|
+
ctx.extraLocals.push(['local', g, 'i32'])
|
|
2437
|
+
return { a0: ['local.tee', g, addr], a1: ['local.get', g] }
|
|
2438
|
+
}
|
|
2439
|
+
const aosLoad = (off, addr) => off ? ['f64.load', `offset=${off}`, addr] : ['f64.load', addr]
|
|
2440
|
+
const aosStore = (off, addr, val) => off ? ['f64.store', `offset=${off}`, addr, val] : ['f64.store', addr, val]
|
|
2441
|
+
|
|
2442
|
+
// A scalar `(f64.load [offset=X] A)` → the f64x2 [pixel i chan, pixel i+1 chan]. Bit-exact:
|
|
2443
|
+
// the two lanes are the exact bytes the two scalar iterations read.
|
|
2444
|
+
function aosGather(expr, ctx) {
|
|
2445
|
+
const delta = ctx.aosPixelStride * LANE_INFO.f64.stride
|
|
2446
|
+
let baseOff = 0, addr
|
|
2447
|
+
if (typeof expr[1] === 'string' && expr[1].startsWith('offset=')) { baseOff = parseInt(expr[1].slice(7)) || 0; addr = expr[2] }
|
|
2448
|
+
else addr = expr[1]
|
|
2449
|
+
const { a0, a1 } = aosAddrPair(addr, ctx)
|
|
2450
|
+
return ['f64x2.replace_lane', 1, ['f64x2.splat', aosLoad(baseOff, a0)], aosLoad(baseOff + delta, a1)]
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// Inline a PURE user function call `(call $f ARG…)` into a single scalar value-BLOCK, feeding
|
|
2454
|
+
// the result back through liftExprV so the callee's ternaries/compares/transcendentals lift via
|
|
2455
|
+
// the SAME machinery (no separate restricted inliner). Bails (null) on any non-value statement
|
|
2456
|
+
// (store/loop/impure) — only straight-line pure helpers (spow, a signed-power, …) inline.
|
|
2457
|
+
//
|
|
2458
|
+
// Every argument AND every callee local is bound ONCE to a fresh block-local; param/local reads
|
|
2459
|
+
// substitute to `(local.get bind)`. This is critical for NESTED calls (spow whose ratio arg is
|
|
2460
|
+
// used 3× and itself nests spow): naive expr substitution would duplicate each arg per use and
|
|
2461
|
+
// blow up exponentially (there is no CSE pass after the 'post' vectorizer). Binding keeps the
|
|
2462
|
+
// SIMD body the same size as the scalar call graph.
|
|
2463
|
+
// Infer the wasm type of a value node — from its `.type` expando (jz stamps every instruction) or
|
|
2464
|
+
// the op prefix (`f64.add`→f64, `i32.mul`→i32, v128 ops→v128). Used to declare inline temps.
|
|
2465
|
+
function nodeWasmType(n) {
|
|
2466
|
+
if (isArr(n)) {
|
|
2467
|
+
if (typeof n.type === 'string') return n.type
|
|
2468
|
+
const op = n[0]
|
|
2469
|
+
if (typeof op === 'string') {
|
|
2470
|
+
if (op.startsWith('f64.') || op === 'f64x2.extract_lane') return 'f64'
|
|
2471
|
+
if (op.startsWith('f32.')) return 'f32'
|
|
2472
|
+
if (op.startsWith('i64.')) return 'i64'
|
|
2473
|
+
if (op.startsWith('i32.')) return 'i32'
|
|
2474
|
+
if (op.startsWith('f64x2.') || op.startsWith('f32x4.') || op.startsWith('i32x4.') || op.startsWith('i8x16.') || op.startsWith('i16x8.') || op.startsWith('i64x2.') || op.startsWith('v128.')) return 'v128'
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return null
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// Inline a pure function call into an expression, returning a `(block (result T) …binds… value)`
|
|
2481
|
+
// (or the bare value if no binding was needed) — or null if the callee isn't straight-line pure.
|
|
2482
|
+
// `resultType` is the callee's result type ('f64' by default, the vectorizer's only use). When a
|
|
2483
|
+
// `localSink` array is passed, the fresh `$__ia` binding temps are declared into it (`['local', n, T]`)
|
|
2484
|
+
// so a general caller can hoist them into the enclosing function; the vectorizer omits it (its lane
|
|
2485
|
+
// lift re-types the block). Params must be read-only (else the substitution model breaks).
|
|
2486
|
+
export function inlinePureCallExpr(callNode, pureFuncMap, freshIdRef, localSink = null, resultType = 'f64', tempPrefix = '$__ia') {
|
|
2487
|
+
const callee = pureFuncMap && pureFuncMap.get(callNode[1])
|
|
2488
|
+
if (!callee) return null
|
|
2489
|
+
const bodyStart = findBodyStart(callee)
|
|
2490
|
+
if (bodyStart < 0) return null
|
|
2491
|
+
const params = [], paramType = new Map(), localType = new Map()
|
|
2492
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
2493
|
+
const d = callee[i]
|
|
2494
|
+
if (isArr(d) && d[0] === 'param' && typeof d[1] === 'string') { params.push(d[1]); paramType.set(d[1], d[2]) }
|
|
2495
|
+
else if (isArr(d) && d[0] === 'local' && typeof d[1] === 'string') localType.set(d[1], d[2])
|
|
2496
|
+
}
|
|
2497
|
+
const args = callNode.slice(2)
|
|
2498
|
+
if (args.length !== params.length) return null
|
|
2499
|
+
const body = callee.slice(bodyStart)
|
|
2500
|
+
for (const p of params) if (writesName(body, p)) return null // params must be read-only
|
|
2501
|
+
const subst = new Map()
|
|
2502
|
+
// Callee-local RENAMING (general-inliner path, localSink passed): a callee local
|
|
2503
|
+
// reached via `local.tee` / control-flow `local.set` isn't captured by bindOnce,
|
|
2504
|
+
// so its NAME would collide with same-named caller locals (the canonical trap:
|
|
2505
|
+
// arrow `(x,k)=>…` inlined into a caller whose variable is also `x`). Rename at
|
|
2506
|
+
// substitution time — sub() returns substituted caller-arg nodes WHOLE without
|
|
2507
|
+
// descending, so a rename can never touch a caller node. A tee/set of a name
|
|
2508
|
+
// bindOnce already substituted means reads and writes diverged — bail (broken).
|
|
2509
|
+
let broken = false
|
|
2510
|
+
// Caller-origin subtrees injected by substitution, tracked by node IDENTITY: the
|
|
2511
|
+
// leak backstop must skip them — a caller local legitimately named like a callee
|
|
2512
|
+
// local (`x`/`k` args into an `(x,k)=>…` arrow) is not a leak.
|
|
2513
|
+
const injected = new Set()
|
|
2514
|
+
const renames = localSink ? new Map() : null
|
|
2515
|
+
const renameOf = (name) => {
|
|
2516
|
+
let r = renames.get(name)
|
|
2517
|
+
if (!r) {
|
|
2518
|
+
r = `${tempPrefix}${freshIdRef.next++}`
|
|
2519
|
+
renames.set(name, r)
|
|
2520
|
+
localSink.push(['local', r, localType.get(name) || 'f64'])
|
|
2521
|
+
}
|
|
2522
|
+
return r
|
|
2523
|
+
}
|
|
2524
|
+
const sub = (n) => {
|
|
2525
|
+
if (!isArr(n)) return n
|
|
2526
|
+
if (n[0] === 'local.get' && typeof n[1] === 'string') {
|
|
2527
|
+
if (subst.has(n[1])) { const v = subst.get(n[1]); injected.add(v); return v }
|
|
2528
|
+
if (renames && localType.has(n[1])) return ['local.get', renameOf(n[1])]
|
|
2529
|
+
}
|
|
2530
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string') {
|
|
2531
|
+
if (subst.has(n[1])) { broken = true; return n }
|
|
2532
|
+
if (renames && localType.has(n[1])) return [n[0], renameOf(n[1]), ...n.slice(2).map(sub)]
|
|
2533
|
+
}
|
|
2534
|
+
return n.map((c, i) => i === 0 ? c : sub(c))
|
|
2535
|
+
}
|
|
2536
|
+
// A constant / bare local read is free to duplicate — substitute it directly (no binding),
|
|
2537
|
+
// which also keeps a constant exponent literal at the `pow` node so it can lower to 2-wide exp∘log.
|
|
2538
|
+
// convert-of-local too: one op over a register read, and keeping the convert SYNTACTIC at
|
|
2539
|
+
// every use is what lets the trunc∘convert / guard-vs-impossible-const identities fire
|
|
2540
|
+
// (the devirt arm-inline spills i32 args in that exact shape).
|
|
2541
|
+
const isTrivial = (n) => isArr(n) && (n[0] === 'f64.const' || n[0] === 'i32.const' ||
|
|
2542
|
+
(n[0] === 'local.get' && typeof n[1] === 'string') || (n[0] === 'global.get' && typeof n[1] === 'string') ||
|
|
2543
|
+
(n[0] === 'f64.convert_i32_s' && isArr(n[1]) && n[1][0] === 'local.get'))
|
|
2544
|
+
const pre = []
|
|
2545
|
+
const bindOnce = (name, valueExpr, declType, alreadySubbed) => {
|
|
2546
|
+
const v = alreadySubbed ? valueExpr : sub(valueExpr)
|
|
2547
|
+
if (isTrivial(v)) { subst.set(name, v); return } // cheap → substitute directly, no temp
|
|
2548
|
+
const bn = `${tempPrefix}${freshIdRef.next++}`
|
|
2549
|
+
const t = declType || nodeWasmType(v) || 'f64'
|
|
2550
|
+
if (localSink) localSink.push(['local', bn, t])
|
|
2551
|
+
pre.push(['local.set', bn, v])
|
|
2552
|
+
subst.set(name, ['local.get', bn])
|
|
2553
|
+
}
|
|
2554
|
+
params.forEach((p, i) => bindOnce(p, args[i], paramType.get(p), true)) // args live in the OUTER scope — do NOT sub
|
|
2555
|
+
// Leak guard: a callee local reached only via `local.tee` (a CSE'd subexpression) or set inside
|
|
2556
|
+
// control flow is NOT captured by the top-level bindOnce, so its name would survive into the caller
|
|
2557
|
+
// where it isn't declared ("$x not in scope"). For the general inliner (localSink passed): RENAME
|
|
2558
|
+
// any surviving TRUE-local name to a fresh caller-scope local declared into the sink — sound,
|
|
2559
|
+
// locals are function-scoped names (a tee'd NaN-guard local `(x,k)=>(x??0)|0` is the canonical
|
|
2560
|
+
// shape). Params are read-only and fully substituted by bindOnce, so a surviving PARAM name means
|
|
2561
|
+
// the model broke — bail (keep the call) as the backstop. The VECTORIZER path (localSink == null)
|
|
2562
|
+
// re-processes the returned expression in its lane context — a tee'd callee local becomes a lane
|
|
2563
|
+
// local there — so it must NOT bail or rename, or pure helpers with a CSE'd tee (spow's `av`)
|
|
2564
|
+
// stop vectorizing.
|
|
2565
|
+
const calleeLocals = new Set([...paramType.keys(), ...localType.keys()])
|
|
2566
|
+
// Backstop: with renaming inlined into sub(), the only way a callee name survives
|
|
2567
|
+
// into a sink-spliced result is a broken substitution model (e.g. a param name in
|
|
2568
|
+
// write position, or a bindOnce'd local later tee'd). Bail — keep the call. The
|
|
2569
|
+
// VECTORIZER path (localSink == null) neither renames nor bails: it re-processes
|
|
2570
|
+
// the expression in its lane context, where a tee'd callee local becomes a lane
|
|
2571
|
+
// local — bailing there would stop pure helpers with a CSE'd tee (spow's `av`)
|
|
2572
|
+
// from vectorizing.
|
|
2573
|
+
const leaks = (n) => isArr(n) && !injected.has(n) &&
|
|
2574
|
+
(((n[0] === 'local.get' || n[0] === 'local.set' || n[0] === 'local.tee') && calleeLocals.has(n[1])) || n.some((c, i) => i > 0 && leaks(c)))
|
|
2575
|
+
const wrap = (val) => {
|
|
2576
|
+
const r = pre.length ? ['block', ['result', resultType], ...pre, val] : val
|
|
2577
|
+
return (localSink && (broken || leaks(r))) ? null : r
|
|
2578
|
+
}
|
|
2579
|
+
for (let k = 0; k < body.length; k++) {
|
|
2580
|
+
const stmt = body[k]
|
|
2581
|
+
if (!isArr(stmt)) return null
|
|
2582
|
+
if (stmt[0] === 'local.set' && typeof stmt[1] === 'string' && stmt.length === 3) { bindOnce(stmt[1], stmt[2], localType.get(stmt[1]), false); continue }
|
|
2583
|
+
if (stmt[0] === 'return' && stmt.length === 2) return wrap(sub(stmt[1]))
|
|
2584
|
+
// Trailing value expression = implicit return (a bare `if`/`block`/… as the function's last
|
|
2585
|
+
// statement, `(v) => cond ? a : b`). Earlier non-set/non-return statements can't be values.
|
|
2586
|
+
if (k === body.length - 1) return wrap(sub(stmt))
|
|
2587
|
+
return null
|
|
2588
|
+
}
|
|
2589
|
+
return null
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// Statement-position containers: a call that is a DIRECT child here may be a statement (void /
|
|
2593
|
+
// block-fallthrough), where the value-producing `(block (result T) …)` inline form is ill-typed.
|
|
2594
|
+
// The general inliner only rewrites calls in operand (value) position — the common `x = f(…)`,
|
|
2595
|
+
// `a[i] = f(…)`, `f(…) * k` shapes — and recurses into these so nested-in-operand calls still inline.
|
|
2596
|
+
const INLINE_STMT_CTX = new Set(['block', 'loop', 'func', 'then', 'else', 'if'])
|
|
2597
|
+
|
|
2598
|
+
// General pre-watr pure-function inlining — jz LOWERING (runs before the vectorizer). Replaces a
|
|
2599
|
+
// `(call $g …)` in value position with $g's inlined body when $g is PURE (pureFuncMap) and
|
|
2600
|
+
// straight-line. jz decides by PURITY + TYPES — knowledge watr's untyped, size-gated inliner lacks —
|
|
2601
|
+
// exposing the callee's arithmetic to the vectorizer / narrower / const-folder. watr keeps only the
|
|
2602
|
+
// mechanical residual. Bit-exact: params are read-only, args bind once (or substitute if trivial),
|
|
2603
|
+
// the callee's straight-line body becomes a result-typed block. Fresh temps are declared into `fn`.
|
|
2604
|
+
export function inlinePureFnsInFn(fn, pureFuncMap, freshIdRef, canInline) {
|
|
2605
|
+
if (!isArr(fn) || fn[0] !== 'func' || !pureFuncMap || !pureFuncMap.size || !canInline || !canInline.size) return
|
|
2606
|
+
const selfName = fn[1]
|
|
2607
|
+
const bodyStart = findBodyStart(fn)
|
|
2608
|
+
if (bodyStart < 0) return
|
|
2609
|
+
const newLocals = []
|
|
2610
|
+
const resultTypeOf = (callee) => {
|
|
2611
|
+
for (let i = 2; i < callee.length; i++) {
|
|
2612
|
+
const d = callee[i]
|
|
2613
|
+
if (!isArr(d)) break
|
|
2614
|
+
if (d[0] === 'result') return d[1]
|
|
2615
|
+
if (d[0] !== 'param' && d[0] !== 'export' && d[0] !== 'local' && d[0] !== 'type') break
|
|
2616
|
+
}
|
|
2617
|
+
return 'f64'
|
|
2618
|
+
}
|
|
2619
|
+
const walk = (node) => {
|
|
2620
|
+
if (!isArr(node)) return node
|
|
2621
|
+
const parentIsStmt = INLINE_STMT_CTX.has(node[0])
|
|
2622
|
+
for (let i = 1; i < node.length; i++) {
|
|
2623
|
+
let child = node[i]
|
|
2624
|
+
if (!isArr(child)) continue
|
|
2625
|
+
child = walk(child) // recurse first → inline nested calls (e.g. in this call's args)
|
|
2626
|
+
node[i] = child
|
|
2627
|
+
if (!parentIsStmt && child[0] === 'call' && typeof child[1] === 'string' &&
|
|
2628
|
+
child[1] !== selfName && canInline.has(child[1]) && pureFuncMap.has(child[1])) {
|
|
2629
|
+
const inlined = inlinePureCallExpr(child, pureFuncMap, freshIdRef, newLocals, resultTypeOf(pureFuncMap.get(child[1])), '$__gi')
|
|
2630
|
+
if (inlined != null) node[i] = inlined
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return node
|
|
2634
|
+
}
|
|
2635
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = walk(fn[i])
|
|
2636
|
+
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2193
2639
|
// Wrap an already-lifted v128 value `coreV` in per-lane NaN canonicalization:
|
|
2194
2640
|
// v128.bitselect(splat(C), coreV, laneNe(coreV, coreV))
|
|
2195
2641
|
// coreV is referenced three times. When it's a bare local.get (the common
|
|
@@ -2229,6 +2675,10 @@ let _whyNotReason = null
|
|
|
2229
2675
|
// on unconditionally. Armed for the duration of a vectorizeLaneLocal call.
|
|
2230
2676
|
let _relaxF32 = false
|
|
2231
2677
|
|
|
2678
|
+
// optimize.crPow, armed the same way — the const-exponent pow arm picks its lowering
|
|
2679
|
+
// from it (lift ctx objects don't carry the optimize config; module flag is the pattern).
|
|
2680
|
+
let _crPow = false
|
|
2681
|
+
|
|
2232
2682
|
// Mark a lift bail and record its reason. First-write-wins: the innermost failing op
|
|
2233
2683
|
// sets ctx.failReason; outer frames see ctx.fail already set and return without
|
|
2234
2684
|
// overwriting, so the reason names the actual blocking op, not a wrapper.
|
|
@@ -2320,7 +2770,10 @@ function liftStmt(stmt, ctx) {
|
|
|
2320
2770
|
// Address-only local: lift the value as-is (it's i32 arithmetic on ind).
|
|
2321
2771
|
return ['local.set', name, stmt[2]]
|
|
2322
2772
|
}
|
|
2323
|
-
|
|
2773
|
+
// 'lane', or an UNCLASSIFIED local — which can only be one introduced by an inlined pure
|
|
2774
|
+
// callee (classification covers every original body local; a pure helper's temps are fresh
|
|
2775
|
+
// per-iteration lane values, never loop-carried). Both lift as lane data.
|
|
2776
|
+
if (kind === 'lane' || kind === undefined) {
|
|
2324
2777
|
const laneName = getOrAllocLanedLocal(name, ctx.newLanedLocals)
|
|
2325
2778
|
const v = liftExprV(stmt[2], ctx)
|
|
2326
2779
|
if (ctx.fail) return null
|
|
@@ -2330,11 +2783,30 @@ function liftStmt(stmt, ctx) {
|
|
|
2330
2783
|
}
|
|
2331
2784
|
|
|
2332
2785
|
if (STORE_OPS[op]) {
|
|
2786
|
+
const sty = STORE_OPS[op]
|
|
2787
|
+
// AoS de-interleave scatter: `(f64.store [offset=X] A V)` → tee the f64x2 V once, then
|
|
2788
|
+
// write lane 0 at X (pixel i) and lane 1 at X + P*elemSize (pixel i+1) — the exact two
|
|
2789
|
+
// scalar stores. Handles the folded `offset=` memarg form (channels d[j+1], d[j+2]).
|
|
2790
|
+
if (ctx.aosPixelStride > 1) {
|
|
2791
|
+
if (sty !== ctx.laneType) return liftFail(ctx, 'AoS narrowing store unsupported')
|
|
2792
|
+
let baseOff = 0, addr, val
|
|
2793
|
+
if (typeof stmt[1] === 'string' && stmt[1].startsWith('offset=')) { baseOff = parseInt(stmt[1].slice(7)) || 0; addr = stmt[2]; val = stmt[3] }
|
|
2794
|
+
else { addr = stmt[1]; val = stmt[2] }
|
|
2795
|
+
const v = liftExprV(val, ctx)
|
|
2796
|
+
if (ctx.fail) return null
|
|
2797
|
+
const delta = ctx.aosPixelStride * LANE_INFO.f64.stride
|
|
2798
|
+
const { a0, a1 } = aosAddrPair(addr, ctx)
|
|
2799
|
+
const vt = `$__aosv${ctx.freshIdRef.next++}`
|
|
2800
|
+
ctx.extraLocals.push(['local', vt, 'v128'])
|
|
2801
|
+
return ['__seq__',
|
|
2802
|
+
['local.set', vt, v],
|
|
2803
|
+
aosStore(baseOff, a0, ['f64x2.extract_lane', 0, ['local.get', vt]]),
|
|
2804
|
+
aosStore(baseOff + delta, a1, ['f64x2.extract_lane', 1, ['local.get', vt]])]
|
|
2805
|
+
}
|
|
2333
2806
|
const addr = stmt[1] // we leave addresses as-is (scalar i32 expressions)
|
|
2334
2807
|
// Handle memarg if present (last positional after addr/val): unlikely in
|
|
2335
2808
|
// pre-watr IR for this shape; bail if more than 3 children.
|
|
2336
2809
|
if (stmt.length !== 3) return liftFail(ctx, `${op} with memarg`)
|
|
2337
|
-
const sty = STORE_OPS[op]
|
|
2338
2810
|
// Narrowing store: a narrower element written from a wider float lane (`o[i] =
|
|
2339
2811
|
// narrow(f(x))` — codec encode / downsample). The scalar store value carries a
|
|
2340
2812
|
// conversion (f32.demote_f64, or the float→int ToInt32 idiom); peel it, lift the
|
|
@@ -2494,6 +2966,9 @@ function liftExprV(expr, ctx) {
|
|
|
2494
2966
|
// Loads → v128.load (preserving address, including any local.tee).
|
|
2495
2967
|
if (LOAD_OPS[op]) {
|
|
2496
2968
|
if (LOAD_OPS[op] !== ctx.laneType) return liftFail(ctx, `${op}: load type ≠ lane type ${ctx.laneType}`)
|
|
2969
|
+
// AoS de-interleave: consecutive elements are DIFFERENT channels, so a plain v128.load
|
|
2970
|
+
// would mix channels — gather the same channel of pixels i, i+1 into the f64x2 instead.
|
|
2971
|
+
if (ctx.aosPixelStride > 1) return aosGather(expr, ctx)
|
|
2497
2972
|
// memarg form `(T.load offset=N addr)` — the stencil neighbour `a[i+1]` jz folds
|
|
2498
2973
|
// onto `a[i]`'s address tee. `v128.load offset=N` reads the N-byte-shifted vector,
|
|
2499
2974
|
// i.e. the (a[i+1], a[i+2]) pair — exactly the δ-shifted lane data. Preserve it.
|
|
@@ -2536,7 +3011,22 @@ function liftExprV(expr, ctx) {
|
|
|
2536
3011
|
if (kind === 'addr' || name === ctx.incVar) {
|
|
2537
3012
|
return liftFail(ctx, `${name}: address/induction var used as lane data`)
|
|
2538
3013
|
}
|
|
2539
|
-
|
|
3014
|
+
// Unclassified (undefined) & not the IV/addr: a local introduced by an inlined pure callee —
|
|
3015
|
+
// read its lane shadow (the matching lane-set default above allocated it).
|
|
3016
|
+
return ['local.get', getOrAllocLanedLocal(name, ctx.newLanedLocals)]
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// `(local.tee $x V)` in value position — a CSE temp inside a value expression (e.g. the base
|
|
3020
|
+
// teed for reuse in an inlined `x**(k/5)` fifthroot / a repeated subexpression). Lift V into the
|
|
3021
|
+
// lane shadow of $x and tee it: later `(local.get $x)` reads resolve to the same shadow.
|
|
3022
|
+
if (op === 'local.tee' && typeof expr[1] === 'string' && expr.length === 3) {
|
|
3023
|
+
const name = expr[1]
|
|
3024
|
+
const kind = ctx.localKind.get(name)
|
|
3025
|
+
if (kind === 'lane' || kind === undefined) {
|
|
3026
|
+
const v = liftExprV(expr[2], ctx); if (ctx.fail) return null
|
|
3027
|
+
return ['local.tee', getOrAllocLanedLocal(name, ctx.newLanedLocals), v]
|
|
3028
|
+
}
|
|
3029
|
+
return liftFail(ctx, `local.tee ${name}: non-lane local in value position`)
|
|
2540
3030
|
}
|
|
2541
3031
|
|
|
2542
3032
|
// Loop-invariant global (e.g. a hoistConstantPool'd const, or any global the
|
|
@@ -2553,6 +3043,24 @@ function liftExprV(expr, ctx) {
|
|
|
2553
3043
|
const inner = expr[1]
|
|
2554
3044
|
const inv = isArr(inner) && (inner[0] === 'global.get' || (inner[0] === 'local.get' && ctx.localKind.get(inner[1]) === 'invariant'))
|
|
2555
3045
|
if (inv) return [info.splat, expr]
|
|
3046
|
+
// Sign / small-int ternary `cond ? A : B` (A,B integer literals) lowered to
|
|
3047
|
+
// `(f64.convert_i32_s (select (i32.const A) (i32.const B) COND))` — e.g. `a<0 ? -1 : 1`.
|
|
3048
|
+
// Convert the literals to f64 and bitselect by COND's lane mask (the f64-lane ternary).
|
|
3049
|
+
if (ctx.laneType === 'f64' && isArr(inner) && inner[0] === 'select' && inner.length === 4 &&
|
|
3050
|
+
isI32Const(inner[1]) && isI32Const(inner[2])) {
|
|
3051
|
+
let cond = inner[3]
|
|
3052
|
+
if (isArr(cond) && cond[0] === 'i32.ne' && isI32Const(cond[2]) && cond[2][1] === 0) cond = cond[1]
|
|
3053
|
+
const cmpS = isArr(cond) && cond.length === 3 ? LANE_COMPARE.f64?.[cond[0]] : null
|
|
3054
|
+
if (cmpS) {
|
|
3055
|
+
const ca = liftExprV(cond[1], ctx); if (ctx.fail) return null
|
|
3056
|
+
const cb = liftExprV(cond[2], ctx); if (ctx.fail) return null
|
|
3057
|
+
const mtmp = `$__mask${ctx.freshIdRef.next++}`
|
|
3058
|
+
ctx.extraLocals.push(['local', mtmp, 'v128'])
|
|
3059
|
+
return ['block', ['result', 'v128'],
|
|
3060
|
+
['local.set', mtmp, [cmpS, ca, cb]],
|
|
3061
|
+
['v128.bitselect', ['f64x2.splat', ['f64.const', inner[1][1]]], ['f64x2.splat', ['f64.const', inner[2][1]]], ['local.get', mtmp]]]
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
2556
3064
|
}
|
|
2557
3065
|
|
|
2558
3066
|
// NaN-canonicalization wrapper (float lanes only; integer lanes never carry
|
|
@@ -2594,9 +3102,24 @@ function liftExprV(expr, ctx) {
|
|
|
2594
3102
|
}
|
|
2595
3103
|
if ((ctx.laneType === 'f64' || ctx.laneType === 'f32') && op === 'block') {
|
|
2596
3104
|
const m = matchCanonBlock(expr, ctx.laneType)
|
|
2597
|
-
if (
|
|
2598
|
-
|
|
2599
|
-
|
|
3105
|
+
if (m) { const coreV = liftExprV(m.core, ctx); return ctx.fail ? null : liftCanon(coreV, m.C, ctx, info) }
|
|
3106
|
+
// General value-block (a let-binding): `(block [label] (result T) …laneSets… TAILVALUE)`.
|
|
3107
|
+
// jz emits these for an inlined value function (e.g. `av ** e` → an exp∘log block). Lift the
|
|
3108
|
+
// intermediate lane-local sets, then the tail value. Sound ONLY when the block is straight-line
|
|
3109
|
+
// (no br/br_if/br_table/return targeting it — an early-exit can't be flattened) — bail otherwise.
|
|
3110
|
+
let bi = 1
|
|
3111
|
+
if (typeof expr[bi] === 'string') bi++
|
|
3112
|
+
if (isArr(expr[bi]) && expr[bi][0] === 'result') bi++
|
|
3113
|
+
const parts = expr.slice(bi)
|
|
3114
|
+
if (parts.length === 0 || parts.some(hasBranchOrReturn)) return liftFail(ctx, 'non-canonical value-block')
|
|
3115
|
+
const out = ['block', ['result', 'v128']]
|
|
3116
|
+
for (let k = 0; k < parts.length - 1; k++) {
|
|
3117
|
+
const l = liftStmt(parts[k], ctx); if (ctx.fail) return null
|
|
3118
|
+
if (l != null) { if (Array.isArray(l) && l[0] === '__seq__') out.push(...l.slice(1)); else out.push(l) }
|
|
3119
|
+
}
|
|
3120
|
+
const tail = liftExprV(parts[parts.length - 1], ctx); if (ctx.fail) return null
|
|
3121
|
+
out.push(tail)
|
|
3122
|
+
return out
|
|
2600
3123
|
}
|
|
2601
3124
|
|
|
2602
3125
|
// Conditional select — jz lowers `cond ? X : Y` to (if (result LT) COND (then X)
|
|
@@ -2615,8 +3138,13 @@ function liftExprV(expr, ctx) {
|
|
|
2615
3138
|
const resTy = isArr(expr[1]) && expr[1][0] === 'result' ? expr[1][1] : null
|
|
2616
3139
|
if (resTy !== ctx.laneType && !(ctx.laneType === 'f32' && resTy === 'f64')) return liftFail(ctx, 'conditional without lane-typed result')
|
|
2617
3140
|
const thenN = expr[3], elseN = expr[4]
|
|
2618
|
-
|
|
2619
|
-
|
|
3141
|
+
// A branch is `(then …preludeSets… TAILVALUE)` — usually just the tail (length 2), but jz's
|
|
3142
|
+
// NaN-canonicalization of a negation tees the value first (`(then (set $t (neg a)) (canon $t))`),
|
|
3143
|
+
// so accept intermediate lane-local sets before the tail. Both branches evaluate speculatively
|
|
3144
|
+
// (lane-pure ⇒ trap-free); each tail is snapshotted into its own temp BEFORE the other branch's
|
|
3145
|
+
// prelude runs, so a shared prelude local can't clobber the already-computed value.
|
|
3146
|
+
if (!isArr(thenN) || thenN[0] !== 'then' || thenN.length < 2) return liftFail(ctx, 'malformed conditional then-branch')
|
|
3147
|
+
if (!isArr(elseN) || elseN[0] !== 'else' || elseN.length < 2) return liftFail(ctx, 'malformed conditional else-branch')
|
|
2620
3148
|
let cond = expr[2]
|
|
2621
3149
|
if (isArr(cond) && cond[0] === 'i32.ne' && isI32Const(cond[2]) && cond[2][1] === 0) cond = cond[1] // strip `!= 0`
|
|
2622
3150
|
// f32 lane: operands were promoted, so the compare is `f64.*` — use its f32x4 form
|
|
@@ -2626,13 +3154,28 @@ function liftExprV(expr, ctx) {
|
|
|
2626
3154
|
if (!cmpSimd) return liftFail(ctx, `${isArr(cond) ? cond[0] : 'condition'}: not a lane-vectorizable comparison`)
|
|
2627
3155
|
const ca = liftExprV(cond[1], ctx); if (ctx.fail) return null
|
|
2628
3156
|
const cb = liftExprV(cond[2], ctx); if (ctx.fail) return null
|
|
2629
|
-
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
|
|
3157
|
+
// Lift a branch: its prelude sets, then its tail value snapshotted into `outTmp`.
|
|
3158
|
+
const liftArm = (arm, outTmp) => {
|
|
3159
|
+
const out = []
|
|
3160
|
+
for (let i = 1; i < arm.length - 1; i++) {
|
|
3161
|
+
const l = liftStmt(arm[i], ctx); if (ctx.fail) return null
|
|
3162
|
+
if (l != null) { if (Array.isArray(l) && l[0] === '__seq__') out.push(...l.slice(1)); else out.push(l) }
|
|
3163
|
+
}
|
|
3164
|
+
const v = liftExprV(arm[arm.length - 1], ctx); if (ctx.fail) return null
|
|
3165
|
+
out.push(['local.set', outTmp, v])
|
|
3166
|
+
return out
|
|
3167
|
+
}
|
|
3168
|
+
const id = ctx.freshIdRef.next++
|
|
3169
|
+
const tv = `$__then${id}`, ev = `$__else${id}`, mtmp = `$__mask${id}`
|
|
3170
|
+
ctx.extraLocals.push(['local', tv, 'v128'], ['local', ev, 'v128'], ['local', mtmp, 'v128'])
|
|
3171
|
+
// Mask FIRST: COND may carry an address `local.tee` the branch values read, so it must run
|
|
3172
|
+
// before them (matching scalar order — COND evaluates before the taken branch).
|
|
3173
|
+
const maskSet = ['local.set', mtmp, [cmpSimd, ca, cb]]
|
|
3174
|
+
const thenSeq = liftArm(thenN, tv); if (ctx.fail) return null
|
|
3175
|
+
const elseSeq = liftArm(elseN, ev); if (ctx.fail) return null
|
|
2633
3176
|
return ['block', ['result', 'v128'],
|
|
2634
|
-
|
|
2635
|
-
['v128.bitselect',
|
|
3177
|
+
maskSet, ...thenSeq, ...elseSeq,
|
|
3178
|
+
['v128.bitselect', ['local.get', tv], ['local.get', ev], ['local.get', mtmp]]]
|
|
2636
3179
|
}
|
|
2637
3180
|
|
|
2638
3181
|
// Lane-pure op?
|
|
@@ -2657,6 +3200,64 @@ function liftExprV(expr, ctx) {
|
|
|
2657
3200
|
return [entry.simd, a, b]
|
|
2658
3201
|
}
|
|
2659
3202
|
|
|
3203
|
+
// Transcendental call → its bit-exact f64x2 mirror (pow/exp/log/exp2/sin/cos/atan2/hypot).
|
|
3204
|
+
// f64 lane only (the *2/_v helpers are f64x2). SIMD_PINNED keeps the scalar target alive
|
|
3205
|
+
// through watr's single-caller inlining so the `call` node still exists at lift time.
|
|
3206
|
+
// `$__to_num` is a numeric coercion jz wraps around a helper param it couldn't prove is f64
|
|
3207
|
+
// (e.g. `decode(src[j])`), boxing it via `i64.reinterpret_f64` first. In the lane every value
|
|
3208
|
+
// is already a genuine finite f64, so `__to_num(reinterpret_i64(x)) == x` — lift straight
|
|
3209
|
+
// through, peeling the box round-trip.
|
|
3210
|
+
if (op === 'call' && expr[1] === '$__to_num' && expr.length === 3) {
|
|
3211
|
+
let arg = expr[2]
|
|
3212
|
+
if (isArr(arg) && arg[0] === 'i64.reinterpret_f64' && arg.length === 2) arg = arg[1]
|
|
3213
|
+
return liftExprV(arg, ctx)
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// `$math.pow(x, c)` with a CONSTANT non-integer exponent, found only during vectorization
|
|
3217
|
+
// (`ctx.constLocals`) — e.g. spow's `av ** nv` after pure-function inlining substitutes the
|
|
3218
|
+
// literal (module/math.js's own `emitPow` const-exponent fold never reaches here: its constant
|
|
3219
|
+
// exponent is known at EMIT time, so it already lowers straight to the scalar const-exponent
|
|
3220
|
+
// path, picked up by the generic PPC_CALL2 lift below). `optimize.crPow` picks the lowering,
|
|
3221
|
+
// mirroring emitPow's own default/crPow split (see the authoritative comment above emitPow):
|
|
3222
|
+
// OFF (DEFAULT): truly-2-wide `exp_v(c · log_v(x))` — bit-identical to the scalar `$math.pow`
|
|
3223
|
+
// for EVERY x when c is non-integer (verified: negative base → NaN and x=0 → 0/∞ both carry
|
|
3224
|
+
// through log/exp identically; only the integer fast path differs, and it is excluded).
|
|
3225
|
+
// ON: the truly-2-wide correctly-rounded `$math.pow_fold_v` (module/math.js) — the SIMD twin
|
|
3226
|
+
// of the scalar `$math.pow_fold` (c needs no pre-split; the shared kernel twoProd-splits
|
|
3227
|
+
// both multiply operands internally — see its own comment). Bit-identical to the scalar
|
|
3228
|
+
// `$math.pow_fold` for every x — same function, called on both lanes.
|
|
3229
|
+
if (op === 'call' && ctx.laneType === 'f64' && expr[1] === '$math.pow' && expr.length === 4) {
|
|
3230
|
+
const ex = expr[3]
|
|
3231
|
+
let c = null
|
|
3232
|
+
if (isArr(ex) && ex[0] === 'f64.const') c = +ex[1]
|
|
3233
|
+
else if (isArr(ex) && ex[0] === 'local.get' && ctx.constLocals && ctx.constLocals.has(ex[1])) c = ctx.constLocals.get(ex[1])
|
|
3234
|
+
if (c != null && Number.isFinite(c) && !Number.isInteger(c)) {
|
|
3235
|
+
const base = liftExprV(expr[2], ctx); if (ctx.fail) return null
|
|
3236
|
+
if (_crPow) {
|
|
3237
|
+
return ['call', '$math.pow_fold_v', base, ['f64x2.splat', ['f64.const', c]]]
|
|
3238
|
+
}
|
|
3239
|
+
return ['call', '$math.exp_v', ['f64x2.mul', ['f64x2.splat', ex], ['call', '$math.log_v', base]]]
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
if (op === 'call' && ctx.laneType === 'f64' && PPC_CALL2[expr[1]]) {
|
|
3244
|
+
const args = []
|
|
3245
|
+
for (let i = 2; i < expr.length; i++) { const a = liftExprV(expr[i], ctx); if (ctx.fail) return null; args.push(a) }
|
|
3246
|
+
return ['call', PPC_CALL2[expr[1]], ...args]
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// Pure user-function call → inline its body as a value-expr and lift that (handles the callee's
|
|
3250
|
+
// ternaries/compares/pow via the arms above). Depth-guarded against pure→pure recursion.
|
|
3251
|
+
if (op === 'call' && ctx.laneType === 'f64' && ctx.pureFuncMap && ctx.pureFuncMap.has(expr[1]) && ctx.inlineDepth < 8) {
|
|
3252
|
+
const inlined = inlinePureCallExpr(expr, ctx.pureFuncMap, ctx.freshIdRef)
|
|
3253
|
+
if (inlined != null) {
|
|
3254
|
+
ctx.inlineDepth++
|
|
3255
|
+
const v = liftExprV(inlined, ctx)
|
|
3256
|
+
ctx.inlineDepth--
|
|
3257
|
+
return v
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
2660
3261
|
return liftFail(ctx, `${op}: no lane-pure SIMD mapping for ${ctx.laneType}`)
|
|
2661
3262
|
}
|
|
2662
3263
|
|
|
@@ -3598,7 +4199,26 @@ function tryBlurMultiPixel(blockNode, fnLocals, freshIdRef) {
|
|
|
3598
4199
|
const rBound = ic[2][1][2]
|
|
3599
4200
|
if (!(isLocalGet(rBound) || isI32Const(rBound))) return null
|
|
3600
4201
|
|
|
3601
|
-
// the RGBA store: 4
|
|
4202
|
+
// the RGBA store: 4 i32.store8 at `ab1 + c`, ab1 tee'd in the first. jz's raw pre-watr
|
|
4203
|
+
// emission of `dst[o]=(sr/win)|0` materializes the divide into its own single-use temp
|
|
4204
|
+
// (`tw = sr/win; store(tw)`) — pre-watr propagateSingleUse runs AFTER the vectorizer
|
|
4205
|
+
// (ordered there so it doesn't scramble the dot-pair matcher), so this indirection is
|
|
4206
|
+
// never folded before this recognizer sees it, unlike the old post-watr pipeline where
|
|
4207
|
+
// watr's own copy-prop had already inlined it into the store operand. Resolve that one-hop
|
|
4208
|
+
// indirection here: if a store's value is a bare local.get and the statement immediately
|
|
4209
|
+
// before it is that local's SOLE def in the loop, substitute the def's RHS.
|
|
4210
|
+
const resolvedTemps = new Set()
|
|
4211
|
+
const resolveStoreVal = (idx, val) => {
|
|
4212
|
+
if (!(isArr(val) && val[0] === 'local.get')) return val
|
|
4213
|
+
const t = val[1], prev = loopNode[idx - 1]
|
|
4214
|
+
if (!(isArr(prev) && prev[0] === 'local.set' && prev.length === 3 && prev[1] === t)) return val
|
|
4215
|
+
let uses = 0
|
|
4216
|
+
const count = (n) => { if (!isArr(n)) return; if (n[0] === 'local.get' && n[1] === t) uses++; n.forEach(count) }
|
|
4217
|
+
count(loopNode)
|
|
4218
|
+
if (uses !== 1) return val
|
|
4219
|
+
resolvedTemps.add(t)
|
|
4220
|
+
return prev[2]
|
|
4221
|
+
}
|
|
3602
4222
|
let storeIdx = -1
|
|
3603
4223
|
for (let i = innerIdx + 1; i + 3 <= bodyEnd; i++) {
|
|
3604
4224
|
const s0 = loopNode[i]
|
|
@@ -3607,11 +4227,14 @@ function tryBlurMultiPixel(blockNode, fnLocals, freshIdRef) {
|
|
|
3607
4227
|
if (storeIdx < 0) return null
|
|
3608
4228
|
const s0 = loopNode[storeIdx]
|
|
3609
4229
|
const ab1 = s0[1][1], dstExpr = s0[1][2] // ab1 local, its address expr
|
|
3610
|
-
const storeVals = [s0[2]]
|
|
4230
|
+
const storeVals = [resolveStoreVal(storeIdx, s0[2])]
|
|
4231
|
+
let scanIdx = storeIdx + 1
|
|
3611
4232
|
for (let c = 1; c < 4; c++) {
|
|
3612
|
-
|
|
4233
|
+
if (isArr(loopNode[scanIdx]) && loopNode[scanIdx][0] === 'local.set') scanIdx++ // skip the next store's div-into-temp
|
|
4234
|
+
const s = loopNode[scanIdx]
|
|
3613
4235
|
if (!(isArr(s) && s[0] === 'i32.store8' && s[1] === `offset=${c}` && isLocalGet(s[2], ab1))) return null
|
|
3614
|
-
storeVals.push(s[3])
|
|
4236
|
+
storeVals.push(resolveStoreVal(scanIdx, s[3]))
|
|
4237
|
+
scanIdx++
|
|
3615
4238
|
}
|
|
3616
4239
|
// each store value must read its accumulator (the divided sum)
|
|
3617
4240
|
for (let c = 0; c < 4; c++) if (!readsVar(storeVals[c], accInits[c])) return null
|
|
@@ -3666,7 +4289,10 @@ function tryBlurMultiPixel(blockNode, fnLocals, freshIdRef) {
|
|
|
3666
4289
|
const newInnerBlock = innerBlock.map(c => c === innerLoop ? newInnerLoop : c)
|
|
3667
4290
|
// store epilogue: the o=(row+x)<<2 setup, then ab1, then per pixel j set acc_c to
|
|
3668
4291
|
// its lane and reuse the scalar store template at byte offset j*4+c.
|
|
3669
|
-
|
|
4292
|
+
// `o = (row+x)<<2` etc. — drop resolved-away div-into-temp defs (unused now; storeVals
|
|
4293
|
+
// carries their RHS directly), else a dead `tw15 = sr/win` reading a stale `sr` gets
|
|
4294
|
+
// carried into the 4-pixel loop (harmless — unused — but unnecessary bytes/work).
|
|
4295
|
+
const preStore = loopNode.slice(innerIdx + 1, storeIdx).filter(s => !(isArr(s) && s[0] === 'local.set' && resolvedTemps.has(s[1])))
|
|
3670
4296
|
const epilogue = [...preStore, ['local.set', ab1, dstExpr]]
|
|
3671
4297
|
for (let j = 0; j < 4; j++) {
|
|
3672
4298
|
const vec = j < 2 ? accLo : accHi, base = (j % 2) * 4
|
|
@@ -4127,22 +4753,49 @@ function tryDivergentEscapeVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
4127
4753
|
const outcomeVarSetEarly = new Set(midBreaks.flatMap(mb => mb.assigns.map(a => a.name)))
|
|
4128
4754
|
const carriedInit = new Map() // carried var → its f64.const seed (before the loop)
|
|
4129
4755
|
const perPxInit = new Map() // c-var → its per-pixel init expr
|
|
4756
|
+
// Pre-watr, jz's raw lowering hasn't run DCE yet — obody[0..innerIdx) may hold pre-loop f64
|
|
4757
|
+
// locals the OLD post-watr recognizer never saw (watr had already deleted them). Two live
|
|
4758
|
+
// shapes beyond direct escape-loop reads (cVars, already populated by the liftable() walk
|
|
4759
|
+
// above): (a) truly dead — a per-pixel grid coord the update ignores (Julia fixed-c: cx/cy
|
|
4760
|
+
// computed but the orbit never reads them — mandelbrot's dual DOES read them); safe to drop.
|
|
4761
|
+
// (b) a carried var's z0 seed reads it INDIRECTLY through one extra local (Julia/Newton
|
|
4762
|
+
// per-pixel z0: x0 = <ramp>; zx = x0) — cVars only sees reads INSIDE the escape loop, so x0
|
|
4763
|
+
// is invisible to it even though it IS the real seed expression. Close that gap with a
|
|
4764
|
+
// needed-set fixpoint before classifying, so x0 promotes to a c-var exactly like a directly
|
|
4765
|
+
// read grid coord — liftCLane already resolves a cVar reference at emit.
|
|
4766
|
+
const epilogue = obody.slice(innerIdx + 1)
|
|
4767
|
+
const preStmts = []
|
|
4130
4768
|
for (let i = 0; i < innerIdx; i++) {
|
|
4131
4769
|
const s = obody[i]
|
|
4132
4770
|
if (!isArr(s) || s[0] !== 'local.set' || s.length !== 3) return null
|
|
4133
|
-
|
|
4771
|
+
preStmts.push({ tgt: s[1], expr: s[2] })
|
|
4772
|
+
}
|
|
4773
|
+
const preTgt = new Set(preStmts.map(p => p.tgt))
|
|
4774
|
+
// A pre-loop local promotes to a c-var only if some OTHER classified site actually reads it —
|
|
4775
|
+
// never one of the reserved roles (carried/temp/itVar/outcome), which already have their own
|
|
4776
|
+
// per-lane home and must keep taking their existing branch below.
|
|
4777
|
+
const promotable = (v) => !carried.has(v) && !temp.has(v) && v !== itVar && !outcomeVarSetEarly.has(v)
|
|
4778
|
+
const needed = new Set(cVars)
|
|
4779
|
+
for (const { tgt, expr } of preStmts) if (carried.has(tgt)) for (const v of preTgt) if (promotable(v) && readsVar(expr, v)) needed.add(v)
|
|
4780
|
+
for (const e of epilogue) for (const v of preTgt) if (promotable(v) && readsVar(e, v)) needed.add(v)
|
|
4781
|
+
for (let changed = true; changed;) {
|
|
4782
|
+
changed = false
|
|
4783
|
+
for (const { tgt, expr } of preStmts) if (needed.has(tgt))
|
|
4784
|
+
for (const v of preTgt) if (v !== tgt && promotable(v) && !needed.has(v) && readsVar(expr, v)) { needed.add(v); changed = true }
|
|
4785
|
+
}
|
|
4786
|
+
for (const { tgt, expr } of preStmts) {
|
|
4134
4787
|
if (carried.has(tgt)) {
|
|
4135
4788
|
// z₀ seed: a constant (mandelbrot/burning-ship z₀=0) OR a per-pixel expr (Julia set, where
|
|
4136
4789
|
// z₀ = the pixel and c is constant — the dual of mandelbrot). liftCLane handles both at emit.
|
|
4137
|
-
carriedInit.set(tgt,
|
|
4790
|
+
carriedInit.set(tgt, expr)
|
|
4138
4791
|
} else if (temp.has(tgt)) { /* recomputed each iteration — init ignored */ }
|
|
4139
|
-
else if (tgt === itVar) { if (constNum(
|
|
4140
|
-
else if (
|
|
4792
|
+
else if (tgt === itVar) { if (constNum(expr) !== 0) return null }
|
|
4793
|
+
else if (needed.has(tgt)) { cVars.add(tgt); perPxInit.set(tgt, expr) }
|
|
4141
4794
|
else if (outcomeVarSetEarly.has(tgt)) { /* i32 outcome var default init — ignored, handled per-lane */ }
|
|
4795
|
+
else if (!hasSideEffect(expr)) { /* dead per-pixel local — the escape update never reads it, drop */ }
|
|
4142
4796
|
else return null
|
|
4143
4797
|
}
|
|
4144
4798
|
for (const c of carried) if (!carriedInit.has(c)) return null
|
|
4145
|
-
const epilogue = obody.slice(innerIdx + 1)
|
|
4146
4799
|
// The epilogue runs scalar per lane; it may only read carried/it/pixel-IV/invariant
|
|
4147
4800
|
// values (each statement's reads, before that statement's writes). A read of an
|
|
4148
4801
|
// inner-loop temp or a per-pixel c-var has no post-loop per-lane value → bail.
|
|
@@ -4468,15 +5121,20 @@ const PPC_CALL2 = {
|
|
|
4468
5121
|
'$math.sin': '$math.sin2', '$math.cos': '$math.cos2',
|
|
4469
5122
|
'$math.pow': '$math.pow2', // 2-arg; bit-exact per-lane scalar (cancellation-sensitive — see module/math.js)
|
|
4470
5123
|
'$math.atan2': '$math.atan2_2', '$math.hypot': '$math.hypot_2', // 2-arg; bit-exact extract/repack
|
|
5124
|
+
'$math.cbrt': '$math.cbrt_v', '$math.fifthroot': '$math.fifthroot_v', // 1-arg; per-lane scalar repack
|
|
5125
|
+
'$math.pow_fold': '$math.pow_fold_v', // 2-arg (x, c); only reachable under optimize.crPow — see module/math.js
|
|
4471
5126
|
// log/exp/exp2: TRUE f64x2 polys — both lanes one evaluation (≈2×, beats V8 native log). Bit-exact
|
|
4472
5127
|
// via hot-path-vectorized + scalar-edge-fallback ($math.log_v/exp_v/exp2_v, module/math.js).
|
|
4473
5128
|
'$math.log': '$math.log_v', '$math.exp': '$math.exp_v', '$math.exp2': '$math.exp2_v',
|
|
4474
5129
|
}
|
|
4475
5130
|
|
|
4476
|
-
//
|
|
4477
|
-
//
|
|
4478
|
-
//
|
|
4479
|
-
|
|
5131
|
+
// Transcendentals the auto-vectorizer bridges to f64x2 mirrors — BOTH the scalar sources (kept
|
|
5132
|
+
// intact in the vectorized loop's scalar tail) AND the f64x2 mirrors themselves (the calls the
|
|
5133
|
+
// SIMD path emits). jz passes this to watOptimize's `pin` option so watr's inliner dissolves
|
|
5134
|
+
// NEITHER: the scalar tail keeps calling `$math.cbrt` and the SIMD body keeps calling
|
|
5135
|
+
// `$math.cbrt_v` (inlining the small per-lane repack mirror would erase the vectorized call the
|
|
5136
|
+
// lift produced). The protection policy lives here in jz, not hardcoded in watr.
|
|
5137
|
+
export const SIMD_PINNED = [...new Set([...Object.keys(PPC_CALL2), ...Object.values(PPC_CALL2)])]
|
|
4480
5138
|
|
|
4481
5139
|
// Per-pixel-color vectorizer. The dual of tryDivergentEscapeVectorize for kernels with NO inner
|
|
4482
5140
|
// escape loop: an outer pixel loop whose body computes an f64 value from the pixel index (via
|
|
@@ -5208,16 +5866,23 @@ function tryConvColumn(blockNode, fnLocals, freshIdRef, enabled) {
|
|
|
5208
5866
|
|
|
5209
5867
|
// A byte tap operand: convert_i32_{s,u}(i32.load8_{s,u}(addr)). Returns { load, addr, signed }.
|
|
5210
5868
|
const matchByteLoad = (n) => {
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5869
|
+
// Accept the f64 form (convert_i32_{s,u}(load8)) AND the bare i32 load —
|
|
5870
|
+
// the emit-level convert-peel narrows int8·int8 to i32.mul(load8, load8),
|
|
5871
|
+
// so the taps arrive unconverted (the better shape: no f64 detour to undo).
|
|
5872
|
+
let ld = null
|
|
5873
|
+
if (isArr(n) && (n[0] === 'f64.convert_i32_s' || n[0] === 'f64.convert_i32_u') && isArr(n[1])) ld = n[1]
|
|
5874
|
+
else if (isArr(n) && (n[0] === 'i32.load8_s' || n[0] === 'i32.load8_u')) ld = n
|
|
5875
|
+
if (!ld || (ld[0] !== 'i32.load8_s' && ld[0] !== 'i32.load8_u')) return null
|
|
5214
5876
|
const addr = (typeof ld[1] === 'string' && ld[1].startsWith('offset=')) ? ld[2] : ld[1]
|
|
5215
5877
|
return { load: ld, addr, signed: ld[0] === 'i32.load8_s' }
|
|
5216
5878
|
}
|
|
5217
5879
|
const load64 = (ld) => (typeof ld[1] === 'string' && ld[1].startsWith('offset=')) ? ['v128.load64_zero', ld[1], ld[2]] : ['v128.load64_zero', ld[1]]
|
|
5218
5880
|
// Lift a single product addend `inp·wt` (exactly one side gathers on ox) to an i16x8 of 8 products.
|
|
5219
5881
|
const liftProduct = (prod) => {
|
|
5220
|
-
|
|
5882
|
+
// f64.mul(cvt(load), cvt(load)) — pre-peel — or f64.convert_i32_s(i32.mul(
|
|
5883
|
+
// load, load)) / bare i32.mul(load, load) — the peeled faithful product.
|
|
5884
|
+
if (isArr(prod) && prod[0] === 'f64.convert_i32_s' && isArr(prod[1]) && prod[1][0] === 'i32.mul') prod = prod[1]
|
|
5885
|
+
if (!isArr(prod) || (prod[0] !== 'f64.mul' && prod[0] !== 'i32.mul')) return null
|
|
5221
5886
|
const a = matchByteLoad(prod[1]), b = matchByteLoad(prod[2])
|
|
5222
5887
|
if (!a || !b) return null
|
|
5223
5888
|
const ag = isGatherAddr(a.addr), bg = isGatherAddr(b.addr)
|
|
@@ -5702,6 +6367,183 @@ function tryToneMap(bl, fnLocals, freshIdRef, enabled) {
|
|
|
5702
6367
|
return { wrapper, newLocalDecls }
|
|
5703
6368
|
}
|
|
5704
6369
|
|
|
6370
|
+
// ---- Radix-2 butterfly 2-wide lift (tryButterfly) ----
|
|
6371
|
+
//
|
|
6372
|
+
// The Cooley-Tukey inner loop — the one shape the generic lane lift can never take:
|
|
6373
|
+
// a DUAL-IV scaffold (`j++` carries the exit test, `k += STEP` walks the twiddle
|
|
6374
|
+
// table) with an in-place complex-rotation update over four disjoint streams
|
|
6375
|
+
// (re/im × a/b, b = a + HALF, twiddles wre/wim read-only). Lanes j and j+1:
|
|
6376
|
+
// - every a/b access is an ADJACENT pair → one v128.load / v128.store,
|
|
6377
|
+
// - the twiddle pair is strided by STEP → two scalar f64.loads + lane combine
|
|
6378
|
+
// (same load count as two scalar iterations),
|
|
6379
|
+
// - the +/−/× rotation lanes with NO reassociation and NO fusion — each lane
|
|
6380
|
+
// computes the exact scalar sequence, so the result is bit-identical and the
|
|
6381
|
+
// cross-engine checksum contract holds.
|
|
6382
|
+
// LEGALITY. The strip body runs only while j+1 < half, so b = a + half ≥ a + 2:
|
|
6383
|
+
// {a,a+1} and {b,b+1} never overlap — the b-pair stores cannot clobber lane 1's
|
|
6384
|
+
// a-pair loads, and the single im[a] pair load legitimately serves both the
|
|
6385
|
+
// im[b] store and the im[a] writeback (im[a] ∉ {im[b−1], im[b]}). re/im/wre/wim
|
|
6386
|
+
// are distinct base locals under the vectorizer's standing distinct-base
|
|
6387
|
+
// non-aliasing model. HALF/STEP and the four bases must not be written in the
|
|
6388
|
+
// loop (checked); j/k are exactly reproduced for the scalar tail (each strip
|
|
6389
|
+
// iteration consumes two scalar iterations), and the tail IS the original loop,
|
|
6390
|
+
// so an odd half (or half < 2) falls through untouched.
|
|
6391
|
+
function tryButterfly(blockNode, fnLocals, freshIdRef) {
|
|
6392
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block' || typeof blockNode[1] !== 'string') return null
|
|
6393
|
+
const brk = blockNode[1]
|
|
6394
|
+
if (blockNode.length !== 3 || !isArr(blockNode[2]) || blockNode[2][0] !== 'loop') return null
|
|
6395
|
+
const loop = blockNode[2]
|
|
6396
|
+
const lbl = loop[1]
|
|
6397
|
+
if (typeof lbl !== 'string') return null
|
|
6398
|
+
// scaffold: (loop $L (br_if $brk (i32.eqz (i32.lt_s J HALF))) BODY×17 INC 'drop' (br $L))
|
|
6399
|
+
const exit = loop[2]
|
|
6400
|
+
if (!isArr(exit) || exit[0] !== 'br_if' || exit[1] !== brk) return null
|
|
6401
|
+
const ez = exit[2]
|
|
6402
|
+
if (!isArr(ez) || ez[0] !== 'i32.eqz' || !isArr(ez[1]) || ez[1][0] !== 'i32.lt_s') return null
|
|
6403
|
+
const [, jGet, halfGet] = ez[1]
|
|
6404
|
+
if (!isLocalGet(jGet) || !isLocalGet(halfGet)) return null
|
|
6405
|
+
const J = jGet[1], HALF = halfGet[1]
|
|
6406
|
+
const end = loop.length - 1
|
|
6407
|
+
if (!isArr(loop[end]) || loop[end][0] !== 'br' || loop[end][1] !== lbl) return null
|
|
6408
|
+
if (loop[end - 1] !== 'drop') return null
|
|
6409
|
+
// inc: (block (result i32) (drop (i32.sub (local.tee J (i32.add J 1)) 1)) (local.tee K (i32.add K STEP)))
|
|
6410
|
+
const inc = loop[end - 2]
|
|
6411
|
+
if (!isArr(inc) || inc[0] !== 'block' || !isArr(inc[1]) || inc[1][0] !== 'result' || inc.length !== 4) return null
|
|
6412
|
+
const jInc = inc[2], kInc = inc[3]
|
|
6413
|
+
if (!isArr(jInc) || jInc[0] !== 'drop' || !isArr(jInc[1]) || jInc[1][0] !== 'i32.sub') return null
|
|
6414
|
+
const jTee = jInc[1][1]
|
|
6415
|
+
if (!isArr(jTee) || jTee[0] !== 'local.tee' || jTee[1] !== J || !isArr(jTee[2]) || jTee[2][0] !== 'i32.add'
|
|
6416
|
+
|| !isLocalGet(jTee[2][1], J) || constNum(jTee[2][2]) !== 1) return null
|
|
6417
|
+
if (!isArr(kInc) || kInc[0] !== 'local.tee' || !isArr(kInc[2]) || kInc[2][0] !== 'i32.add') return null
|
|
6418
|
+
const K = kInc[1]
|
|
6419
|
+
if (typeof K !== 'string' || !isLocalGet(kInc[2][1], K) || !isLocalGet(kInc[2][2])) return null
|
|
6420
|
+
const STEP = kInc[2][2][1]
|
|
6421
|
+
const body = loop.slice(3, end - 2)
|
|
6422
|
+
if (body.length !== 17) return null
|
|
6423
|
+
|
|
6424
|
+
// unification environment over the exact emit shapes
|
|
6425
|
+
const U = {}
|
|
6426
|
+
const bind = (name, v) => U[name] === undefined ? (U[name] = v, true) : U[name] === v
|
|
6427
|
+
const idx8 = (n, base, iv) => isArr(n) && n[0] === 'i32.add'
|
|
6428
|
+
&& isLocalGet(n[1]) && bind(base, n[1][1])
|
|
6429
|
+
&& isArr(n[2]) && n[2][0] === 'i32.shl' && isLocalGet(n[2][1]) && bind(iv, n[2][1][1])
|
|
6430
|
+
&& constNum(n[2][2]) === 3
|
|
6431
|
+
const setF64Load = (st, name, base, iv, ab) => {
|
|
6432
|
+
if (!isArr(st) || st[0] !== 'local.set' || st.length !== 3 || !isArr(st[2]) || st[2][0] !== 'f64.load') return false
|
|
6433
|
+
let addr = st[2][1]
|
|
6434
|
+
if (ab != null) {
|
|
6435
|
+
if (!isArr(addr) || addr[0] !== 'local.tee') return false
|
|
6436
|
+
if (!bind(ab, addr[1])) return false
|
|
6437
|
+
addr = addr[2]
|
|
6438
|
+
}
|
|
6439
|
+
if (!idx8(addr, base, iv)) return false
|
|
6440
|
+
return bind(name, st[1])
|
|
6441
|
+
}
|
|
6442
|
+
const g = (n, name) => isLocalGet(n) && U[name] !== undefined && n[1] === U[name]
|
|
6443
|
+
const mulPair = (n, x, y) => isArr(n) && n[0] === 'f64.mul' && g(n[1], x) && g(n[2], y)
|
|
6444
|
+
const setArith = (st, name, op, mk) => {
|
|
6445
|
+
if (!isArr(st) || st[0] !== 'local.set' || st.length !== 3 || !isArr(st[2]) || st[2][0] !== op) return false
|
|
6446
|
+
if (!mk(st[2])) return false
|
|
6447
|
+
return bind(name, st[1])
|
|
6448
|
+
}
|
|
6449
|
+
// flat pair: (local.set T (op LHS VAL)) ; (f64.store (local.get AB) (local.get T))
|
|
6450
|
+
const storePair = (setSt, stoSt, op, lhs, val2, ab) => {
|
|
6451
|
+
if (!isArr(setSt) || setSt[0] !== 'local.set' || !isArr(setSt[2]) || setSt[2][0] !== op) return false
|
|
6452
|
+
const e = setSt[2]
|
|
6453
|
+
if (!lhs(e[1]) || !g(e[2], val2)) return false
|
|
6454
|
+
if (!isArr(stoSt) || stoSt[0] !== 'f64.store' || !g(stoSt[1], ab) || !isLocalGet(stoSt[2], setSt[1])) return false
|
|
6455
|
+
return true
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
if (!setF64Load(body[0], 'WR', 'WRE', 'K0', null) || U.K0 !== K) return null
|
|
6459
|
+
if (!setF64Load(body[1], 'WI', 'WIM', 'K1', null) || U.K1 !== K) return null
|
|
6460
|
+
{ // a = I + j (either order), I ≠ J
|
|
6461
|
+
const st = body[2]
|
|
6462
|
+
if (!isArr(st) || st[0] !== 'local.set' || !isArr(st[2]) || st[2][0] !== 'i32.add') return null
|
|
6463
|
+
const [, l, r] = st[2]
|
|
6464
|
+
if (isLocalGet(l) && isLocalGet(r, J) && l[1] !== J) U.I = l[1]
|
|
6465
|
+
else if (isLocalGet(r) && isLocalGet(l, J) && r[1] !== J) U.I = r[1]
|
|
6466
|
+
else return null
|
|
6467
|
+
U.A = st[1]
|
|
6468
|
+
}
|
|
6469
|
+
{ // b = a + half (either order)
|
|
6470
|
+
const st = body[3]
|
|
6471
|
+
if (!isArr(st) || st[0] !== 'local.set' || !isArr(st[2]) || st[2][0] !== 'i32.add') return null
|
|
6472
|
+
const [, l, r] = st[2]
|
|
6473
|
+
if (!((isLocalGet(l, U.A) && isLocalGet(r, HALF)) || (isLocalGet(r, U.A) && isLocalGet(l, HALF)))) return null
|
|
6474
|
+
U.B = st[1]
|
|
6475
|
+
}
|
|
6476
|
+
if (!setF64Load(body[4], 'XR', 'RE', 'B0', 'AB4') || U.B0 !== U.B) return null
|
|
6477
|
+
if (!setF64Load(body[5], 'XI', 'IM', 'B1', 'AB5') || U.B1 !== U.B) return null
|
|
6478
|
+
if (!setArith(body[6], 'TR', 'f64.sub', e => mulPair(e[1], 'WR', 'XR') && mulPair(e[2], 'WI', 'XI'))) return null
|
|
6479
|
+
if (!setArith(body[7], 'TI', 'f64.add', e => mulPair(e[1], 'WR', 'XI') && mulPair(e[2], 'WI', 'XR'))) return null
|
|
6480
|
+
if (!setF64Load(body[8], 'C0', 'RE', 'A0', 'AB6') || U.A0 !== U.A) return null
|
|
6481
|
+
const c0lhs = (n) => g(n, 'C0')
|
|
6482
|
+
const ab7teeLhs = (n) => { // (f64.load (local.tee AB7 (im + a<<3)))
|
|
6483
|
+
if (!isArr(n) || n[0] !== 'f64.load' || !isArr(n[1]) || n[1][0] !== 'local.tee') return false
|
|
6484
|
+
if (!bind('AB7', n[1][1])) return false
|
|
6485
|
+
return idx8(n[1][2], 'IM', 'A2') && U.A2 === U.A
|
|
6486
|
+
}
|
|
6487
|
+
const ab7getLhs = (n) => isArr(n) && n[0] === 'f64.load' && g(n[1], 'AB7')
|
|
6488
|
+
if (!storePair(body[9], body[10], 'f64.sub', c0lhs, 'TR', 'AB4')) return null
|
|
6489
|
+
if (!storePair(body[11], body[12], 'f64.sub', ab7teeLhs, 'TI', 'AB5')) return null
|
|
6490
|
+
if (!storePair(body[13], body[14], 'f64.add', c0lhs, 'TR', 'AB6')) return null
|
|
6491
|
+
if (!storePair(body[15], body[16], 'f64.add', ab7getLhs, 'TI', 'AB7')) return null
|
|
6492
|
+
// loop-invariance: the four bases, HALF/STEP and the outer offset I are never written in the body
|
|
6493
|
+
const invariants = new Set([U.RE, U.IM, U.WRE, U.WIM, HALF, STEP, U.I].filter(x => typeof x === 'string'))
|
|
6494
|
+
if (invariants.size !== 7) return null
|
|
6495
|
+
let clobbered = false
|
|
6496
|
+
const wscan = (n) => { if (clobbered || !isArr(n)) return
|
|
6497
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && invariants.has(n[1])) { clobbered = true; return }
|
|
6498
|
+
for (let i = 1; i < n.length; i++) wscan(n[i]) }
|
|
6499
|
+
for (const st of body) wscan(st)
|
|
6500
|
+
if (clobbered) return null
|
|
6501
|
+
if (new Set([U.RE, U.IM, U.WRE, U.WIM]).size !== 4) return null
|
|
6502
|
+
|
|
6503
|
+
const id = freshIdRef.next++
|
|
6504
|
+
const nm = (t) => `$__bf${id}_${t}`
|
|
6505
|
+
const L = (x) => ['local.get', x]
|
|
6506
|
+
const addr = (base, iv) => ['i32.add', L(base), ['i32.shl', L(iv), ['i32.const', 3]]]
|
|
6507
|
+
const twiddle = (base) => ['f64x2.replace_lane', 1,
|
|
6508
|
+
['f64x2.splat', ['f64.load', addr(base, K)]],
|
|
6509
|
+
['f64.load', ['i32.add', L(base), ['i32.shl', ['i32.add', L(K), L(STEP)], ['i32.const', 3]]]]]
|
|
6510
|
+
const wrv = nm('wrv'), wiv = nm('wiv'), xrv = nm('xrv'), xiv = nm('xiv')
|
|
6511
|
+
const trv = nm('trv'), tiv = nm('tiv'), c0v = nm('c0v'), iav = nm('iav')
|
|
6512
|
+
const av = nm('a'), bv = nm('b'), vl = nm('L'), strip = nm('go')
|
|
6513
|
+
const newLocalDecls = [
|
|
6514
|
+
['local', wrv, 'v128'], ['local', wiv, 'v128'], ['local', xrv, 'v128'], ['local', xiv, 'v128'],
|
|
6515
|
+
['local', trv, 'v128'], ['local', tiv, 'v128'], ['local', c0v, 'v128'], ['local', iav, 'v128'],
|
|
6516
|
+
['local', av, 'i32'], ['local', bv, 'i32'],
|
|
6517
|
+
]
|
|
6518
|
+
const stripGuard = () => ['i32.lt_s', ['i32.add', L(J), ['i32.const', 1]], L(HALF)]
|
|
6519
|
+
const vbody = [
|
|
6520
|
+
['local.set', wrv, twiddle(U.WRE)],
|
|
6521
|
+
['local.set', wiv, twiddle(U.WIM)],
|
|
6522
|
+
['local.set', av, ['i32.add', L(U.I), L(J)]],
|
|
6523
|
+
['local.set', bv, ['i32.add', L(av), L(HALF)]],
|
|
6524
|
+
['local.set', xrv, ['v128.load', addr(U.RE, bv)]],
|
|
6525
|
+
['local.set', xiv, ['v128.load', addr(U.IM, bv)]],
|
|
6526
|
+
['local.set', trv, ['f64x2.sub', ['f64x2.mul', L(wrv), L(xrv)], ['f64x2.mul', L(wiv), L(xiv)]]],
|
|
6527
|
+
['local.set', tiv, ['f64x2.add', ['f64x2.mul', L(wrv), L(xiv)], ['f64x2.mul', L(wiv), L(xrv)]]],
|
|
6528
|
+
['local.set', c0v, ['v128.load', addr(U.RE, av)]],
|
|
6529
|
+
['v128.store', addr(U.RE, bv), ['f64x2.sub', L(c0v), L(trv)]],
|
|
6530
|
+
['local.set', iav, ['v128.load', addr(U.IM, av)]],
|
|
6531
|
+
['v128.store', addr(U.IM, bv), ['f64x2.sub', L(iav), L(tiv)]],
|
|
6532
|
+
['v128.store', addr(U.RE, av), ['f64x2.add', L(c0v), L(trv)]],
|
|
6533
|
+
['v128.store', addr(U.IM, av), ['f64x2.add', L(iav), L(tiv)]],
|
|
6534
|
+
['local.set', J, ['i32.add', L(J), ['i32.const', 2]]],
|
|
6535
|
+
['local.set', K, ['i32.add', L(K), ['i32.add', L(STEP), L(STEP)]]],
|
|
6536
|
+
['br_if', vl, stripGuard()],
|
|
6537
|
+
]
|
|
6538
|
+
const wrapper = ['block',
|
|
6539
|
+
['block', strip,
|
|
6540
|
+
['br_if', strip, ['i32.eqz', stripGuard()]],
|
|
6541
|
+
['loop', vl, ...vbody]],
|
|
6542
|
+
blockNode] // the ORIGINAL loop is the scalar tail: 0..1 leftover iterations, or everything when half < 2
|
|
6543
|
+
return { wrapper, newLocalDecls }
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6546
|
+
|
|
5705
6547
|
// ---- Pass entry ------------------------------------------------------------
|
|
5706
6548
|
|
|
5707
6549
|
/**
|
|
@@ -5713,13 +6555,27 @@ function tryToneMap(bl, fnLocals, freshIdRef, enabled) {
|
|
|
5713
6555
|
// multiAcc, relaxedFma, blurMP, whyNot, stencil, outerStrip, pureFuncMap, toneMap.
|
|
5714
6556
|
export function vectorizeLaneLocal(fn, opts = {}) {
|
|
5715
6557
|
const { multiAcc = false, relaxedFma = false, blurMP = true, whyNot = false,
|
|
5716
|
-
stencil = false, outerStrip = false, pureFuncMap = null, toneMap = false, slp = false } = opts
|
|
6558
|
+
stencil = false, outerStrip = false, pureFuncMap = null, toneMap = false, slp = false, crPow = false } = opts
|
|
5717
6559
|
if (!isArr(fn) || fn[0] !== 'func') return
|
|
5718
6560
|
const bodyStart = findBodyStart(fn)
|
|
5719
6561
|
if (bodyStart < 0) return
|
|
5720
6562
|
const fnName = typeof fn[1] === 'string' ? fn[1] : '(anon)'
|
|
5721
6563
|
let whyNotN = 0
|
|
5722
6564
|
|
|
6565
|
+
// Normalize jz's per-statement `block` grouping into flat statement lists ONCE, up front —
|
|
6566
|
+
// the recognizers below (both the scaffold consumers and the raw-node matchers like ramp-map,
|
|
6567
|
+
// stencil, per-pixel) were tuned on watr's flattened shape. Pre-watr, jz wraps each source
|
|
6568
|
+
// statement group in a transparent block; without this every loop body would arrive as a
|
|
6569
|
+
// single opaque `block` node and no lift would fire. Walking `fn` itself also flattens a
|
|
6570
|
+
// top-level body block (decls never match — they aren't blocks).
|
|
6571
|
+
normalizeTransparentBlocks(fn)
|
|
6572
|
+
// Canonicalize the raw arithmetic identities watr would fold (chiefly `i<<0` byte addresses),
|
|
6573
|
+
// so the address/value matchers read dataflow, not jz's un-folded emission.
|
|
6574
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = foldVecIdentities(fn[i])
|
|
6575
|
+
// Canonicalize the `if COND (then (br L))` break idiom to `br_if L COND` (watr's brif shape),
|
|
6576
|
+
// so the loop-scan recognizers see the branch form they were tuned against.
|
|
6577
|
+
canonicalizeIfBr(fn)
|
|
6578
|
+
|
|
5723
6579
|
// Build local-name → wasm-type map.
|
|
5724
6580
|
const fnLocals = new Map()
|
|
5725
6581
|
for (let i = 2; i < bodyStart; i++) {
|
|
@@ -5731,8 +6587,31 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5731
6587
|
}
|
|
5732
6588
|
}
|
|
5733
6589
|
|
|
6590
|
+
// Loop-invariant constant locals — hoistConstantPool's `$…_pg` pool (`set $L (f64.const C)`,
|
|
6591
|
+
// written exactly once). name → numeric value, used to resolve a constant `pow` exponent that
|
|
6592
|
+
// reached the loop as a pooled local rather than a literal.
|
|
6593
|
+
const constLocals = new Map()
|
|
6594
|
+
{
|
|
6595
|
+
const setCount = new Map()
|
|
6596
|
+
const walkC = (n) => {
|
|
6597
|
+
if (!isArr(n)) return
|
|
6598
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string' && n.length === 3) {
|
|
6599
|
+
setCount.set(n[1], (setCount.get(n[1]) || 0) + 1)
|
|
6600
|
+
if (isArr(n[2]) && n[2][0] === 'f64.const') constLocals.set(n[1], +n[2][1])
|
|
6601
|
+
}
|
|
6602
|
+
for (let i = 1; i < n.length; i++) walkC(n[i])
|
|
6603
|
+
}
|
|
6604
|
+
for (let i = bodyStart; i < fn.length; i++) walkC(fn[i])
|
|
6605
|
+
for (const [k, c] of setCount) if (c !== 1) constLocals.delete(k) // multiply-written → not invariant
|
|
6606
|
+
}
|
|
6607
|
+
|
|
5734
6608
|
const freshIdRef = { next: 0 }
|
|
5735
6609
|
const newLocalDeclsAll = []
|
|
6610
|
+
// Whether a REAL SIMD lift happened (as opposed to the scalar tryStrengthReduceIV
|
|
6611
|
+
// fallback below, which also populates newLocalDeclsAll with plain i32 locals) —
|
|
6612
|
+
// the caller pins the function's $name/$name$exp boundary wrapper on this, so a
|
|
6613
|
+
// false positive here would needlessly block watr's inlineOnce on a non-SIMD fn.
|
|
6614
|
+
let simdFired = false
|
|
5736
6615
|
|
|
5737
6616
|
// Hoist loop-invariant partial products out of unrolled dot reductions (rust/LLVM's
|
|
5738
6617
|
// mat4 prologue trick). Reassociates the float sum, so tied to the relaxedFma tier;
|
|
@@ -5743,6 +6622,7 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5743
6622
|
// SLP within-iteration store pairs. Sound only with no aliasing typed-array view
|
|
5744
6623
|
// in the module (else a shifted view could reorder-hazard the packed read/write).
|
|
5745
6624
|
if (slp && !ctx.features.typedView) slpStorePairsIn(fn, fnLocals, freshIdRef, newLocalDeclsAll, slpGetCounts(fn))
|
|
6625
|
+
if (newLocalDeclsAll.length) simdFired = true
|
|
5746
6626
|
|
|
5747
6627
|
// Walk body recursively. Process inner-most matches first (post-order)
|
|
5748
6628
|
// so we don't try to vectorize an outer loop whose inner is the lane-local one.
|
|
@@ -5769,7 +6649,7 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5769
6649
|
const bl = matchBlockLoop(node, { allowPreamble: true, allowInlinedLi: true })
|
|
5770
6650
|
let r = tryDivergentEscapeVectorize(node, fnLocals, freshIdRef)
|
|
5771
6651
|
?? tryMemCopyFill(bl, fnLocals, freshIdRef)
|
|
5772
|
-
?? tryVectorize(bl, fnLocals, freshIdRef)
|
|
6652
|
+
?? tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals)
|
|
5773
6653
|
?? tryReduceVectorize(bl, fnLocals, freshIdRef, multiAcc)
|
|
5774
6654
|
?? tryMapReduceVectorize(bl, fnLocals, freshIdRef)
|
|
5775
6655
|
?? tryStencil(node, fnLocals, freshIdRef, stencil)
|
|
@@ -5782,6 +6662,7 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5782
6662
|
?? tryIteratedReduce(node, fnLocals, freshIdRef, outerStrip)
|
|
5783
6663
|
?? tryConvColumn(node, fnLocals, freshIdRef, outerStrip)
|
|
5784
6664
|
?? tryToneMap(bl, fnLocals, freshIdRef, toneMap)
|
|
6665
|
+
?? tryButterfly(node, fnLocals, freshIdRef)
|
|
5785
6666
|
// --why-not-simd: a canonical loop-shaped candidate that no SIMD pass took.
|
|
5786
6667
|
// Reported BEFORE the scalar strength-reduce fallback (which fires on most
|
|
5787
6668
|
// affine loops and would otherwise mask "didn't vectorize"). Diagnostic only.
|
|
@@ -5791,19 +6672,42 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5791
6672
|
`${fnName}: loop #${whyNotN} not vectorized — ${_whyNotReason || 'no SIMD-liftable shape (loop-carried dependency, non-affine address, or unsupported control flow)'}`,
|
|
5792
6673
|
{ fn: `${fnName}#${whyNotN}` })
|
|
5793
6674
|
}
|
|
5794
|
-
|
|
5795
|
-
r = r ?? tryStrengthReduceIV(bl, fnLocals, freshIdRef)
|
|
6675
|
+
if (r) simdFired = true // one of the real SIMD recognizers above matched
|
|
5796
6676
|
if (r) {
|
|
6677
|
+
// Mark the consumed subtree: the wrapper REUSES these nodes (scalar tail, and the
|
|
6678
|
+
// lane splats alias the original load nodes), so a deferred strength-reduce must
|
|
6679
|
+
// never rewrite inside them — it would mutate the lifted lanes through the alias.
|
|
6680
|
+
const mark = (n) => { if (isArr(n)) { srConsumed.add(n); for (let i = 0; i < n.length; i++) mark(n[i]) } }
|
|
6681
|
+
mark(node)
|
|
5797
6682
|
parent[idx] = r.wrapper
|
|
5798
6683
|
newLocalDeclsAll.push(...r.newLocalDecls)
|
|
6684
|
+
} else if (bl) {
|
|
6685
|
+
// Scalar IV strength-reduction is a non-SIMD fallback — DEFERRED to after the
|
|
6686
|
+
// whole walk. Applied eagerly here (post-order = innermost first) it rewrites an
|
|
6687
|
+
// inner reduction loop into its wrapper before the ENCLOSING loop's outer
|
|
6688
|
+
// recognizers (outer-strip / iterated-reduce / conv-column / tone-map) ever run,
|
|
6689
|
+
// and they bail on the non-canonical inner shape — metaballs' pixel loop lost its
|
|
6690
|
+
// whole f64x2 outer-strip to an eager strength-reduce of the blob loop.
|
|
6691
|
+
deferredSR.push([node, parent, idx, bl])
|
|
5799
6692
|
}
|
|
5800
6693
|
}
|
|
5801
6694
|
}
|
|
6695
|
+
const deferredSR = [], srConsumed = new Set()
|
|
5802
6696
|
_whyNotActive = whyNot
|
|
5803
6697
|
_relaxF32 = relaxedFma
|
|
6698
|
+
_crPow = crPow
|
|
5804
6699
|
for (let i = bodyStart; i < fn.length; i++) walk(fn, i)
|
|
5805
6700
|
_whyNotActive = false
|
|
5806
6701
|
_relaxF32 = false
|
|
6702
|
+
_crPow = false
|
|
6703
|
+
// Apply the deferred scalar fallback innermost-first (push order), skipping candidates
|
|
6704
|
+
// inside any SIMD-consumed subtree (see the mark above), plus a same-slot check for
|
|
6705
|
+
// wrappers that replaced the candidate node itself.
|
|
6706
|
+
for (const [node, parent, idx, bl] of deferredSR) {
|
|
6707
|
+
if (srConsumed.has(node) || parent[idx] !== node) continue
|
|
6708
|
+
const r = tryStrengthReduceIV(bl, fnLocals, freshIdRef)
|
|
6709
|
+
if (r) { parent[idx] = r.wrapper; newLocalDeclsAll.push(...r.newLocalDecls) }
|
|
6710
|
+
}
|
|
5807
6711
|
|
|
5808
6712
|
if (newLocalDeclsAll.length) {
|
|
5809
6713
|
// Sibling loops (and the straight-line dot pass) can each lift the SAME source
|
|
@@ -5815,4 +6719,5 @@ export function vectorizeLaneLocal(fn, opts = {}) {
|
|
|
5815
6719
|
// so keep one decl per name (all dups are the identical `['local', name, 'v128']`).
|
|
5816
6720
|
fn.splice(bodyStart, 0, ...new Map(newLocalDeclsAll.map(d => [d[1], d])).values())
|
|
5817
6721
|
}
|
|
6722
|
+
return simdFired
|
|
5818
6723
|
}
|