jz 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
package/src/optimize.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* @module optimize
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { LAYOUT } from './ctx.js'
|
|
30
|
+
import { LAYOUT, ctx } from './ctx.js'
|
|
31
31
|
import { findBodyStart } from './ir.js'
|
|
32
32
|
import { vectorizeLaneLocal } from './vectorize.js'
|
|
33
33
|
|
|
@@ -45,17 +45,19 @@ const UNDEF_BITS = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHI
|
|
|
45
45
|
* 0 — nothing. Fastest compile, largest output. Useful for live coding.
|
|
46
46
|
* 1 — encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
|
|
47
47
|
* Cheap, no IR rewrites that perturb V8's tier-up shape.
|
|
48
|
-
* 2 — default.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* 2 — default. All stable jz passes + full watr (treeshake / dedupe / dedupTypes /
|
|
49
|
+
* coalesce / propagate / packData / fold / peephole / vacuum / mergeBlocks /
|
|
50
|
+
* brif / loopify / inlineOnce / …). `inline` stays off (watr's own default —
|
|
51
|
+
* opt-in only; can duplicate bodies).
|
|
52
|
+
* 3 — level 2 + larger array/hash initial caps + `hoistConstantPool` off
|
|
53
|
+
* (inline `f64.const` over mutable globals); trades size for speed.
|
|
51
54
|
*
|
|
52
55
|
* String aliases (the size↔speed tradeoff lives entirely in the unroll/scalar
|
|
53
|
-
* knobs
|
|
54
|
-
* 'size' —
|
|
55
|
-
*
|
|
56
|
+
* knobs; watr is on for all three):
|
|
57
|
+
* 'size' — loop/const unroll + lane vectorization off, tight scalar-replacement
|
|
58
|
+
* caps. Smallest wasm.
|
|
56
59
|
* 'balanced' — the default (= level 2).
|
|
57
|
-
* 'speed' —
|
|
58
|
-
* minus the heavy third-party watr pass).
|
|
60
|
+
* 'speed' — full nested unroll + lane vectorization (= level 3).
|
|
59
61
|
*/
|
|
60
62
|
export const PASS_NAMES = [
|
|
61
63
|
'watr', // third-party WAT-level CSE/DCE/inlining (heaviest)
|
|
@@ -67,6 +69,7 @@ export const PASS_NAMES = [
|
|
|
67
69
|
'hoistInvariantCellLoads',
|
|
68
70
|
'cseScalarLoad',
|
|
69
71
|
'csePureExpr',
|
|
72
|
+
'dropDeadZeroInit',
|
|
70
73
|
'deadStoreElim',
|
|
71
74
|
'promoteGlobals', // read-only global.get → local for multi-read globals
|
|
72
75
|
'sortLocalsByUse',
|
|
@@ -80,6 +83,7 @@ export const PASS_NAMES = [
|
|
|
80
83
|
'vectorizeLaneLocal', // SIMD-128 lift for lane-pure typed-array loops
|
|
81
84
|
'arenaRewind', // per-call heap rewind for no-arg scalar allocator kernels
|
|
82
85
|
'treeshake',
|
|
86
|
+
'jsstring', // boundary opt-in: flip exported string params to externref
|
|
83
87
|
]
|
|
84
88
|
|
|
85
89
|
const ALL_ON = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, true])))
|
|
@@ -87,16 +91,36 @@ const ALL_OFF = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, false])
|
|
|
87
91
|
const LEVEL_PRESETS = Object.freeze({
|
|
88
92
|
0: ALL_OFF,
|
|
89
93
|
1: Object.freeze({ ...ALL_OFF, treeshake: true, sortLocalsByUse: true, fusedRewrite: true }),
|
|
90
|
-
2:
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
+
// Default (level 2 / 'balanced'): every stable pass + full watr. Pre-4.6.9 had to
|
|
95
|
+
// force 'light' mode here (inline / inlineOnce / coalesce all off) to dodge the
|
|
96
|
+
// W1a/W1b miscompiles; watr 4.6.9 fixes both, and the L2 default now runs the full
|
|
97
|
+
// watr pipeline. `inline` stays off by watr's own default — opt-in only.
|
|
98
|
+
2: Object.freeze({ ...ALL_ON, nestedSmallConstForUnroll: 'auto' }),
|
|
99
|
+
// L3/'speed' trades a bit of heap headroom for fewer __arr_grow / __hash growth
|
|
100
|
+
// cycles. arrayMinCap=16 means `[]` and `new Array()` skip the first two doublings
|
|
101
|
+
// (0→2→4→8→16); hashSmallInitCap=8 keeps per-object __dyn_props at the same load
|
|
102
|
+
// factor as the global __hash_new on first set, avoiding the 2→4→8 grow chain.
|
|
103
|
+
// Net cost: ~128 B per empty array, ~144 B per per-object hash. Net win on the
|
|
104
|
+
// watr.compile profile: __arr_grow ~6.7% → ~3%, and lower __ihash_get_local
|
|
105
|
+
// probe depth from a denser-load global hash.
|
|
106
|
+
// L3/'speed' also turns hoistConstantPool OFF: pooling repeated `f64.const`
|
|
107
|
+
// into `(mut f64)` globals is a pure size win (~7 B/reuse) but a speed loss —
|
|
108
|
+
// a mutable global can't be constant-folded by V8 (any call may mutate it), so
|
|
109
|
+
// every use becomes a load, and promoteGlobals then snapshots the pool into
|
|
110
|
+
// `_pg` locals at each hot function's entry (register pressure in the big
|
|
111
|
+
// closures). Inline `f64.const` is the minimal lowering: V8 CSEs identical
|
|
112
|
+
// constants for free. Measured −3% on jessie parse for +14% binary — exactly
|
|
113
|
+
// the size↔speed trade 'speed' exists to make.
|
|
114
|
+
3: Object.freeze({ ...ALL_ON, hoistConstantPool: false, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
115
|
+
// 'balanced' = level 2; 'size' tightens scalar/unroll caps; 'speed' = level 3.
|
|
116
|
+
balanced: Object.freeze({ ...ALL_ON, nestedSmallConstForUnroll: 'auto' }),
|
|
94
117
|
size: Object.freeze({
|
|
95
|
-
...ALL_ON,
|
|
118
|
+
...ALL_ON,
|
|
96
119
|
smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false,
|
|
97
120
|
scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
|
|
98
121
|
}),
|
|
99
|
-
speed:
|
|
122
|
+
// 'speed' === level 3: full watr (inlining on) + L3 cap/hash tuning, pool off.
|
|
123
|
+
speed: Object.freeze({ ...ALL_ON, hoistConstantPool: false, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
100
124
|
})
|
|
101
125
|
|
|
102
126
|
/**
|
|
@@ -118,7 +142,14 @@ export function resolveOptimize(opt) {
|
|
|
118
142
|
const baseLevel = typeof opt.level === 'number' || typeof opt.level === 'string' ? opt.level : 2
|
|
119
143
|
const base = LEVEL_PRESETS[baseLevel] || ALL_ON
|
|
120
144
|
const out = { ...base }
|
|
121
|
-
for (const n of PASS_NAMES)
|
|
145
|
+
for (const n of PASS_NAMES) {
|
|
146
|
+
if (!(n in opt)) continue
|
|
147
|
+
const v = opt[n]
|
|
148
|
+
// Preserve sentinel value `nestedSmallConstForUnroll: 'auto'`
|
|
149
|
+
// (resolved by a heuristic at emit time).
|
|
150
|
+
if (n === 'nestedSmallConstForUnroll' && v === 'auto') out[n] = 'auto'
|
|
151
|
+
else out[n] = !!v
|
|
152
|
+
}
|
|
122
153
|
// Preserve non-pass tuning keys (e.g. plan.js thresholds)
|
|
123
154
|
for (const k of Object.keys(opt)) if (!PASS_NAMES.includes(k)) out[k] = opt[k]
|
|
124
155
|
return out
|
|
@@ -841,44 +872,33 @@ export function hoistInvariantCellLoads(fn) {
|
|
|
841
872
|
* — emits 6 f64.load on $p (each of x/y/z twice); collapses to 3 unique loads
|
|
842
873
|
* shared via tee'd snap locals.
|
|
843
874
|
*
|
|
844
|
-
* Safety:
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
875
|
+
* Safety: candidacy is the emit-side `cseSafeLoadBases` whitelist (src/analyze.js),
|
|
876
|
+
* stamped onto the func node as `fn.cseLoadBases`. Every base in it is a
|
|
877
|
+
* bound-once unboxed pointer used solely as a member-read receiver whose
|
|
878
|
+
* allocation kind is disjoint from every store the function performs. So
|
|
879
|
+
* `(f64.store ADDR ...)` anywhere in the body cannot touch addresses reachable
|
|
880
|
+
* via `$X + K` for a whitelisted $X — the proof is carried from emit, where the
|
|
881
|
+
* VAL kinds and binding shapes are still known, never re-guessed at WAT level.
|
|
849
882
|
*
|
|
850
883
|
* Region boundaries that flush the table:
|
|
851
884
|
* - branch (br/br_if/br_table/return/unreachable)
|
|
852
885
|
* - non-pure call
|
|
853
886
|
* - loop / if (control flow)
|
|
854
887
|
* - local.set/local.tee on a tracked $X (invalidates that X's entries)
|
|
855
|
-
* - store whose address tree references a tracked $X
|
|
888
|
+
* - store whose address tree references a tracked $X (defence-in-depth —
|
|
889
|
+
* the whitelist already guarantees this never happens)
|
|
856
890
|
* Blocks are treated as transparent — recurse into children.
|
|
857
891
|
*/
|
|
858
892
|
export function cseScalarLoad(fn) {
|
|
859
|
-
// DISABLED: the safety claim above relies on `analyzePtrUnboxable` having vetted
|
|
860
|
-
// every i32 local as a non-aliased fresh-allocation pointer. But this pass scans
|
|
861
|
-
// *all* i32 locals from `(local … i32)` decls — wasm-native i32 scalars (lengths,
|
|
862
|
-
// indices), narrow-ABI helper returns, and analyze.js's new arrayElemSchema-driven
|
|
863
|
-
// unboxes share the same declaration form. The metacircular path (jz-compiled
|
|
864
|
-
// watr.wasm) trips this: CSE'd loads survive across stores that legitimately
|
|
865
|
-
// mutate the same memory through a different i32 local, returning stale bytes
|
|
866
|
-
// (manifests as "memory access out of bounds" once a corrupted offset is
|
|
867
|
-
// dereferenced). Re-enable once candidacy is restricted to vetted pointers
|
|
868
|
-
// (e.g. emit-side annotation or rep-derived whitelist).
|
|
869
|
-
return
|
|
870
893
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
871
894
|
const bodyStart = findBodyStart(fn)
|
|
872
895
|
if (bodyStart < 0) return
|
|
873
896
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (!i32Locals.size) return
|
|
897
|
+
// Soundness gate: only the emit-proven non-aliasing bases. Absent the stamp
|
|
898
|
+
// (e.g. a post-watrOptimize re-run on rebuilt nodes) the set is empty and the
|
|
899
|
+
// pass is a strict no-op — never a speculative CSE.
|
|
900
|
+
const bases = fn.cseLoadBases
|
|
901
|
+
if (!(bases instanceof Set) || bases.size === 0) return
|
|
882
902
|
|
|
883
903
|
let snapId = 0
|
|
884
904
|
while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__cs${snapId}`)) snapId++
|
|
@@ -893,10 +913,10 @@ export function cseScalarLoad(fn) {
|
|
|
893
913
|
}
|
|
894
914
|
}
|
|
895
915
|
|
|
896
|
-
// Scan a node's subtree and return the set of
|
|
916
|
+
// Scan a node's subtree and return the set of tracked bases referenced via local.get.
|
|
897
917
|
const collectGets = (node, out) => {
|
|
898
918
|
if (!Array.isArray(node)) return
|
|
899
|
-
if (node[0] === 'local.get' && typeof node[1] === 'string' &&
|
|
919
|
+
if (node[0] === 'local.get' && typeof node[1] === 'string' && bases.has(node[1])) {
|
|
900
920
|
out.add(node[1])
|
|
901
921
|
return
|
|
902
922
|
}
|
|
@@ -983,7 +1003,7 @@ export function cseScalarLoad(fn) {
|
|
|
983
1003
|
const lp = parseLoad(node)
|
|
984
1004
|
if (lp) {
|
|
985
1005
|
const addr = node[lp.addrIdx]
|
|
986
|
-
if (Array.isArray(addr) && addr[0] === 'local.get' && typeof addr[1] === 'string' &&
|
|
1006
|
+
if (Array.isArray(addr) && addr[0] === 'local.get' && typeof addr[1] === 'string' && bases.has(addr[1])) {
|
|
987
1007
|
const X = addr[1]
|
|
988
1008
|
const key = `${X}|${lp.K}`
|
|
989
1009
|
const entry = table.get(key)
|
|
@@ -1042,8 +1062,16 @@ export function csePureExpr(fn) {
|
|
|
1042
1062
|
const bodyStart = findBodyStart(fn)
|
|
1043
1063
|
if (bodyStart < 0) return
|
|
1044
1064
|
|
|
1065
|
+
// High-water mark across ALL surviving `$__pe<N>` locals, not the first gap.
|
|
1066
|
+
// A prior csePureExpr run + watr.coalesce can leave non-contiguous numbering
|
|
1067
|
+
// (e.g. $__pe0,$__pe1,$__pe5,$__pe20 — coalesce removed the merged ones); picking
|
|
1068
|
+
// the first gap (2) then allocating sequentially would collide on $__pe5 / $__pe20.
|
|
1045
1069
|
let snapId = 0
|
|
1046
|
-
|
|
1070
|
+
for (const n of fn) {
|
|
1071
|
+
if (!Array.isArray(n) || n[0] !== 'local' || typeof n[1] !== 'string') continue
|
|
1072
|
+
const m = /^\$__pe(\d+)$/.exec(n[1])
|
|
1073
|
+
if (m) { const k = +m[1]; if (k >= snapId) snapId = k + 1 }
|
|
1074
|
+
}
|
|
1047
1075
|
const newLocals = []
|
|
1048
1076
|
|
|
1049
1077
|
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'])
|
|
@@ -1148,6 +1176,72 @@ export function csePureExpr(fn) {
|
|
|
1148
1176
|
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
1149
1177
|
}
|
|
1150
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* Drop redundant zero-initialisation of fresh function-scope locals.
|
|
1181
|
+
*
|
|
1182
|
+
* WASM zero-initialises every local on entry (0 / 0.0 / null). jz lowers source
|
|
1183
|
+
* `let x = 0` to `(local $x …)` + `(local.set $x (<zero const>))` at the top of
|
|
1184
|
+
* the function body — the explicit set is a no-op when nothing has touched `$x`
|
|
1185
|
+
* yet. `wasm-opt -Oz` elides these; do the same so jz's own output is minimal.
|
|
1186
|
+
*
|
|
1187
|
+
* Only removes a `(local.set $L (i32|i64|f64|f32.const 0))` when:
|
|
1188
|
+
* - `$L` is a non-param local (a param's "default" is the incoming arg, not 0),
|
|
1189
|
+
* - it is a *top-level* body statement (never descend into block/loop/if — a
|
|
1190
|
+
* nested zero-set inside a loop genuinely re-initialises across iterations),
|
|
1191
|
+
* - `$L` has not been referenced by any earlier top-level statement (so the
|
|
1192
|
+
* local still holds its entry-time zero at this point),
|
|
1193
|
+
* - `$L` is read (`local.get`) somewhere in the function (otherwise leave the
|
|
1194
|
+
* store for deadStoreElim and avoid orphaning the `(local $L …)` decl),
|
|
1195
|
+
* - the constant is +0 / +0.0 (a `-0.0` f64 set is *not* redundant — locals
|
|
1196
|
+
* default to +0.0, which differs in bits from -0.0).
|
|
1197
|
+
*/
|
|
1198
|
+
export function dropDeadZeroInit(fn) {
|
|
1199
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
1200
|
+
const bodyStart = findBodyStart(fn)
|
|
1201
|
+
if (bodyStart < 0) return
|
|
1202
|
+
|
|
1203
|
+
const seen = new Set() // params + locals referenced by an earlier stmt
|
|
1204
|
+
const reads = new Set() // locals read by `local.get` anywhere
|
|
1205
|
+
for (const c of fn) if (Array.isArray(c) && c[0] === 'param' && typeof c[1] === 'string') seen.add(c[1])
|
|
1206
|
+
|
|
1207
|
+
const collectGets = (node) => {
|
|
1208
|
+
if (!Array.isArray(node)) return
|
|
1209
|
+
if (node[0] === 'local.get' && typeof node[1] === 'string') reads.add(node[1])
|
|
1210
|
+
for (let i = 1; i < node.length; i++) collectGets(node[i])
|
|
1211
|
+
}
|
|
1212
|
+
for (let i = bodyStart; i < fn.length; i++) collectGets(fn[i])
|
|
1213
|
+
|
|
1214
|
+
const collectRefs = (node) => {
|
|
1215
|
+
if (!Array.isArray(node)) return
|
|
1216
|
+
const op = node[0]
|
|
1217
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') seen.add(node[1])
|
|
1218
|
+
for (let i = 1; i < node.length; i++) collectRefs(node[i])
|
|
1219
|
+
}
|
|
1220
|
+
const isPlusZeroConst = (e) => {
|
|
1221
|
+
if (!Array.isArray(e) || e.length !== 2) return false
|
|
1222
|
+
if (e[0] !== 'i32.const' && e[0] !== 'i64.const' && e[0] !== 'f64.const' && e[0] !== 'f32.const') return false
|
|
1223
|
+
const v = e[1]
|
|
1224
|
+
if (typeof v === 'bigint') return v === 0n
|
|
1225
|
+
if (typeof v === 'number') return v === 0 && !Object.is(v, -0)
|
|
1226
|
+
if (typeof v === 'string') { const t = v.trim(); return t === '0' || t === '0.0' || t === '+0' || t === '+0.0' }
|
|
1227
|
+
return false
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const drop = []
|
|
1231
|
+
for (let i = bodyStart; i < fn.length; i++) {
|
|
1232
|
+
const node = fn[i]
|
|
1233
|
+
if (!Array.isArray(node)) continue
|
|
1234
|
+
if (node[0] === 'local.set' && node.length === 3 && typeof node[1] === 'string' &&
|
|
1235
|
+
!seen.has(node[1]) && reads.has(node[1]) && isPlusZeroConst(node[2])) {
|
|
1236
|
+
drop.push(i)
|
|
1237
|
+
seen.add(node[1])
|
|
1238
|
+
continue
|
|
1239
|
+
}
|
|
1240
|
+
collectRefs(node)
|
|
1241
|
+
}
|
|
1242
|
+
for (let i = drop.length - 1; i >= 0; i--) fn.splice(drop[i], 1)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1151
1245
|
/**
|
|
1152
1246
|
* Dead-store elimination: remove `local.set` / `local.tee` and `drop` of pure
|
|
1153
1247
|
* expressions whose values are never consumed.
|
|
@@ -1192,21 +1286,31 @@ export function deadStoreElim(fn) {
|
|
|
1192
1286
|
if (!Array.isArray(node)) continue
|
|
1193
1287
|
const op = node[0]
|
|
1194
1288
|
|
|
1195
|
-
// Reads invalidate pending dead writes
|
|
1289
|
+
// Reads invalidate pending dead writes. For local.tee/local.set, the RHS reads
|
|
1290
|
+
// happen BEFORE the write — so a `local.get $x` inside `(local.tee $x ...)` is a
|
|
1291
|
+
// real read of the OLD $x and must invalidate any pending dead-write of $x.
|
|
1196
1292
|
const reads = new Set()
|
|
1197
1293
|
collectGets(node, reads)
|
|
1198
|
-
if (op === 'local.tee') reads.delete(node[1])
|
|
1199
1294
|
for (const name of reads) lastWrite.delete(name)
|
|
1200
1295
|
|
|
1201
|
-
// Drop of pure expr → dead
|
|
1202
|
-
|
|
1296
|
+
// Drop of pure expr → dead. Only `(drop EXPR)`: a bare `(drop)` consumes
|
|
1297
|
+
// an implicit stack value (e.g. a `try_table` catch payload) — removing it
|
|
1298
|
+
// would unbalance the stack.
|
|
1299
|
+
if (op === 'drop' && node.length === 2 && isPure(node[1])) {
|
|
1203
1300
|
dead.push({ parent: items, idx: i, drop: true })
|
|
1204
1301
|
}
|
|
1205
1302
|
|
|
1206
1303
|
// Local write tracking
|
|
1207
1304
|
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') {
|
|
1208
1305
|
const prev = lastWrite.get(node[1])
|
|
1209
|
-
if (prev)
|
|
1306
|
+
if (prev) {
|
|
1307
|
+
// The store-to-local is dead, but a `local.set` is only *removable*
|
|
1308
|
+
// if its RHS is pure — `local.set $x (call f …)` where `f` mutates
|
|
1309
|
+
// memory must still run. (A `local.tee` is always safe: removal demotes
|
|
1310
|
+
// it to its value expression, so any side effects there are preserved.)
|
|
1311
|
+
const pn = prev.parent[prev.idx]
|
|
1312
|
+
if (pn[0] === 'local.tee' || isPure(pn[2])) dead.push(prev)
|
|
1313
|
+
}
|
|
1210
1314
|
lastWrite.set(node[1], { parent: items, idx: i })
|
|
1211
1315
|
}
|
|
1212
1316
|
|
|
@@ -1250,6 +1354,36 @@ export function deadStoreElim(fn) {
|
|
|
1250
1354
|
}
|
|
1251
1355
|
}
|
|
1252
1356
|
|
|
1357
|
+
/**
|
|
1358
|
+
* Module-wide scan for "volatile" globals — those mutated (`global.set`) in any
|
|
1359
|
+
* function other than `$__start`. Globals written only in `$__start` are
|
|
1360
|
+
* init-once: `$__start` runs to completion before any other function, so they
|
|
1361
|
+
* are effectively read-only afterwards and stay promotable.
|
|
1362
|
+
*
|
|
1363
|
+
* promoteGlobals uses this to avoid caching a callee-mutable global into a
|
|
1364
|
+
* function-entry local across a call (which would leave the local stale).
|
|
1365
|
+
*
|
|
1366
|
+
* @param {Array<Array>} funcs - all module function IR nodes
|
|
1367
|
+
* @returns {Set<string>} volatile global names (with leading `$`)
|
|
1368
|
+
*/
|
|
1369
|
+
export function collectVolatileGlobals(funcs) {
|
|
1370
|
+
const volatile = new Set()
|
|
1371
|
+
const scan = (node) => {
|
|
1372
|
+
if (!Array.isArray(node)) return
|
|
1373
|
+
if (node[0] === 'global.set') {
|
|
1374
|
+
if (typeof node[1] === 'string') volatile.add(node[1])
|
|
1375
|
+
for (let i = 2; i < node.length; i++) scan(node[i])
|
|
1376
|
+
return
|
|
1377
|
+
}
|
|
1378
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
1379
|
+
}
|
|
1380
|
+
for (const fn of funcs) {
|
|
1381
|
+
if (!Array.isArray(fn) || fn[0] !== 'func' || fn[1] === '$__start') continue
|
|
1382
|
+
for (let i = 2; i < fn.length; i++) scan(fn[i])
|
|
1383
|
+
}
|
|
1384
|
+
return volatile
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1253
1387
|
/**
|
|
1254
1388
|
* Promote read-only globals to locals within each function.
|
|
1255
1389
|
*
|
|
@@ -1264,17 +1398,26 @@ export function deadStoreElim(fn) {
|
|
|
1264
1398
|
* also written (global.set) are left untouched — the promotion would be unsound if
|
|
1265
1399
|
* the global changes between reads.
|
|
1266
1400
|
*
|
|
1401
|
+
* A within-function read-only check is NOT sufficient: a callee can mutate the
|
|
1402
|
+
* global between two reads in this function. `volatileGlobals` (globals written
|
|
1403
|
+
* anywhere outside `$__start`) gates that case — a volatile global is not
|
|
1404
|
+
* promoted in any function that makes a call. Init-once globals (written only in
|
|
1405
|
+
* `$__start`) stay promotable everywhere.
|
|
1406
|
+
*
|
|
1267
1407
|
* @param {Array} fn - Function IR (WAT-as-array)
|
|
1268
1408
|
* @param {Map<string,string>} [globalTypes] - Optional: global name → wasm type ('i32'|'f64'|'i64'|'funcref')
|
|
1409
|
+
* @param {Set<string>} [volatileGlobals] - Optional: globals mutated outside `$__start` (see collectVolatileGlobals)
|
|
1269
1410
|
*/
|
|
1270
|
-
export function promoteGlobals(fn, globalTypes) {
|
|
1411
|
+
export function promoteGlobals(fn, globalTypes, volatileGlobals) {
|
|
1271
1412
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
1272
1413
|
const bodyStart = findBodyStart(fn)
|
|
1273
1414
|
if (bodyStart < 0) return
|
|
1274
1415
|
|
|
1275
|
-
// Collect global.get counts
|
|
1416
|
+
// Collect global.get counts, detect any global.set, and note whether the
|
|
1417
|
+
// function makes a call (a callee may mutate a volatile global between reads).
|
|
1276
1418
|
const getCounts = new Map() // globalName → count
|
|
1277
1419
|
const written = new Set()
|
|
1420
|
+
let hasCall = false
|
|
1278
1421
|
|
|
1279
1422
|
const scan = (node) => {
|
|
1280
1423
|
if (!Array.isArray(node)) return
|
|
@@ -1288,6 +1431,7 @@ export function promoteGlobals(fn, globalTypes) {
|
|
|
1288
1431
|
if (node[2]) scan(node[2])
|
|
1289
1432
|
return
|
|
1290
1433
|
}
|
|
1434
|
+
if (op === 'call' || op === 'call_indirect') hasCall = true
|
|
1291
1435
|
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
1292
1436
|
}
|
|
1293
1437
|
|
|
@@ -1307,6 +1451,8 @@ export function promoteGlobals(fn, globalTypes) {
|
|
|
1307
1451
|
const replacements = new Map()
|
|
1308
1452
|
for (const [gName, count] of getCounts) {
|
|
1309
1453
|
if (count < 3 || written.has(gName)) continue
|
|
1454
|
+
// Unsound to cache a callee-mutable global across a call in this function.
|
|
1455
|
+
if (hasCall && volatileGlobals && volatileGlobals.has(gName)) continue
|
|
1310
1456
|
// Determine type: use provided map, or infer from context
|
|
1311
1457
|
const type = globalTypes?.get(gName) || inferTypeFromContext(fn, gName, bodyStart)
|
|
1312
1458
|
if (!type) continue // can't determine type, skip
|
|
@@ -1472,7 +1618,8 @@ export function specializeMkptr(funcs, addFunc, parseWat) {
|
|
|
1472
1618
|
// Any target not listed here is left untouched. Order matters only for readability.
|
|
1473
1619
|
const SPECS = {
|
|
1474
1620
|
'$__mkptr': { params: ['i32', 'i32', 'i32'], result: 'f64', inline: true },
|
|
1475
|
-
'$__alloc_hdr':
|
|
1621
|
+
'$__alloc_hdr': { params: ['i32', 'i32'], result: 'i32' },
|
|
1622
|
+
'$__alloc_hdr_n': { params: ['i32', 'i32', 'i32'], result: 'i32' },
|
|
1476
1623
|
'$__typed_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
1477
1624
|
'$__str_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
1478
1625
|
}
|
|
@@ -1772,8 +1919,10 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
|
|
|
1772
1919
|
*
|
|
1773
1920
|
* @param fn func IR node
|
|
1774
1921
|
* @param cfg optional resolved config from resolveOptimize() — when omitted, all on.
|
|
1922
|
+
* @param globalTypes optional global name → wasm type map (for promoteGlobals)
|
|
1923
|
+
* @param volatileGlobals optional set of callee-mutable globals (see collectVolatileGlobals)
|
|
1775
1924
|
*/
|
|
1776
|
-
export function optimizeFunc(fn, cfg, globalTypes) {
|
|
1925
|
+
export function optimizeFunc(fn, cfg, globalTypes, volatileGlobals) {
|
|
1777
1926
|
if (cfg && cfg.hoistPtrType === false &&
|
|
1778
1927
|
cfg.hoistInvariantPtrOffset === false &&
|
|
1779
1928
|
cfg.hoistInvariantPtrOffsetLoop === false &&
|
|
@@ -1782,6 +1931,7 @@ export function optimizeFunc(fn, cfg, globalTypes) {
|
|
|
1782
1931
|
cfg.hoistInvariantCellLoads === false &&
|
|
1783
1932
|
cfg.cseScalarLoad === false &&
|
|
1784
1933
|
cfg.csePureExpr === false &&
|
|
1934
|
+
cfg.dropDeadZeroInit === false &&
|
|
1785
1935
|
cfg.deadStoreElim === false &&
|
|
1786
1936
|
cfg.promoteGlobals === false &&
|
|
1787
1937
|
cfg.sortLocalsByUse === false &&
|
|
@@ -1795,12 +1945,17 @@ export function optimizeFunc(fn, cfg, globalTypes) {
|
|
|
1795
1945
|
if (!cfg || cfg.hoistInvariantCellLoads !== false) hoistInvariantCellLoads(fn)
|
|
1796
1946
|
if (!cfg || cfg.cseScalarLoad !== false) cseScalarLoad(fn)
|
|
1797
1947
|
if (!cfg || cfg.csePureExpr !== false) csePureExpr(fn)
|
|
1948
|
+
if (!cfg || cfg.dropDeadZeroInit !== false) dropDeadZeroInit(fn)
|
|
1798
1949
|
if (!cfg || cfg.deadStoreElim !== false) deadStoreElim(fn)
|
|
1799
|
-
if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes)
|
|
1800
|
-
// Vectorizer runs
|
|
1801
|
-
//
|
|
1950
|
+
if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes, volatileGlobals)
|
|
1951
|
+
// Vectorizer runs PRE-watr unless full watr is enabled (`watr: true`). For full watr,
|
|
1952
|
+
// defer to post — full passes (notably `inlineOnce` + the post-inline `propagate`
|
|
1953
|
+
// sweep) reshape the IR so much that pre-watr SIMD patterns get scrambled. Light
|
|
1954
|
+
// watr (or no watr) leaves the lane locals intact for vectorize to pattern-match,
|
|
1955
|
+
// and lets a non-trivial chunk of SIMD survive the propagate+fold pipeline.
|
|
1802
1956
|
if (cfg && cfg.vectorizeLaneLocal === true) {
|
|
1803
|
-
const
|
|
1957
|
+
const fullWatr = cfg.watr === true
|
|
1958
|
+
const runVectorizer = (fullWatr && cfg.__phase === 'post') || (!fullWatr && cfg.__phase !== 'post')
|
|
1804
1959
|
if (runVectorizer) vectorizeLaneLocal(fn)
|
|
1805
1960
|
}
|
|
1806
1961
|
if (!cfg || cfg.sortLocalsByUse !== false) sortLocalsByUse(fn, cfg && cfg.fusedRewrite !== false ? counts : null)
|
|
@@ -1860,11 +2015,18 @@ function walkRewrite(node, doInline, counts) {
|
|
|
1860
2015
|
['i64.eq', node[2], ['i64.const', NULL_BITS]],
|
|
1861
2016
|
['i64.eq', node[2], ['i64.const', UNDEF_BITS]]]
|
|
1862
2017
|
if (fname === '$__is_truthy' && Array.isArray(node[2]) && node[2][0] === 'i64.reinterpret_f64'
|
|
1863
|
-
&& Array.isArray(node[2][1]) && node[2][1][0] === 'local.get') {
|
|
1864
|
-
|
|
1865
|
-
|
|
2018
|
+
&& Array.isArray(node[2][1]) && (node[2][1][0] === 'local.get' || node[2][1][0] === 'local.tee')) {
|
|
2019
|
+
// `local.tee $x SRC` evaluates SRC once, stores to $x, returns the value —
|
|
2020
|
+
// hot for `a || b` lowering (`__is_truthy(local.tee $t …)`). Keep the tee
|
|
2021
|
+
// as the first use (the `if` condition runs before then/else, and f64.eq's
|
|
2022
|
+
// left operand runs first), so $x is set before every `local.get` repeat.
|
|
2023
|
+
const ref = node[2][1]
|
|
2024
|
+
const lname = ref[1]
|
|
2025
|
+
const lget = ['local.get', lname]
|
|
2026
|
+
const first = ref[0] === 'local.tee' ? ref : lget
|
|
2027
|
+
const bits = ['i64.reinterpret_f64', lget]
|
|
1866
2028
|
return ['if', ['result', 'i32'],
|
|
1867
|
-
['f64.eq',
|
|
2029
|
+
['f64.eq', first, lget],
|
|
1868
2030
|
['then', ['f64.ne', lget, ['f64.const', 0]]],
|
|
1869
2031
|
['else', ['i32.and',
|
|
1870
2032
|
['i32.and',
|
|
@@ -1902,46 +2064,18 @@ function walkRewrite(node, doInline, counts) {
|
|
|
1902
2064
|
if (Array.isArray(a) && a[0] === 'f64.convert_i32_s' && a.length === 2) return ['i64.extend_i32_s', a[1]]
|
|
1903
2065
|
if (Array.isArray(a) && a[0] === 'f64.convert_i32_u' && a.length === 2) return ['i64.extend_i32_u', a[1]]
|
|
1904
2066
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
if (op === 'f64.reinterpret_i64'
|
|
1910
|
-
const
|
|
1911
|
-
if (
|
|
2067
|
+
// Rep-specific folds (NaN-box layout-aware reinterpret/wrap simplifications under
|
|
2068
|
+
// the nanbox preset). See abi/number/<rep>.js — each rep owns the rules that
|
|
2069
|
+
// depend on its own carrier layout. The universal `i32.wrap_i64 (i64.extend_i32_*)`
|
|
2070
|
+
// fold below stays here because it's pure WASM bit-pattern, ABI-agnostic.
|
|
2071
|
+
if (op === 'i64.reinterpret_f64' || op === 'f64.reinterpret_i64' || op === 'i32.wrap_i64') {
|
|
2072
|
+
const repFold = ctx.abi?.number?.peephole(node)
|
|
2073
|
+
if (repFold != null) return repFold
|
|
1912
2074
|
}
|
|
1913
2075
|
if (op === 'i32.wrap_i64' && node.length === 2) {
|
|
1914
2076
|
const a = node[1]
|
|
1915
2077
|
if (Array.isArray(a) && (a[0] === 'i64.extend_i32_u' || a[0] === 'i64.extend_i32_s') && a.length === 2)
|
|
1916
2078
|
return a[1]
|
|
1917
|
-
// (i32.wrap_i64 (i64.reinterpret_f64 (f64.load ADDR ?offset))) → (i32.load ADDR ?offset).
|
|
1918
|
-
// Wasm is little-endian; the low 32 bits of the f64 at ADDR are exactly
|
|
1919
|
-
// i32.load(ADDR). Saves two ops on every NaN-box pointer extraction from
|
|
1920
|
-
// an array slot or struct field.
|
|
1921
|
-
if (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) {
|
|
1922
|
-
const inner = a[1]
|
|
1923
|
-
if (Array.isArray(inner) && inner[0] === 'f64.load') {
|
|
1924
|
-
const out = ['i32.load']
|
|
1925
|
-
for (let i = 1; i < inner.length; i++) out.push(inner[i])
|
|
1926
|
-
return out
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
if (Array.isArray(a) && a[0] === 'i64.or' && a.length === 3) {
|
|
1930
|
-
const l = a[1], r = a[2]
|
|
1931
|
-
const isHighOnly = (n) => {
|
|
1932
|
-
if (!Array.isArray(n) || n[0] !== 'i64.const') return false
|
|
1933
|
-
const v = n[1]
|
|
1934
|
-
let bi
|
|
1935
|
-
if (typeof v === 'number') bi = BigInt(v)
|
|
1936
|
-
else if (typeof v === 'string') {
|
|
1937
|
-
try { bi = v.startsWith('-') ? -BigInt(v.slice(1)) : BigInt(v) } catch { return false }
|
|
1938
|
-
} else return false
|
|
1939
|
-
return (bi & 0xFFFFFFFFn) === 0n
|
|
1940
|
-
}
|
|
1941
|
-
const isExtend = (n) => Array.isArray(n) && (n[0] === 'i64.extend_i32_u' || n[0] === 'i64.extend_i32_s') && n.length === 2
|
|
1942
|
-
if (isHighOnly(l) && isExtend(r)) return r[1]
|
|
1943
|
-
if (isHighOnly(r) && isExtend(l)) return l[1]
|
|
1944
|
-
}
|
|
1945
2079
|
}
|
|
1946
2080
|
|
|
1947
2081
|
// shl-distribute-over-add: (i32.shl (i32.add x (i32.const K)) (i32.const S))
|
|
@@ -2081,6 +2215,36 @@ export function treeshake(funcSections, allModuleNodes, opts) {
|
|
|
2081
2215
|
}
|
|
2082
2216
|
}
|
|
2083
2217
|
}
|
|
2218
|
+
|
|
2219
|
+
// Dead-global elimination: after dead funcs are gone, drop `(global $g …)` decls
|
|
2220
|
+
// that nothing references (a `global.get`/`global.set` in a remaining func, a kept
|
|
2221
|
+
// global's init expr, a data/elem offset, or an `(export … (global $g))`). Imported
|
|
2222
|
+
// globals live in `allModuleNodes`, not in `opts.globals`, so they're never touched.
|
|
2223
|
+
// Fixpoint: a kept global's init may reference another global.
|
|
2224
|
+
const globals = removeDead && opts && Array.isArray(opts.globals) ? opts.globals : null
|
|
2225
|
+
if (globals) {
|
|
2226
|
+
const collectGlobalRefs = (node, refd) => {
|
|
2227
|
+
if (!Array.isArray(node)) return
|
|
2228
|
+
if ((node[0] === 'global.get' || node[0] === 'global.set') && typeof node[1] === 'string') refd.add(node[1])
|
|
2229
|
+
else if (node[0] === 'export' && Array.isArray(node[2]) && node[2][0] === 'global' && typeof node[2][1] === 'string') refd.add(node[2][1])
|
|
2230
|
+
for (const c of node) collectGlobalRefs(c, refd)
|
|
2231
|
+
}
|
|
2232
|
+
let changed = true
|
|
2233
|
+
while (changed) {
|
|
2234
|
+
changed = false
|
|
2235
|
+
const refd = new Set()
|
|
2236
|
+
for (const { arr } of funcSections) for (const n of arr) collectGlobalRefs(n, refd)
|
|
2237
|
+
for (const n of allModuleNodes) collectGlobalRefs(n, refd)
|
|
2238
|
+
for (const g of globals) collectGlobalRefs(g, refd)
|
|
2239
|
+
for (let i = globals.length - 1; i >= 0; i--) {
|
|
2240
|
+
const g = globals[i]
|
|
2241
|
+
if (Array.isArray(g) && g[0] === 'global' && typeof g[1] === 'string' && !refd.has(g[1])) {
|
|
2242
|
+
globals.splice(i, 1); changed = true
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2084
2248
|
return { removed, callCount }
|
|
2085
2249
|
}
|
|
2086
2250
|
|
|
@@ -2146,7 +2310,7 @@ export function sortLocalsByUse(fn, precomputedCounts) {
|
|
|
2146
2310
|
*/
|
|
2147
2311
|
export function arenaRewindModule(fns) {
|
|
2148
2312
|
const BUILTIN_SAFE = new Set([
|
|
2149
|
-
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
2313
|
+
'$__alloc', '$__alloc_hdr', '$__alloc_hdr_n', '$__mkptr',
|
|
2150
2314
|
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
2151
2315
|
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
2152
2316
|
])
|
|
@@ -2179,7 +2343,7 @@ export function arenaRewindModule(fns) {
|
|
|
2179
2343
|
else if (op === 'call_ref') hasCallRef = true
|
|
2180
2344
|
else if (op === 'call') {
|
|
2181
2345
|
const callee = node[1]
|
|
2182
|
-
if (callee === '$__alloc' || callee === '$__alloc_hdr') hasAlloc = true
|
|
2346
|
+
if (callee === '$__alloc' || callee === '$__alloc_hdr' || callee === '$__alloc_hdr_n') hasAlloc = true
|
|
2183
2347
|
if (typeof callee === 'string' && !BUILTIN_SAFE.has(callee)) calls.add(callee)
|
|
2184
2348
|
}
|
|
2185
2349
|
for (let i = 1; i < node.length; i++) scan(node[i])
|