jz 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/optimize.js CHANGED
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { LAYOUT } from './ctx.js'
31
+ import { findBodyStart } from './ir.js'
31
32
  import { vectorizeLaneLocal } from './vectorize.js'
32
33
 
33
34
  const MEMOP = /^[fi](32|64)\.(load|store)(\d+(_[su])?)?$/
@@ -44,9 +45,17 @@ const UNDEF_BITS = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHI
44
45
  * 0 — nothing. Fastest compile, largest output. Useful for live coding.
45
46
  * 1 — encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
46
47
  * Cheap, no IR rewrites that perturb V8's tier-up shape.
47
- * 2 — default. Stable passes (watr CSE/DCE/inline + every jz pass that has
48
- * benchmark proof).
48
+ * 2 — default. Stable jz passes (every pass with benchmark proof). watr disabled
49
+ * by default — adds ~2s compile time for identical runtime performance.
49
50
  * 3 — aggressive experimental passes in addition to level 2.
51
+ *
52
+ * String aliases (the size↔speed tradeoff lives entirely in the unroll/scalar
53
+ * knobs, so the aliases just dial those):
54
+ * 'size' — level 2 with loop/const unroll + lane vectorization off and tight
55
+ * scalar-replacement caps. Smallest wasm; still fully optimized.
56
+ * 'balanced' — the default (= level 2).
57
+ * 'speed' — level 2 with full nested unroll + lane vectorization on (= level 3
58
+ * minus the heavy third-party watr pass).
50
59
  */
51
60
  export const PASS_NAMES = [
52
61
  'watr', // third-party WAT-level CSE/DCE/inlining (heaviest)
@@ -58,6 +67,8 @@ export const PASS_NAMES = [
58
67
  'hoistInvariantCellLoads',
59
68
  'cseScalarLoad',
60
69
  'csePureExpr',
70
+ 'deadStoreElim',
71
+ 'promoteGlobals', // read-only global.get → local for multi-read globals
61
72
  'sortLocalsByUse',
62
73
  'specializeMkptr',
63
74
  'specializePtrBase',
@@ -76,8 +87,16 @@ const ALL_OFF = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, false])
76
87
  const LEVEL_PRESETS = Object.freeze({
77
88
  0: ALL_OFF,
78
89
  1: Object.freeze({ ...ALL_OFF, treeshake: true, sortLocalsByUse: true, fusedRewrite: true }),
79
- 2: Object.freeze({ ...ALL_ON, nestedSmallConstForUnroll: 'auto' }),
90
+ 2: Object.freeze({ ...ALL_ON, watr: false, nestedSmallConstForUnroll: 'auto' }),
80
91
  3: ALL_ON,
92
+ // Named aliases — dial the size↔speed knobs on top of the level-2 base.
93
+ balanced: Object.freeze({ ...ALL_ON, watr: false, nestedSmallConstForUnroll: 'auto' }),
94
+ size: Object.freeze({
95
+ ...ALL_ON, watr: false,
96
+ smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false,
97
+ scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
98
+ }),
99
+ speed: Object.freeze({ ...ALL_ON, watr: false }),
81
100
  })
82
101
 
83
102
  /**
@@ -86,18 +105,22 @@ const LEVEL_PRESETS = Object.freeze({
86
105
  * resolveOptimize(undefined | true) → level 2 stable defaults
87
106
  * resolveOptimize(false | 0) → all off
88
107
  * resolveOptimize(1 | 2 | 3) → preset for that level
108
+ * resolveOptimize('size' | 'speed' | 'balanced') → named alias preset
89
109
  * resolveOptimize({ level: 1, watr: true }) → level 1 base, with watr forced on
110
+ * resolveOptimize({ level: 'size', vectorizeLaneLocal: true }) → 'size' base, override
90
111
  * resolveOptimize({ hoistAddrBase: false }) → level 2 base, hoistAddrBase off
91
112
  */
