jz 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/src/wat/optimize.js
CHANGED
|
@@ -1204,6 +1204,38 @@ const isPure = (node) => {
|
|
|
1204
1204
|
return true
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
// Structured / control-flow forms: they do NOT evaluate all children eagerly
|
|
1208
|
+
// (an `if` runs one arm; a `block`/`loop` scopes branches), so their side effects
|
|
1209
|
+
// can't be flattened to the children's — they stay whole under a drop. (`br*`,
|
|
1210
|
+
// `try_table` are already in IMPURE_OPS.)
|
|
1211
|
+
const STRUCTURED_OPS = new Set(['if', 'then', 'else', 'block', 'loop', 'try'])
|
|
1212
|
+
|
|
1213
|
+
// `op` is an EAGER value operation: it evaluates every operand unconditionally
|
|
1214
|
+
// and only computes a result (arithmetic, compare, convert, select, load) — so
|
|
1215
|
+
// discarding its value leaves just the operands' side effects. Excludes impure
|
|
1216
|
+
// ops and the structured forms above.
|
|
1217
|
+
const isEagerValueOp = (op) => typeof op === 'string' && !IMPURE_OPS.has(op) &&
|
|
1218
|
+
!STRUCTURED_OPS.has(op) && !IMPURE_SUBSTRINGS.some(s => op.includes(s))
|
|
1219
|
+
|
|
1220
|
+
// Statements that preserve `node`'s side effects when its VALUE is discarded.
|
|
1221
|
+
// A fully-pure value contributes nothing; an eager value op contributes only its
|
|
1222
|
+
// operands' effects (the op result is dead); a `local.tee` keeps the store as a
|
|
1223
|
+
// `local.set`; anything else (call, store-expr, structured form) stays under a drop.
|
|
1224
|
+
// This turns `drop(i32.sub(tee X V, 1))` — the post-increment's dropped old value
|
|
1225
|
+
// — into the bare `local.set X V`, eliminating dead arithmetic the plain
|
|
1226
|
+
// `drop(PURE)→nop` rule can't (the tee makes the whole subtree impure).
|
|
1227
|
+
const dropEffects = (node) => {
|
|
1228
|
+
if (!Array.isArray(node) || isPure(node)) return []
|
|
1229
|
+
const op = node[0]
|
|
1230
|
+
if (op === 'local.tee' && node.length === 3) return [['local.set', node[1], node[2]]]
|
|
1231
|
+
if (isEagerValueOp(op)) {
|
|
1232
|
+
const eff = []
|
|
1233
|
+
for (let i = 1; i < node.length; i++) eff.push(...dropEffects(node[i]))
|
|
1234
|
+
return eff
|
|
1235
|
+
}
|
|
1236
|
+
return [['drop', node]]
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1207
1239
|
/** Count all local.get/set/tee occurrences in one walk */
|
|
1208
1240
|
const countLocalUses = (node) => {
|
|
1209
1241
|
const counts = new Map()
|
|
@@ -1259,6 +1291,19 @@ const purgeRefs = (known, name) => {
|
|
|
1259
1291
|
}
|
|
1260
1292
|
}
|
|
1261
1293
|
|
|
1294
|
+
/** Drop tracked values that read global `$name`: a `global.set $name` makes them stale.
|
|
1295
|
+
* The local-only {@link purgeRefs} misses this — so a value captured from a global
|
|
1296
|
+
* (`let s = f`, where `f` is a reassignable module-level binding) would survive an
|
|
1297
|
+
* intervening `f = …` and substitute the NEW global. That silently breaks the canonical
|
|
1298
|
+
* pointer swap `let s = f; f = g; g = s` (g would read post-swap f, i.e. itself). */
|
|
1299
|
+
const purgeGlobalRefs = (known, name) => {
|
|
1300
|
+
for (const [key, tracked] of known) {
|
|
1301
|
+
let refs = false
|
|
1302
|
+
walk(tracked.val, n => { if (Array.isArray(n) && n[0] === 'global.get' && n[1] === name) refs = true })
|
|
1303
|
+
if (refs) known.delete(key)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1262
1307
|
/** True if `node` recursively contains an op that may read linear memory.
|
|
1263
1308
|
* Tracked values whose RHS reads memory go stale after any intervening
|
|
1264
1309
|
* memory-mutating op (`*.store`, `memory.copy/fill/init`, atomic stores/rmw). */
|
|
@@ -1346,6 +1391,9 @@ const substGets = (node, known) => {
|
|
|
1346
1391
|
if (inner === known) inner = new Map(known)
|
|
1347
1392
|
inner.delete(n[1])
|
|
1348
1393
|
purgeRefs(inner, n[1])
|
|
1394
|
+
} else if (n[0] === 'global.set' && typeof n[1] === 'string') { // same staleness as a local write — a sibling operand's
|
|
1395
|
+
if (inner === known) inner = new Map(known) // global write invalidates a later operand's global-sourced copy
|
|
1396
|
+
purgeGlobalRefs(inner, n[1])
|
|
1349
1397
|
}
|
|
1350
1398
|
})
|
|
1351
1399
|
}
|
|
@@ -1388,6 +1436,7 @@ const forwardPropagate = (funcNode, params, useCounts) => {
|
|
|
1388
1436
|
if (!Array.isArray(n)) return
|
|
1389
1437
|
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
|
|
1390
1438
|
{ known.delete(n[1]); purgeRefs(known, n[1]) }
|
|
1439
|
+
else if (n[0] === 'global.set' && typeof n[1] === 'string') purgeGlobalRefs(known, n[1])
|
|
1391
1440
|
})
|
|
1392
1441
|
const uses = getUseCount(instr[1])
|
|
1393
1442
|
purgeRefs(known, instr[1]) // entries that read this local just went stale
|
|
@@ -1434,8 +1483,10 @@ const forwardPropagate = (funcNode, params, useCounts) => {
|
|
|
1434
1483
|
// pre-write tracked value (correct), but later reads must see the new
|
|
1435
1484
|
// (untracked) value, not the stale constant.
|
|
1436
1485
|
walk(instr, n => {
|
|
1437
|
-
if (Array.isArray(n)
|
|
1486
|
+
if (!Array.isArray(n)) return
|
|
1487
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
|
|
1438
1488
|
{ known.delete(n[1]); purgeRefs(known, n[1]) }
|
|
1489
|
+
else if (n[0] === 'global.set' && typeof n[1] === 'string') purgeGlobalRefs(known, n[1])
|
|
1439
1490
|
})
|
|
1440
1491
|
// Memory write in this statement (any nested store / memory.copy / etc.)
|
|
1441
1492
|
// invalidates every tracked value whose RHS reads memory: inlining one
|
|
@@ -1632,113 +1683,269 @@ const propagate = (ast) => {
|
|
|
1632
1683
|
|
|
1633
1684
|
// ==================== FUNCTION INLINING ====================
|
|
1634
1685
|
|
|
1686
|
+
// Shared inliner primitives, used by BOTH passes below: `inline` (duplicates tiny
|
|
1687
|
+
// multi-caller bodies — size-for-speed) and `inlineOnce` (splices single-caller
|
|
1688
|
+
// bodies, never duplicates). The lift technique is identical — rename the callee's
|
|
1689
|
+
// params/locals/labels to fresh `$__inlN_*` names, evaluate args once into the
|
|
1690
|
+
// renamed param locals, turn `return X` into `br $__inlN X`, wrap the body in a
|
|
1691
|
+
// `(block $__inlN (result T)? …)`. Only the SELECTION policy differs (one caller vs
|
|
1692
|
+
// every caller of a small body), so the lift lives here once.
|
|
1693
|
+
|
|
1694
|
+
let inlineUid = 0
|
|
1695
|
+
const INL_HEAD = new Set(['export', 'type', 'param', 'result', 'local'])
|
|
1696
|
+
const inlBodyStart = (fn) => {
|
|
1697
|
+
let i = 2
|
|
1698
|
+
while (i < fn.length && (typeof fn[i] === 'string' || (Array.isArray(fn[i]) && INL_HEAD.has(fn[i][0])))) i++
|
|
1699
|
+
return i
|
|
1700
|
+
}
|
|
1701
|
+
const inlIsBranch = op => op === 'br' || op === 'br_if' || op === 'br_table'
|
|
1702
|
+
// A subtree we can't lift into a (block …): depth-relative branch labels (which would
|
|
1703
|
+
// shift under the added nesting) or tail calls (which would escape the wrapping block).
|
|
1704
|
+
const inlUnsafe = (n) => {
|
|
1705
|
+
if (!Array.isArray(n)) return false
|
|
1706
|
+
const op = n[0]
|
|
1707
|
+
if (op === 'return_call' || op === 'return_call_indirect' || op === 'return_call_ref') return true
|
|
1708
|
+
if (op === 'try' || op === 'try_table' || op === 'delegate' || op === 'rethrow') return true // exception labels — not handled by the relabeler below
|
|
1709
|
+
if (inlIsBranch(op)) for (let i = 1; i < n.length; i++) if (typeof n[i] === 'number' || (typeof n[i] === 'string' && /^\d+$/.test(n[i]))) return true
|
|
1710
|
+
for (let i = 1; i < n.length; i++) if (inlUnsafe(n[i])) return true
|
|
1711
|
+
return false
|
|
1712
|
+
}
|
|
1713
|
+
const inlCallsSelf = (n, name) => {
|
|
1714
|
+
if (!Array.isArray(n)) return false
|
|
1715
|
+
if ((n[0] === 'call' || n[0] === 'return_call') && n[1] === name) return true
|
|
1716
|
+
for (let i = 1; i < n.length; i++) if (inlCallsSelf(n[i], name)) return true
|
|
1717
|
+
return false
|
|
1718
|
+
}
|
|
1719
|
+
// Per-call zero-init constant for a callee local re-entered from a caller loop.
|
|
1720
|
+
// null ⇒ a type we can't safely zero here (skip inlining such a callee).
|
|
1721
|
+
const inlZeroFor = (t) => {
|
|
1722
|
+
if (t === 'i32') return ['i32.const', 0]
|
|
1723
|
+
if (t === 'i64') return ['i64.const', 0]
|
|
1724
|
+
if (t === 'f32') return ['f32.const', 0]
|
|
1725
|
+
if (t === 'f64') return ['f64.const', 0]
|
|
1726
|
+
if (t === 'v128') return ['v128.const', 'i64x2', '0', '0'] // STRING lanes — watr's v128 encoder calls .replaceAll
|
|
1727
|
+
return null
|
|
1728
|
+
}
|
|
1729
|
+
// A callee local needs a per-entry reset only if some path reads it before any
|
|
1730
|
+
// unconditional write (so it relied on the callee's fresh zero-init). Mirrors
|
|
1731
|
+
// coalesceLocals' readsZero heuristic; unconditionally-written scratch needs none.
|
|
1732
|
+
const inlNeedsReset = (body, name) => {
|
|
1733
|
+
let seen = false, conditional = false, depth = 0
|
|
1734
|
+
const visit = (n) => {
|
|
1735
|
+
if (seen || !Array.isArray(n)) return
|
|
1736
|
+
const op = n[0]
|
|
1737
|
+
const isSet = op === 'local.set' || op === 'local.tee'
|
|
1738
|
+
if ((isSet || op === 'local.get') && n[1] === name) {
|
|
1739
|
+
if (isSet) for (let i = 2; i < n.length && !seen; i++) visit(n[i])
|
|
1740
|
+
if (seen) return
|
|
1741
|
+
seen = true
|
|
1742
|
+
if (op === 'local.get' || depth > 0) conditional = true
|
|
1743
|
+
return
|
|
1744
|
+
}
|
|
1745
|
+
const isIf = op === 'if'
|
|
1746
|
+
for (let i = 1; i < n.length && !seen; i++) {
|
|
1747
|
+
const c = n[i]
|
|
1748
|
+
const cond = isIf && Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')
|
|
1749
|
+
if (cond) depth++
|
|
1750
|
+
visit(c)
|
|
1751
|
+
if (cond) depth--
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
for (const n of body) { if (seen) break; visit(n) }
|
|
1755
|
+
if (!seen) return false
|
|
1756
|
+
return conditional
|
|
1757
|
+
}
|
|
1758
|
+
// Module-level references that pin a function (can't be inlined-away/removed).
|
|
1759
|
+
const inlCollectPinned = (n, pinned) => {
|
|
1760
|
+
if (!Array.isArray(n)) return
|
|
1761
|
+
const op = n[0]
|
|
1762
|
+
if (op === 'export' && Array.isArray(n[2]) && n[2][0] === 'func' && typeof n[2][1] === 'string') pinned.add(n[2][1])
|
|
1763
|
+
else if (op === 'start' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1764
|
+
else if (op === 'ref.func' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1765
|
+
else if (op === 'elem') for (const c of n) if (typeof c === 'string' && c[0] === '$') pinned.add(c)
|
|
1766
|
+
for (const c of n) inlCollectPinned(c, pinned)
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Parse a func node into { params, locals, inlResult } once, enforcing the
|
|
1770
|
+
// liftability contract (named params/locals, zero-init-able local types, ≤1
|
|
1771
|
+
// result, no inline export). Returns null if the func can't be lifted.
|
|
1772
|
+
const inlParse = (fn) => {
|
|
1773
|
+
const params = [], locals = []
|
|
1774
|
+
let inlResult = null, ok = true, nResult = 0
|
|
1775
|
+
for (let i = 2; i < fn.length; i++) {
|
|
1776
|
+
const c = fn[i]
|
|
1777
|
+
if (typeof c === 'string') continue
|
|
1778
|
+
if (!Array.isArray(c)) { ok = false; break }
|
|
1779
|
+
if (c[0] === 'param') { if (typeof c[1] !== 'string' || c[1][0] !== '$') { ok = false; break } params.push({ name: c[1], type: c[2] }) }
|
|
1780
|
+
else if (c[0] === 'local') { if (typeof c[1] !== 'string' || c[1][0] !== '$' || !inlZeroFor(c[2])) { ok = false; break } locals.push({ name: c[1], type: c[2] }) }
|
|
1781
|
+
else if (c[0] === 'result') { nResult += c.length - 1; if (c.length > 1) inlResult = c[1] }
|
|
1782
|
+
else if (c[0] === 'export') { ok = false; break }
|
|
1783
|
+
else if (c[0] === 'type') continue
|
|
1784
|
+
else break
|
|
1785
|
+
}
|
|
1786
|
+
if (nResult > 1) ok = false
|
|
1787
|
+
return ok ? { params, locals, inlResult } : null
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// IR-node count of a callee body — the cheap size proxy gating multi-caller inline.
|
|
1791
|
+
const inlBodySize = (fn) => {
|
|
1792
|
+
let n = 0
|
|
1793
|
+
const count = (x) => { if (!Array.isArray(x)) return; n++; for (let i = 1; i < x.length; i++) count(x[i]) }
|
|
1794
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) count(fn[i])
|
|
1795
|
+
return n
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1635
1798
|
/**
|
|
1636
|
-
*
|
|
1799
|
+
* Lift one callee into ONE `(call …)` node. Returns `{ block, decls }` — `block`
|
|
1800
|
+
* replaces the call; `decls` are the renamed param+local declarations to splice into
|
|
1801
|
+
* the caller's local list. A fresh uid per invocation keeps every inlined copy's
|
|
1802
|
+
* locals/labels unique, so the same body can be lifted into many sites.
|
|
1803
|
+
*
|
|
1804
|
+
* (call $f a0 a1 …) → (block $__inlN (result T)?
|
|
1805
|
+
* (local.set $__inlN_p0 a0) … ;; args evaluated once, in order
|
|
1806
|
+
* (local.set $__inlN_l reset) … ;; only locals that rely on zero-init
|
|
1807
|
+
* …body, renamed, `return X` → `br $__inlN X`…)
|
|
1808
|
+
*/
|
|
1809
|
+
const buildInline = (params, locals, inlResult, cBody, args) => {
|
|
1810
|
+
const uid = ++inlineUid
|
|
1811
|
+
const exit = `$__inl${uid}`
|
|
1812
|
+
const rename = new Map()
|
|
1813
|
+
for (const p of params) rename.set(p.name, `$__inl${uid}_${p.name.slice(1)}`)
|
|
1814
|
+
for (const l of locals) rename.set(l.name, `$__inl${uid}_${l.name.slice(1)}`)
|
|
1815
|
+
// The callee's own block/loop/if labels would shadow same-named caller labels (and
|
|
1816
|
+
// break depth resolution) under the added nesting — give them fresh names too.
|
|
1817
|
+
const labelRename = new Map()
|
|
1818
|
+
const collectLabels = (n) => {
|
|
1819
|
+
if (!Array.isArray(n)) return
|
|
1820
|
+
if (isBranchScope(n[0]) && typeof n[1] === 'string' && n[1][0] === '$' && !labelRename.has(n[1]))
|
|
1821
|
+
labelRename.set(n[1], `$__inl${uid}L_${n[1].slice(1)}`)
|
|
1822
|
+
for (let i = 1; i < n.length; i++) collectLabels(n[i])
|
|
1823
|
+
}
|
|
1824
|
+
for (const n of cBody) collectLabels(n)
|
|
1825
|
+
const sub = (n) => {
|
|
1826
|
+
if (!Array.isArray(n)) return n
|
|
1827
|
+
const op = n[0]
|
|
1828
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string' && rename.has(n[1]))
|
|
1829
|
+
return [op, rename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1830
|
+
if (op === 'return') return ['br', exit, ...n.slice(1).map(sub)]
|
|
1831
|
+
if (isBranchScope(op) && typeof n[1] === 'string' && labelRename.has(n[1]))
|
|
1832
|
+
return [op, labelRename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1833
|
+
if (inlIsBranch(op)) return [op, ...n.slice(1).map(c => (typeof c === 'string' && labelRename.has(c)) ? labelRename.get(c) : sub(c))]
|
|
1834
|
+
return n.map((c, i) => i === 0 ? c : sub(c))
|
|
1835
|
+
}
|
|
1836
|
+
const setup = params.map((p, k) => ['local.set', rename.get(p.name), args[k]])
|
|
1837
|
+
const resets = locals.filter(l => inlNeedsReset(cBody, l.name)).map(l => ['local.set', rename.get(l.name), inlZeroFor(l.type)])
|
|
1838
|
+
const inner = cBody.map(sub)
|
|
1839
|
+
const block = inlResult
|
|
1840
|
+
? ['block', exit, ['result', inlResult], ...setup, ...resets, ...inner]
|
|
1841
|
+
: ['block', exit, ...setup, ...resets, ...inner]
|
|
1842
|
+
const decls = [...params, ...locals].map(p => ['local', rename.get(p.name), p.type])
|
|
1843
|
+
return { block, decls }
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Inline SMALL functions into every caller, then delete them — the multi-caller
|
|
1848
|
+
* complement to {@link inlineOnce}. inlineOnce only fires for a lone caller (so it
|
|
1849
|
+
* never duplicates); this duplicates a tiny body across ALL its sites, trading a
|
|
1850
|
+
* bounded amount of size to remove call overhead from hot inner loops (e.g. a
|
|
1851
|
+
* raymarcher's per-step SDF, evaluated 4-wide but still paying a wasm call each
|
|
1852
|
+
* march step). Size-for-speed — opt-in, on at the 'speed' level only.
|
|
1853
|
+
*
|
|
1854
|
+
* A callee qualifies when it is small (≤ INLINE_MAX_NODES IR nodes), named with
|
|
1855
|
+
* named params/locals, single-result-or-void, non-recursive, not pinned
|
|
1856
|
+
* (export/start/elem/ref.func), not a SIMD_PROTECTED transcendental, and free of
|
|
1857
|
+
* depth-relative branches / tail calls (the inlParse + inlUnsafe liftability
|
|
1858
|
+
* contract). Runs to a fixpoint so small-helper chains (sdf → sdRep) collapse.
|
|
1859
|
+
*
|
|
1860
|
+
* `simdOnly` (the speed-level default) restricts inlining to pure SIMD helpers —
|
|
1861
|
+
* every param and the result are `v128`. That targets the case this exists for —
|
|
1862
|
+
* a hand-vectorized hot loop's per-step helper (a raymarcher's SDF), where the call
|
|
1863
|
+
* overhead is paid every iteration and V8's wasm JIT won't inline it — while leaving
|
|
1864
|
+
* SCALAR helpers untouched: those are where jz's codegen-shape/size tuning and the
|
|
1865
|
+
* auto-vectorizer's call-lifting (plasma's fbm → sin2) live, and duplicating them
|
|
1866
|
+
* both bloats and perturbs that machinery for no gain (V8's JIT inlines scalar
|
|
1867
|
+
* helpers itself). The unrestricted form stays available as `watr: { inline: true }`.
|
|
1868
|
+
*
|
|
1637
1869
|
* @param {Array} ast
|
|
1870
|
+
* @param {{simdOnly?: boolean}} [opts]
|
|
1638
1871
|
* @returns {Array}
|
|
1639
1872
|
*/
|
|
1640
|
-
const
|
|
1873
|
+
const INLINE_MAX_NODES = 90
|
|
1874
|
+
const isV128SimdHelper = (params, inlResult) =>
|
|
1875
|
+
inlResult === 'v128' && params.length > 0 && params.every(p => p.type === 'v128')
|
|
1876
|
+
const inline = (ast, { simdOnly = false } = {}) => {
|
|
1641
1877
|
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1642
1878
|
|
|
1643
|
-
//
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1650
|
-
if (!name) continue
|
|
1651
|
-
|
|
1652
|
-
// Check if function is small enough to inline
|
|
1653
|
-
let params = []
|
|
1654
|
-
let body = []
|
|
1655
|
-
let hasLocals = false
|
|
1656
|
-
let hasExport = false
|
|
1879
|
+
const skip = new Set() // callees with a non-inlinable site (arity mismatch) — don't re-pick
|
|
1880
|
+
for (let round = 0; round < MAX_INLINE_ROUNDS; round++) {
|
|
1881
|
+
const funcs = ast.filter(n => Array.isArray(n) && n[0] === 'func')
|
|
1882
|
+
const funcByName = new Map()
|
|
1883
|
+
for (const n of funcs) if (typeof n[1] === 'string') funcByName.set(n[1], n)
|
|
1657
1884
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
if (!Array.isArray(
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
} else {
|
|
1666
|
-
// Unnamed params - harder to inline
|
|
1667
|
-
params = null
|
|
1668
|
-
break
|
|
1669
|
-
}
|
|
1670
|
-
} else if (sub[0] === 'local') {
|
|
1671
|
-
hasLocals = true
|
|
1672
|
-
} else if (sub[0] === 'export') {
|
|
1673
|
-
hasExport = true
|
|
1674
|
-
} else if (sub[0] !== 'result' && sub[0] !== 'type') {
|
|
1675
|
-
body.push(sub)
|
|
1676
|
-
}
|
|
1885
|
+
const callRefs = new Map(), otherRef = new Set()
|
|
1886
|
+
const countRefs = (n) => {
|
|
1887
|
+
if (!Array.isArray(n)) return
|
|
1888
|
+
const op = n[0]
|
|
1889
|
+
if (op === 'call' && typeof n[1] === 'string') callRefs.set(n[1], (callRefs.get(n[1]) || 0) + 1)
|
|
1890
|
+
else if (op === 'return_call' && typeof n[1] === 'string') otherRef.add(n[1])
|
|
1891
|
+
for (let i = 1; i < n.length; i++) countRefs(n[i])
|
|
1677
1892
|
}
|
|
1893
|
+
countRefs(ast)
|
|
1894
|
+
const pinned = new Set()
|
|
1895
|
+
for (const n of ast) if (!Array.isArray(n) || n[0] !== 'func') inlCollectPinned(n, pinned)
|
|
1678
1896
|
|
|
1679
|
-
//
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
let
|
|
1690
|
-
let
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
if ((n[0] === 'local.set' || n[0] === 'local.tee') && paramNames.has(n[1])) {
|
|
1694
|
-
mutatesParam = true
|
|
1695
|
-
}
|
|
1696
|
-
if (n[0] === 'return' || n[0] === 'return_call' || n[0] === 'return_call_indirect') {
|
|
1697
|
-
hasReturn = true
|
|
1698
|
-
}
|
|
1699
|
-
})
|
|
1700
|
-
if (!mutatesParam && !hasReturn) {
|
|
1701
|
-
inlinable.set(name, { body: body[0], params })
|
|
1702
|
-
}
|
|
1897
|
+
// Pick a small, liftable, non-recursive callee with ≥1 plain-call site.
|
|
1898
|
+
let calleeName = null, parsed = null
|
|
1899
|
+
for (const [name, fn] of funcByName) {
|
|
1900
|
+
if (skip.has(name) || pinned.has(name) || otherRef.has(name) || SIMD_PROTECTED.has(name)) continue
|
|
1901
|
+
if (!(callRefs.get(name) >= 1)) continue
|
|
1902
|
+
if (inlBodySize(fn) > INLINE_MAX_NODES) continue
|
|
1903
|
+
if (inlCallsSelf(fn, name)) continue
|
|
1904
|
+
const p = inlParse(fn)
|
|
1905
|
+
if (!p) continue
|
|
1906
|
+
if (simdOnly && !isV128SimdHelper(p.params, p.inlResult)) continue
|
|
1907
|
+
let bad = false
|
|
1908
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) if (inlUnsafe(fn[i])) { bad = true; break }
|
|
1909
|
+
if (bad) continue
|
|
1910
|
+
calleeName = name; parsed = p; break
|
|
1703
1911
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
// Replace calls with inlined body
|
|
1707
|
-
if (inlinable.size === 0) return ast
|
|
1708
|
-
|
|
1709
|
-
walkPost(ast, (node) => {
|
|
1710
|
-
if (!Array.isArray(node) || node[0] !== 'call') return
|
|
1711
|
-
const fname = node[1]
|
|
1712
|
-
if (!inlinable.has(fname)) return
|
|
1713
|
-
|
|
1714
|
-
const { body, params } = inlinable.get(fname)
|
|
1715
|
-
const args = node.slice(2)
|
|
1912
|
+
if (!calleeName) break
|
|
1716
1913
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1914
|
+
const callee = funcByName.get(calleeName)
|
|
1915
|
+
const { params, locals, inlResult } = parsed
|
|
1916
|
+
const cBody = callee.slice(inlBodyStart(callee))
|
|
1917
|
+
const expected = callRefs.get(calleeName) || 0 // callee is non-recursive ⇒ all sites are in other funcs
|
|
1918
|
+
let replaced = 0
|
|
1721
1919
|
|
|
1722
|
-
//
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1920
|
+
// Splice into EVERY caller. A body that itself still calls an as-yet-uninlined
|
|
1921
|
+
// helper is fine — later rounds collapse it (or it stays a call).
|
|
1922
|
+
for (const fn of funcs) {
|
|
1923
|
+
if (fn === callee) continue
|
|
1924
|
+
const addDecls = []
|
|
1925
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) {
|
|
1926
|
+
fn[i] = walkPost(fn[i], (n) => {
|
|
1927
|
+
if (!Array.isArray(n) || n[0] !== 'call' || n[1] !== calleeName) return
|
|
1928
|
+
const args = n.slice(2)
|
|
1929
|
+
if (args.length !== params.length) return // arity mismatch — leave the call
|
|
1930
|
+
const { block, decls } = buildInline(params, locals, inlResult, cBody, args)
|
|
1931
|
+
addDecls.push(...decls)
|
|
1932
|
+
replaced++
|
|
1933
|
+
return block
|
|
1934
|
+
})
|
|
1729
1935
|
}
|
|
1730
|
-
|
|
1936
|
+
if (addDecls.length) fn.splice(inlBodyStart(fn), 0, ...addDecls)
|
|
1937
|
+
}
|
|
1731
1938
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1939
|
+
// Drop the callee only if every site inlined; else keep it and stop re-picking it.
|
|
1940
|
+
if (replaced === expected) { const idx = ast.indexOf(callee); if (idx >= 0) ast.splice(idx, 1) }
|
|
1941
|
+
else skip.add(calleeName)
|
|
1942
|
+
}
|
|
1734
1943
|
|
|
1735
1944
|
return ast
|
|
1736
1945
|
}
|
|
1737
1946
|
|
|
1738
1947
|
// ==================== INLINE-ONCE ====================
|
|
1739
1948
|
|
|
1740
|
-
let inlineUid = 0
|
|
1741
|
-
|
|
1742
1949
|
/**
|
|
1743
1950
|
* Devirtualize `call_indirect` through NaN-boxed closure values with a statically
|
|
1744
1951
|
* known candidate set. `let f = c ? a : b; … f(x)` emits a select of two i64
|
|
@@ -2027,99 +2234,18 @@ const devirt = (ast) => {
|
|
|
2027
2234
|
* @param {Array} ast
|
|
2028
2235
|
* @returns {Array}
|
|
2029
2236
|
*/
|
|
2237
|
+
// Scalar transcendental helpers the auto-vectorizer rewrites to f64x2 mirrors (PPC_CALL2 in
|
|
2238
|
+
// src/optimize/vectorize.js). inlineOnce must NOT dissolve their call nodes when single-caller —
|
|
2239
|
+
// the post-phase lift needs the call to rewrite it. Keep in sync with PPC_CALL2's keys.
|
|
2240
|
+
const SIMD_PROTECTED = new Set(['$math.sin_core', '$math.cos_core', '$math.sin', '$math.cos', '$math.pow', '$math.atan2', '$math.hypot', '$math.log'])
|
|
2241
|
+
|
|
2030
2242
|
const inlineOnce = (ast) => {
|
|
2031
2243
|
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
2032
2244
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
return i
|
|
2038
|
-
}
|
|
2039
|
-
const isBranch = op => op === 'br' || op === 'br_if' || op === 'br_table'
|
|
2040
|
-
// A subtree we can't lift into a (block …): depth-relative branch labels (shift
|
|
2041
|
-
// under added nesting) or tail calls (would escape the wrapping block).
|
|
2042
|
-
const unsafe = (n) => {
|
|
2043
|
-
if (!Array.isArray(n)) return false
|
|
2044
|
-
const op = n[0]
|
|
2045
|
-
if (op === 'return_call' || op === 'return_call_indirect' || op === 'return_call_ref') return true
|
|
2046
|
-
if (op === 'try' || op === 'try_table' || op === 'delegate' || op === 'rethrow') return true // exception labels — not handled by the relabeler below
|
|
2047
|
-
if (isBranch(op)) for (let i = 1; i < n.length; i++) if (typeof n[i] === 'number' || (typeof n[i] === 'string' && /^\d+$/.test(n[i]))) return true
|
|
2048
|
-
for (let i = 1; i < n.length; i++) if (unsafe(n[i])) return true
|
|
2049
|
-
return false
|
|
2050
|
-
}
|
|
2051
|
-
const callsSelf = (n, name) => {
|
|
2052
|
-
if (!Array.isArray(n)) return false
|
|
2053
|
-
if ((n[0] === 'call' || n[0] === 'return_call') && n[1] === name) return true
|
|
2054
|
-
for (let i = 1; i < n.length; i++) if (callsSelf(n[i], name)) return true
|
|
2055
|
-
return false
|
|
2056
|
-
}
|
|
2057
|
-
// Locals must be re-zeroed each time the inlined block is entered IF the
|
|
2058
|
-
// callee body actually relies on zero-init — i.e. some path reads the local
|
|
2059
|
-
// before any unconditional write. In the original callee they got fresh
|
|
2060
|
-
// zero-init per call; after inlining they're outer-func locals, zeroed only
|
|
2061
|
-
// at outer entry, so a caller-loop that re-enters the inlined block reads
|
|
2062
|
-
// stale values otherwise. Returns null for any type we can't safely
|
|
2063
|
-
// zero-init here (skip inlining such callees).
|
|
2064
|
-
const zeroFor = (t) => {
|
|
2065
|
-
if (t === 'i32') return ['i32.const', 0]
|
|
2066
|
-
if (t === 'i64') return ['i64.const', 0]
|
|
2067
|
-
if (t === 'f32') return ['f32.const', 0]
|
|
2068
|
-
if (t === 'f64') return ['f64.const', 0]
|
|
2069
|
-
if (t === 'v128') return ['v128.const', 'i64x2', 0, 0]
|
|
2070
|
-
// Nullable ref types (`(ref null …)`, `funcref`, `externref`, `anyref`, etc.)
|
|
2071
|
-
// zero-init to `ref.null …` per call; emitting that here would need the exact
|
|
2072
|
-
// heap-type. Non-nullable refs aren't zero-init at all (codegen must seed
|
|
2073
|
-
// them). Either way, skip — let the call survive.
|
|
2074
|
-
return null
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
// Locals whose first observed use is a read — or whose first write is inside
|
|
2078
|
-
// a conditional branch, where the alternate path bypasses it — depend on
|
|
2079
|
-
// zero-init and need a reset when inlined into a caller-loop. Locals that
|
|
2080
|
-
// are unconditionally written before any read (the common scratch pattern,
|
|
2081
|
-
// e.g. `(local.set $bits (local.get $ptr))` opening a helper) don't, and
|
|
2082
|
-
// emitting a spurious reset would only inflate that local's set-count and
|
|
2083
|
-
// block downstream propagation/coalescing. Mirrors `coalesceLocals`'
|
|
2084
|
-
// `readsZero` heuristic.
|
|
2085
|
-
const needsReset = (body, name) => {
|
|
2086
|
-
let seen = false, conditional = false, depth = 0
|
|
2087
|
-
const visit = (n) => {
|
|
2088
|
-
if (seen || !Array.isArray(n)) return
|
|
2089
|
-
const op = n[0]
|
|
2090
|
-
const isSet = op === 'local.set' || op === 'local.tee'
|
|
2091
|
-
if ((isSet || op === 'local.get') && n[1] === name) {
|
|
2092
|
-
if (isSet) for (let i = 2; i < n.length && !seen; i++) visit(n[i])
|
|
2093
|
-
if (seen) return
|
|
2094
|
-
seen = true
|
|
2095
|
-
if (op === 'local.get' || depth > 0) conditional = true
|
|
2096
|
-
return
|
|
2097
|
-
}
|
|
2098
|
-
const isIf = op === 'if'
|
|
2099
|
-
for (let i = 1; i < n.length && !seen; i++) {
|
|
2100
|
-
const c = n[i]
|
|
2101
|
-
const cond = isIf && Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')
|
|
2102
|
-
if (cond) depth++
|
|
2103
|
-
visit(c)
|
|
2104
|
-
if (cond) depth--
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
for (const n of body) { if (seen) break; visit(n) }
|
|
2108
|
-
// If the local is never used (dead), no reset; the dead decl will be pruned.
|
|
2109
|
-
if (!seen) return false
|
|
2110
|
-
return conditional
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
// Module-level references that pin a function (can't be removed/inlined-away).
|
|
2114
|
-
const collectPinned = (n, pinned) => {
|
|
2115
|
-
if (!Array.isArray(n)) return
|
|
2116
|
-
const op = n[0]
|
|
2117
|
-
if (op === 'export' && Array.isArray(n[2]) && n[2][0] === 'func' && typeof n[2][1] === 'string') pinned.add(n[2][1])
|
|
2118
|
-
else if (op === 'start' && typeof n[1] === 'string') pinned.add(n[1])
|
|
2119
|
-
else if (op === 'ref.func' && typeof n[1] === 'string') pinned.add(n[1])
|
|
2120
|
-
else if (op === 'elem') for (const c of n) if (typeof c === 'string' && c[0] === '$') pinned.add(c)
|
|
2121
|
-
for (const c of n) collectPinned(c, pinned)
|
|
2122
|
-
}
|
|
2245
|
+
// Lift primitives are shared with `inline` (defined once above buildInline). inlineOnce
|
|
2246
|
+
// splices into a SINGLE caller (never duplicating); `inline` duplicates into every caller.
|
|
2247
|
+
const bodyStart = inlBodyStart, callsSelf = inlCallsSelf, unsafe = inlUnsafe, isBranch = inlIsBranch
|
|
2248
|
+
const zeroFor = inlZeroFor, needsReset = inlNeedsReset, collectPinned = inlCollectPinned
|
|
2123
2249
|
|
|
2124
2250
|
for (let round = 0; round < MAX_INLINE_ROUNDS; round++) {
|
|
2125
2251
|
const funcs = ast.filter(n => Array.isArray(n) && n[0] === 'func')
|
|
@@ -2146,6 +2272,11 @@ const inlineOnce = (ast) => {
|
|
|
2146
2272
|
for (const [name, fn] of funcByName) {
|
|
2147
2273
|
if (pinned.has(name) || otherRef.has(name)) continue
|
|
2148
2274
|
if (callRefs.get(name) !== 1) continue
|
|
2275
|
+
// Keep the scalar transcendentals that the auto-vectorizer maps to f64x2 mirrors (PPC_CALL2 in
|
|
2276
|
+
// src/optimize/vectorize.js): inlining a single-caller $math.atan2/hypot/log would dissolve the
|
|
2277
|
+
// call node before the post-phase lift can rewrite it to $math.atan2_2/hypot_2/log_v. Keep in
|
|
2278
|
+
// sync with PPC_CALL2's keys.
|
|
2279
|
+
if (SIMD_PROTECTED.has(name)) continue
|
|
2149
2280
|
if (callsSelf(fn, name)) continue
|
|
2150
2281
|
// named params/locals only (we'll rename them); reject locals with types
|
|
2151
2282
|
// we can't zero-init on block re-entry.
|
|
@@ -2528,9 +2659,14 @@ const vacuum = (ast) => {
|
|
|
2528
2659
|
// Remove nop entirely (return array marker; parent or post-pass cleans it)
|
|
2529
2660
|
if (op === 'nop') return ['nop']
|
|
2530
2661
|
|
|
2531
|
-
// (drop
|
|
2532
|
-
|
|
2533
|
-
|
|
2662
|
+
// (drop V) → just V's side effects. Pure V vanishes (→ nop); a pure op over
|
|
2663
|
+
// a `local.tee` collapses to the bare store (kills the post-increment's dead
|
|
2664
|
+
// old-value arithmetic); impure V is kept under a drop.
|
|
2665
|
+
if (op === 'drop' && node.length === 2) {
|
|
2666
|
+
const eff = dropEffects(node[1])
|
|
2667
|
+
if (eff.length === 0) return ['nop']
|
|
2668
|
+
if (eff.length === 1) return eff[0]
|
|
2669
|
+
return ['block', ...eff]
|
|
2534
2670
|
}
|
|
2535
2671
|
|
|
2536
2672
|
// (select x x cond) → x — only when cond is PURE. An impure cond may set a
|
|
@@ -2558,13 +2694,20 @@ const vacuum = (ast) => {
|
|
|
2558
2694
|
for (let i = 1; i < node.length; i++) {
|
|
2559
2695
|
const child = node[i]
|
|
2560
2696
|
if (child === 'nop' || (Array.isArray(child) && child[0] === 'nop')) continue
|
|
2561
|
-
//
|
|
2697
|
+
// Stack-form `EXPR drop`: a pure EXPR drops out entirely; a bare
|
|
2698
|
+
// `tee X V drop` keeps just the store (`set X V`) — the dropped value
|
|
2699
|
+
// was the only reason it was a tee.
|
|
2562
2700
|
const next = node[i + 1]
|
|
2563
2701
|
const isDrop = next === 'drop' || (Array.isArray(next) && next[0] === 'drop' && next.length === 1)
|
|
2564
|
-
if (Array.isArray(child) && isPure(child)
|
|
2702
|
+
if (Array.isArray(child) && isDrop && isPure(child)) {
|
|
2565
2703
|
i++ // skip the drop too
|
|
2566
2704
|
continue
|
|
2567
2705
|
}
|
|
2706
|
+
if (Array.isArray(child) && isDrop && child[0] === 'local.tee' && child.length === 3) {
|
|
2707
|
+
cleaned.push(['local.set', child[1], child[2]])
|
|
2708
|
+
i++ // skip the drop
|
|
2709
|
+
continue
|
|
2710
|
+
}
|
|
2568
2711
|
cleaned.push(child)
|
|
2569
2712
|
}
|
|
2570
2713
|
if (cleaned.length !== node.length) return cleaned
|
|
@@ -3681,6 +3824,26 @@ const mayInline = (ast) => {
|
|
|
3681
3824
|
return false
|
|
3682
3825
|
}
|
|
3683
3826
|
|
|
3827
|
+
// A `__start` whose body DCE emptied down to nothing — plus its `(start)`
|
|
3828
|
+
// directive — is pure noise: the directive invokes a no-op. jz's buildStartFn
|
|
3829
|
+
// only emits `__start` when it has content, so an empty one is always a post-DCE
|
|
3830
|
+
// artifact (e.g. a top-level `1 + 2;` whose dropped value the dead-code pass
|
|
3831
|
+
// removed). Drop both. Header nodes (param/result/local/export/type) don't count
|
|
3832
|
+
// as a body — a function carrying only locals still does nothing.
|
|
3833
|
+
const START_HEAD = new Set(['export', 'type', 'param', 'result', 'local'])
|
|
3834
|
+
function pruneEmptyStart(ast) {
|
|
3835
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
3836
|
+
const fn = ast.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
3837
|
+
if (!fn) return ast
|
|
3838
|
+
// Skip the name (index 1) and header nodes; a bare-string node past them is a
|
|
3839
|
+
// real instruction (`drop`/`nop`/`unreachable`), so it stops the scan.
|
|
3840
|
+
let b = 2
|
|
3841
|
+
while (b < fn.length && Array.isArray(fn[b]) && START_HEAD.has(fn[b][0])) b++
|
|
3842
|
+
if (b < fn.length) return ast // real instructions remain
|
|
3843
|
+
return ast.filter(n => !(Array.isArray(n) &&
|
|
3844
|
+
((n[0] === 'func' && n[1] === '$__start') || (n[0] === 'start' && n[1] === '$__start'))))
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3684
3847
|
export default function optimize(ast, opts = true) {
|
|
3685
3848
|
const strictGuard = opts === true // default: zero tolerance for bloat
|
|
3686
3849
|
opts = normalize(opts)
|
|
@@ -3696,7 +3859,22 @@ export default function optimize(ast, opts = true) {
|
|
|
3696
3859
|
// not trip the size-guard into reverting a whole round. A single sweep is
|
|
3697
3860
|
// complete: every call_indirect is visited; rewritten sites keep the original
|
|
3698
3861
|
// as the guarded fallback arm.
|
|
3699
|
-
|
|
3862
|
+
// `inline` (multi-caller, size-for-speed) is like `devirt`: it INTENTIONALLY grows
|
|
3863
|
+
// the binary, so it must run OUTSIDE the per-round size-revert guard below (which
|
|
3864
|
+
// would otherwise undo it). Run it once after the rounds converge, then tidy the
|
|
3865
|
+
// (block (local.set $p arg) … body) wrappers it leaves with the same cleanup passes
|
|
3866
|
+
// a normal round would. opt-in (speed level); a no-op when no small callee qualifies.
|
|
3867
|
+
const runInline = (a) => {
|
|
3868
|
+
if (!opts.inline) return a
|
|
3869
|
+
// `inline: true` → safe SIMD-helper-only default; `inline: 'all'` → unrestricted.
|
|
3870
|
+
a = inline(a, { simdOnly: opts.inline !== 'all' })
|
|
3871
|
+
if (opts.propagate) a = propagate(a)
|
|
3872
|
+
if (opts.mergeBlocks) a = mergeBlocks(a)
|
|
3873
|
+
if (opts.vacuum) a = vacuum(a)
|
|
3874
|
+
if (opts.coalesceLocals) a = coalesceLocals(a)
|
|
3875
|
+
return a
|
|
3876
|
+
}
|
|
3877
|
+
const finish = (a) => { a = runInline(a); return pruneEmptyStart(opts.devirt ? devirt(a) : a) }
|
|
3700
3878
|
|
|
3701
3879
|
// Fast path: jz owns this optimizer and feeds it a controlled, type-aware IR.
|
|
3702
3880
|
// The only passes that can *grow* the binary are inlineOnce/inline; when no
|
|
@@ -3727,7 +3905,7 @@ export default function optimize(ast, opts = true) {
|
|
|
3727
3905
|
for (let round = 0; round < 3; round++) {
|
|
3728
3906
|
beforeRound = clone(ast)
|
|
3729
3907
|
|
|
3730
|
-
for (const [key, fn] of PASSES) if (opts[key] && key !== 'devirt') ast = fn(ast)
|
|
3908
|
+
for (const [key, fn] of PASSES) if (opts[key] && key !== 'devirt' && key !== 'inline') ast = fn(ast)
|
|
3731
3909
|
// Second propagate sweep: `inlineOnce`/`inline` (above) leave fresh
|
|
3732
3910
|
// `(local.set $p arg) … (local.get $p)` wrappers around each inlined call;
|
|
3733
3911
|
// re-running propagation collapses them within this same round, so the size
|