92
113
  export function resolveOptimize(opt) {
93
114
  if (opt === false || opt === 0) return { ...ALL_OFF }
94
115
  if (opt === true || opt == null) return { ...LEVEL_PRESETS[2] }
95
- if (typeof opt === 'number') return { ...(LEVEL_PRESETS[opt] || ALL_ON) }
116
+ if (typeof opt === 'number' || typeof opt === 'string') return { ...(LEVEL_PRESETS[opt] || LEVEL_PRESETS[2]) }
96
117
  if (typeof opt === 'object') {
97
- const baseLevel = typeof opt.level === 'number' ? opt.level : 2
118
+ const baseLevel = typeof opt.level === 'number' || typeof opt.level === 'string' ? opt.level : 2
98
119
  const base = LEVEL_PRESETS[baseLevel] || ALL_ON
99
120
  const out = { ...base }
100
121
  for (const n of PASS_NAMES) if (n in opt) out[n] = n === 'nestedSmallConstForUnroll' && opt[n] === 'auto' ? 'auto' : !!opt[n]
122
+ // Preserve non-pass tuning keys (e.g. plan.js thresholds)
123
+ for (const k of Object.keys(opt)) if (!PASS_NAMES.includes(k)) out[k] = opt[k]
101
124
  return out
102
125
  }
103
126
  return { ...ALL_ON }
@@ -1023,14 +1046,23 @@ export function csePureExpr(fn) {
1023
1046
  while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__pe${snapId}`)) snapId++
1024
1047
  const newLocals = []
1025
1048
 
1026
- const COMMUTATIVE = new Set(['f64.mul', 'f64.add'])
1027
- const TARGET_OPS = new Set(['f64.mul', 'f64.add', 'f64.sub'])
1049
+ const COMMUTATIVE = new Set(['f64.mul', 'f64.add', 'i32.mul', 'i32.add', 'i32.and', 'i32.or', 'i32.xor', 'i64.mul', 'i64.add', 'i64.and', 'i64.or', 'i64.xor'])
1050
+ const TARGET_OPS = new Set([
1051
+ 'f64.mul', 'f64.add', 'f64.sub',
1052
+ 'i32.mul', 'i32.add', 'i32.sub', 'i32.shl', 'i32.shr_u', 'i32.shr_s', 'i32.and', 'i32.or', 'i32.xor',
1053
+ 'i64.mul', 'i64.add', 'i64.sub', 'i64.shl', 'i64.shr_u', 'i64.shr_s', 'i64.and', 'i64.or', 'i64.xor',
1054
+ ])
1055
+ const OP_TYPE = {
1056
+ 'f64.mul': 'f64', 'f64.add': 'f64', 'f64.sub': 'f64',
1057
+ 'i32.mul': 'i32', 'i32.add': 'i32', 'i32.sub': 'i32', 'i32.shl': 'i32', 'i32.shr_u': 'i32', 'i32.shr_s': 'i32', 'i32.and': 'i32', 'i32.or': 'i32', 'i32.xor': 'i32',
1058
+ 'i64.mul': 'i64', 'i64.add': 'i64', 'i64.sub': 'i64', 'i64.shl': 'i64', 'i64.shr_u': 'i64', 'i64.shr_s': 'i64', 'i64.and': 'i64', 'i64.or': 'i64', 'i64.xor': 'i64',
1059
+ }
1028
1060
 
1029
1061
  // Encode a leaf operand to a stable string key. Returns null if not pure-leaf.
1030
1062
  const leafKey = (n) => {
1031
1063
  if (!Array.isArray(n)) return null
1032
1064
  if (n[0] === 'local.get' && typeof n[1] === 'string') return `L:${n[1]}`
1033
- if (n[0] === 'f64.const') return `C:${n[1]}`
1065
+ if (n[0] === 'f64.const' || n[0] === 'i32.const' || n[0] === 'i64.const' || n[0] === 'f32.const') return `C:${n[0]}:${n[1]}`
1034
1066
  return null
1035
1067
  }
1036
1068
 
@@ -1091,7 +1123,7 @@ export function csePureExpr(fn) {
1091
1123
  if (!entry.snapName) {
1092
1124
  const snapName = `$__pe${snapId++}`
1093
1125
  entry.snapName = snapName
1094
- newLocals.push(['local', snapName, 'f64'])
1126
+ newLocals.push(['local', snapName, OP_TYPE[op] || 'f64'])
1095
1127
  const orig = entry.anchorParent[entry.anchorIdx]
1096
1128
  entry.anchorParent[entry.anchorIdx] = ['local.tee', snapName, orig]
1097
1129
  }
@@ -1117,17 +1149,246 @@ export function csePureExpr(fn) {
1117
1149
  }
1118
1150
 
1119
1151
  /**
1120
- * Find the index of the first body-content child in a func node.
1121
- * Skips `$name`, (export …), (import …), (type …), (param …), (result …), (local …).
1152
+ * Dead-store elimination: remove `local.set` / `local.tee` and `drop` of pure
1153
+ * expressions whose values are never consumed.
1154
+ *
1155
+ * Conservative single-block analysis: tracks last-write per local within each
1156
+ * straight-line sequence. A write is dead if the same local is written again
1157
+ * before any intervening read in the same block. Control-flow boundaries
1158
+ * (block, loop, if) reset the table — we don't eliminate across branches.
1159
+ *
1160
+ * Also removes `drop` of pure expressions (e.g. leftover ptr-type calls).
1122
1161
  */
1123
- function findBodyStart(fn) {
1124
- for (let i = 2; i < fn.length; i++) {
1162
+ export function deadStoreElim(fn) {
1163
+ if (!Array.isArray(fn) || fn[0] !== 'func') return
1164
+ const bodyStart = findBodyStart(fn)
1165
+ if (bodyStart < 0) return
1166
+
1167
+ const dead = []
1168
+
1169
+ const collectGets = (node, out) => {
1170
+ if (!Array.isArray(node)) return
1171
+ if (node[0] === 'local.get' && typeof node[1] === 'string') { out.add(node[1]); return }
1172
+ for (let i = 1; i < node.length; i++) collectGets(node[i], out)
1173
+ }
1174
+
1175
+ const isPure = (node) => {
1176
+ if (!Array.isArray(node)) return true
1177
+ const op = node[0]
1178
+ if (typeof op === 'string' && MEMOP.test(op)) return false
1179
+ if (op === 'call' || op === 'call_indirect' || op === 'call_ref') return false
1180
+ if (op === 'global.get' || op === 'global.set') return false
1181
+ if (op === 'local.tee') return false
1182
+ if (op === 'memory.size' || op === 'memory.grow') return false
1183
+ for (let i = 1; i < node.length; i++) if (!isPure(node[i])) return false
1184
+ return true
1185
+ }
1186
+
1187
+ const scanBlock = (items, start, end) => {
1188
+ const lastWrite = new Map() // localName → { parent, idx }
1189
+
1190
+ for (let i = start; i < end; i++) {
1191
+ const node = items[i]
1192
+ if (!Array.isArray(node)) continue
1193
+ const op = node[0]
1194
+
1195
+ // Reads invalidate pending dead writes
1196
+ const reads = new Set()
1197
+ collectGets(node, reads)
1198
+ if (op === 'local.tee') reads.delete(node[1])
1199
+ for (const name of reads) lastWrite.delete(name)
1200
+
1201
+ // Drop of pure expr → dead
1202
+ if (op === 'drop' && isPure(node[1])) {
1203
+ dead.push({ parent: items, idx: i, drop: true })
1204
+ }
1205
+
1206
+ // Local write tracking
1207
+ if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') {
1208
+ const prev = lastWrite.get(node[1])
1209
+ if (prev) dead.push(prev)
1210
+ lastWrite.set(node[1], { parent: items, idx: i })
1211
+ }
1212
+
1213
+ // Recurse into nested blocks with fresh state
1214
+ if (op === 'block' || op === 'loop') {
1215
+ let j = 1
1216
+ while (j < node.length && Array.isArray(node[j]) && node[j][0] === 'result') j++
1217
+ scanBlock(node, j, node.length)
1218
+ } else if (op === 'if') {
1219
+ let j = 1
1220
+ while (j < node.length && Array.isArray(node[j]) && node[j][0] === 'result') j++
1221
+ const condReads = new Set()
1222
+ collectGets(node[j], condReads)
1223
+ for (const name of condReads) lastWrite.delete(name)
1224
+ j++
1225
+ for (; j < node.length; j++) {
1226
+ const c = node[j]
1227
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) scanBlock(c, 1, c.length)
1228
+ }
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ scanBlock(fn, bodyStart, fn.length)
1234
+
1235
+ // Remove in reverse order so indices stay valid
1236
+ for (let i = dead.length - 1; i >= 0; i--) {
1237
+ const d = dead[i]
1238
+ if (d.drop) {
1239
+ d.parent.splice(d.idx, 1)
1240
+ } else {
1241
+ const node = d.parent[d.idx]
1242
+ if (node[0] === 'local.tee') {
1243
+ // tee in statement position: replace with just the value (implicitly dropped)
1244
+ d.parent[d.idx] = node[2]
1245
+ } else {
1246
+ // set in statement position: remove entirely
1247
+ d.parent.splice(d.idx, 1)
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * Promote read-only globals to locals within each function.
1255
+ *
1256
+ * When a global is only read (never written) within a function and read ≥ 2 times,
1257
+ * load it once at function entry into a fresh local and replace all global.get with local.get.
1258
+ *
1259
+ * This eliminates repeated global.get instructions (5 bytes each with LEB128 idx) in
1260
+ * favour of cheaper local.get (1–2 bytes), and helps V8's TurboFan by reducing the
1261
+ * number of load-from-global operations it must track.
1262
+ *
1263
+ * Only promotes globals that appear read-only in the function body. Globals that are
1264
+ * also written (global.set) are left untouched — the promotion would be unsound if
1265
+ * the global changes between reads.
1266
+ *
1267
+ * @param {Array} fn - Function IR (WAT-as-array)
1268
+ * @param {Map<string,string>} [globalTypes] - Optional: global name → wasm type ('i32'|'f64'|'i64'|'funcref')
1269
+ */
1270
+ export function promoteGlobals(fn, globalTypes) {
1271
+ if (!Array.isArray(fn) || fn[0] !== 'func') return
1272
+ const bodyStart = findBodyStart(fn)
1273
+ if (bodyStart < 0) return
1274
+
1275
+ // Collect global.get counts and detect any global.set
1276
+ const getCounts = new Map() // globalName → count
1277
+ const written = new Set()
1278
+
1279
+ const scan = (node) => {
1280
+ if (!Array.isArray(node)) return
1281
+ const op = node[0]
1282
+ if (op === 'global.get' && typeof node[1] === 'string') {
1283
+ getCounts.set(node[1], (getCounts.get(node[1]) || 0) + 1)
1284
+ return // don't recurse into the name string
1285
+ }
1286
+ if (op === 'global.set') {
1287
+ if (typeof node[1] === 'string') written.add(node[1])
1288
+ if (node[2]) scan(node[2])
1289
+ return
1290
+ }
1291
+ for (let i = 1; i < node.length; i++) scan(node[i])
1292
+ }
1293
+
1294
+ for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
1295
+
1296
+ // Build replacement map: globalName → { localName, type } for globals read ≥ 3 times, not written.
1297
+ // Threshold 3 avoids size regressions in tiny functions where local setup cost dominates.
1298
+ // Find the highest existing $_pg index to avoid duplicate local names on re-runs.
1299
+ let localIdx = 0
1300
+ for (let i = 2; i < bodyStart; i++) {
1125
1301
  const c = fn[i]
1126
- if (!Array.isArray(c)) continue
1127
- if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' || c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
1128
- return i
1302
+ if (Array.isArray(c) && c[0] === 'local' && typeof c[1] === 'string') {
1303
+ const m = c[1].match(/^\$_pg(\d+)$/)
1304
+ if (m) localIdx = Math.max(localIdx, parseInt(m[1], 10) + 1)
1305
+ }
1306
+ }
1307
+ const replacements = new Map()
1308
+ for (const [gName, count] of getCounts) {
1309
+ if (count < 3 || written.has(gName)) continue
1310
+ // Determine type: use provided map, or infer from context
1311
+ const type = globalTypes?.get(gName) || inferTypeFromContext(fn, gName, bodyStart)
1312
+ if (!type) continue // can't determine type, skip
1313
+ const lName = `$_pg${localIdx++}`
1314
+ replacements.set(gName, { lName, type })
1315
+ }
1316
+ if (!replacements.size) return
1317
+
1318
+ // Inject local declarations for promoted globals
1319
+ for (const [, { lName, type }] of replacements) {
1320
+ fn.splice(bodyStart, 0, ['local', lName, type])
1321
+ }
1322
+ // After all splices, bodyStart has shifted
1323
+ const newBodyStart = bodyStart + replacements.size
1324
+
1325
+ // Insert local.set at the very start of the body (after the new locals)
1326
+ let insertIdx = newBodyStart
1327
+ for (const [gName, { lName }] of replacements) {
1328
+ fn.splice(insertIdx, 0, ['local.set', lName, ['global.get', gName]])
1329
+ insertIdx++
1330
+ }
1331
+
1332
+ // Replace all global.get with local.get (only for promoted globals)
1333
+ const replace = (node) => {
1334
+ if (!Array.isArray(node)) return
1335
+ const op = node[0]
1336
+ if (op === 'global.get' && typeof node[1] === 'string') {
1337
+ const info = replacements.get(node[1])
1338
+ if (info) { node[0] = 'local.get'; node[1] = info.lName }
1339
+ return
1340
+ }
1341
+ for (let i = 1; i < node.length; i++) replace(node[i])
1342
+ }
1343
+ for (let i = insertIdx; i < fn.length; i++) replace(fn[i])
1344
+ }
1345
+
1346
+ /**
1347
+ * Infer a global's type from its first usage context within a function body.
1348
+ * Looks at how the global.get result is consumed:
1349
+ * - wrapped in i32.wrap_i64 → global is i64 (but jz doesn't use i64 globals)
1350
+ * - used as arg to i32 ops (i32.add, i32.store, etc.) → i32
1351
+ * - stored to i32-typed local → i32
1352
+ * - otherwise → f64 (default for NaN-boxing scheme)
1353
+ */
1354
+ function inferTypeFromContext(fn, gName, bodyStart) {
1355
+ let inferred = null
1356
+ const check = (node, parent, idx) => {
1357
+ if (!Array.isArray(node) || inferred) return
1358
+ if (node[0] === 'global.get' && node[1] === gName) {
1359
+ // Check parent context
1360
+ if (Array.isArray(parent)) {
1361
+ const pOp = parent[0]
1362
+ // If parent is an i32 op that takes this as operand, likely i32
1363
+ if (typeof pOp === 'string') {
1364
+ if (pOp.startsWith('i32.') && pOp !== 'i32.wrap_i64' && pOp !== 'i32.trunc_f64') {
1365
+ inferred = 'i32'
1366
+ return
1367
+ }
1368
+ if (pOp === 'i32.store' && idx === 2) { inferred = 'i32'; return } // addr
1369
+ if (pOp === 'f64.store' && idx === 2) { inferred = 'f64'; return } // addr can be i32, but value is f64
1370
+ if (pOp === 'i32.eq' || pOp === 'i32.ne' || pOp === 'i32.lt_s' || pOp === 'i32.lt_u' ||
1371
+ pOp === 'i32.gt_s' || pOp === 'i32.gt_u' || pOp === 'i32.le_s' || pOp === 'i32.le_u' ||
1372
+ pOp === 'i32.ge_s' || pOp === 'i32.ge_u') {
1373
+ // Comparison — could be i32, but in jz NaN-boxing scheme most globals are f64
1374
+ // Only if we can confirm from local.set context
1375
+ }
1376
+ if (pOp === 'local.set' && idx === 0) {
1377
+ // Can't determine local type from here easily
1378
+ }
1379
+ }
1380
+ }
1381
+ // Default: f64 (the NaN-boxing carrier)
1382
+ if (!inferred) inferred = 'f64'
1383
+ return
1384
+ }
1385
+ for (let i = 0; i < node.length; i++) {
1386
+ if (Array.isArray(node[i])) check(node[i], node, i)
1387
+ if (inferred) return
1388
+ }
1129
1389
  }
1130
- return fn.length
1390
+ for (let i = bodyStart; i < fn.length && !inferred; i++) check(fn[i], null, i)
1391
+ return inferred
1131
1392
  }
1132
1393
 
1133
1394
  /**
@@ -1470,12 +1731,20 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
1470
1731
  // Sort by freq descending; tie-break by length ascending (pack short hot strings into low-offset range).
1471
1732
  entries.sort((a, b) => (freq.get(b.oldOff) || 0) - (freq.get(a.oldOff) || 0) || a.len - b.len)
1472
1733
 
1473
- // Rebuild pool; map old → new offsets.
1734
+ // Rebuild pool; map old → new offsets. Deduplicate identical strings — keep the
1735
+ // first (hottest) occurrence as canonical and point duplicates to it.
1474
1736
  const remap = new Map()
1737
+ const canon = new Map() // str content → new offset
1475
1738
  let newPool = ''
1476
1739
  for (const e of entries) {
1740
+ const existing = canon.get(e.str)
1741
+ if (existing !== undefined) {
1742
+ remap.set(e.oldOff, existing)
1743
+ continue
1744
+ }
1477
1745
  newPool += String.fromCharCode(e.len & 0xFF, (e.len >> 8) & 0xFF, (e.len >> 16) & 0xFF, (e.len >> 24) & 0xFF)
1478
1746
  remap.set(e.oldOff, newPool.length)
1747
+ canon.set(e.str, newPool.length)
1479
1748
  newPool += e.str
1480
1749
  }
1481
1750
  strPoolRef.pool = newPool
@@ -1504,7 +1773,7 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
1504
1773
  * @param fn func IR node
1505
1774
  * @param cfg optional resolved config from resolveOptimize() — when omitted, all on.
1506
1775
  */
1507
- export function optimizeFunc(fn, cfg) {
1776
+ export function optimizeFunc(fn, cfg, globalTypes) {
1508
1777
  if (cfg && cfg.hoistPtrType === false &&
1509
1778
  cfg.hoistInvariantPtrOffset === false &&
1510
1779
  cfg.hoistInvariantPtrOffsetLoop === false &&
@@ -1513,6 +1782,8 @@ export function optimizeFunc(fn, cfg) {
1513
1782
  cfg.hoistInvariantCellLoads === false &&
1514
1783
  cfg.cseScalarLoad === false &&
1515
1784
  cfg.csePureExpr === false &&
1785
+ cfg.deadStoreElim === false &&
1786
+ cfg.promoteGlobals === false &&
1516
1787
  cfg.sortLocalsByUse === false &&
1517
1788
  cfg.vectorizeLaneLocal === false) return
1518
1789
  if (!cfg || cfg.hoistPtrType !== false) hoistPtrType(fn)
@@ -1524,11 +1795,14 @@ export function optimizeFunc(fn, cfg) {
1524
1795
  if (!cfg || cfg.hoistInvariantCellLoads !== false) hoistInvariantCellLoads(fn)
1525
1796
  if (!cfg || cfg.cseScalarLoad !== false) cseScalarLoad(fn)
1526
1797
  if (!cfg || cfg.csePureExpr !== false) csePureExpr(fn)
1527
- // Vectorizer runs only in the POST-watr phase (`__phase === 'post'`), where
1528
- // narrow.js + watr have produced the canonical lane-local shape. Running it
1529
- // pre-watr matches a noisier IR and would re-fire post-watr on the residual
1530
- // tail producing nested SIMD blocks. Single-pass via phase gating.
1531
- if (cfg && cfg.vectorizeLaneLocal === true && cfg.__phase === 'post') vectorizeLaneLocal(fn)
1798
+ if (!cfg || cfg.deadStoreElim !== false) deadStoreElim(fn)
1799
+ if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes)
1800
+ // Vectorizer runs in POST-watr when watr is enabled, or in PRE when watr is
1801
+ // disabled (there is no post pass then). This keeps single-pass semantics.
1802
+ if (cfg && cfg.vectorizeLaneLocal === true) {
1803
+ const runVectorizer = cfg.__phase === 'post' || (cfg.__phase !== 'post' && cfg.watr === false)
1804
+ if (runVectorizer) vectorizeLaneLocal(fn)
1805
+ }
1532
1806
  if (!cfg || cfg.sortLocalsByUse !== false) sortLocalsByUse(fn, cfg && cfg.fusedRewrite !== false ? counts : null)
1533
1807
  }
1534
1808