jz 0.4.0 → 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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +251 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +551 -128
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +540 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- 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 +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +662 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +786 -94
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +822 -150
- 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/fuse.js +0 -159
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,12 +45,12 @@ 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. All stable jz passes + watr
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
54
|
*
|
|
55
55
|
* String aliases (the size↔speed tradeoff lives entirely in the unroll/scalar
|
|
56
56
|
* knobs; watr is on for all three):
|
|
@@ -83,6 +83,7 @@ export const PASS_NAMES = [
|
|
|
83
83
|
'vectorizeLaneLocal', // SIMD-128 lift for lane-pure typed-array loops
|
|
84
84
|
'arenaRewind', // per-call heap rewind for no-arg scalar allocator kernels
|
|
85
85
|
'treeshake',
|
|
86
|
+
'jsstring', // boundary opt-in: flip exported string params to externref
|
|
86
87
|
]
|
|
87
88
|
|
|
88
89
|
const ALL_ON = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, true])))
|
|
@@ -90,13 +91,11 @@ const ALL_OFF = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, false])
|
|
|
90
91
|
const LEVEL_PRESETS = Object.freeze({
|
|
91
92
|
0: ALL_OFF,
|
|
92
93
|
1: Object.freeze({ ...ALL_OFF, treeshake: true, sortLocalsByUse: true, fusedRewrite: true }),
|
|
93
|
-
// Default (level 2 / 'balanced'): every stable pass + watr
|
|
94
|
-
// 'light'
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
// deliver most of watr's size win at essentially zero compile cost.
|
|
99
|
-
2: Object.freeze({ ...ALL_ON, watr: 'light', nestedSmallConstForUnroll: 'auto' }),
|
|
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' }),
|
|
100
99
|
// L3/'speed' trades a bit of heap headroom for fewer __arr_grow / __hash growth
|
|
101
100
|
// cycles. arrayMinCap=16 means `[]` and `new Array()` skip the first two doublings
|
|
102
101
|
// (0→2→4→8→16); hashSmallInitCap=8 keeps per-object __dyn_props at the same load
|
|
@@ -104,16 +103,24 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
104
103
|
// Net cost: ~128 B per empty array, ~144 B per per-object hash. Net win on the
|
|
105
104
|
// watr.compile profile: __arr_grow ~6.7% → ~3%, and lower __ihash_get_local
|
|
106
105
|
// probe depth from a denser-load global hash.
|
|
107
|
-
|
|
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 }),
|
|
108
115
|
// 'balanced' = level 2; 'size' tightens scalar/unroll caps; 'speed' = level 3.
|
|
109
|
-
balanced: Object.freeze({ ...ALL_ON,
|
|
116
|
+
balanced: Object.freeze({ ...ALL_ON, nestedSmallConstForUnroll: 'auto' }),
|
|
110
117
|
size: Object.freeze({
|
|
111
118
|
...ALL_ON,
|
|
112
119
|
smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false,
|
|
113
120
|
scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
|
|
114
121
|
}),
|
|
115
|
-
// 'speed' === level 3: full watr (inlining on) + L3 cap/hash tuning.
|
|
116
|
-
speed: Object.freeze({ ...ALL_ON, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
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 }),
|
|
117
124
|
})
|
|
118
125
|
|
|
119
126
|
/**
|
|
@@ -138,11 +145,9 @@ export function resolveOptimize(opt) {
|
|
|
138
145
|
for (const n of PASS_NAMES) {
|
|
139
146
|
if (!(n in opt)) continue
|
|
140
147
|
const v = opt[n]
|
|
141
|
-
// Preserve sentinel
|
|
142
|
-
//
|
|
143
|
-
// watr: 'light' (curated subset — see index.js watrOpts)
|
|
148
|
+
// Preserve sentinel value `nestedSmallConstForUnroll: 'auto'`
|
|
149
|
+
// (resolved by a heuristic at emit time).
|
|
144
150
|
if (n === 'nestedSmallConstForUnroll' && v === 'auto') out[n] = 'auto'
|
|
145
|
-
else if (n === 'watr' && v === 'light') out[n] = 'light'
|
|
146
151
|
else out[n] = !!v
|
|
147
152
|
}
|
|
148
153
|
// Preserve non-pass tuning keys (e.g. plan.js thresholds)
|
|
@@ -867,44 +872,33 @@ export function hoistInvariantCellLoads(fn) {
|
|
|
867
872
|
* — emits 6 f64.load on $p (each of x/y/z twice); collapses to 3 unique loads
|
|
868
873
|
* shared via tee'd snap locals.
|
|
869
874
|
*
|
|
870
|
-
* Safety:
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
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.
|
|
875
882
|
*
|
|
876
883
|
* Region boundaries that flush the table:
|
|
877
884
|
* - branch (br/br_if/br_table/return/unreachable)
|
|
878
885
|
* - non-pure call
|
|
879
886
|
* - loop / if (control flow)
|
|
880
887
|
* - local.set/local.tee on a tracked $X (invalidates that X's entries)
|
|
881
|
-
* - 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)
|
|
882
890
|
* Blocks are treated as transparent — recurse into children.
|
|
883
891
|
*/
|
|
884
892
|
export function cseScalarLoad(fn) {
|
|
885
|
-
// DISABLED: the safety claim above relies on `analyzePtrUnboxable` having vetted
|
|
886
|
-
// every i32 local as a non-aliased fresh-allocation pointer. But this pass scans
|
|
887
|
-
// *all* i32 locals from `(local … i32)` decls — wasm-native i32 scalars (lengths,
|
|
888
|
-
// indices), narrow-ABI helper returns, and analyze.js's new arrayElemSchema-driven
|
|
889
|
-
// unboxes share the same declaration form. The metacircular path (jz-compiled
|
|
890
|
-
// watr.wasm) trips this: CSE'd loads survive across stores that legitimately
|
|
891
|
-
// mutate the same memory through a different i32 local, returning stale bytes
|
|
892
|
-
// (manifests as "memory access out of bounds" once a corrupted offset is
|
|
893
|
-
// dereferenced). Re-enable once candidacy is restricted to vetted pointers
|
|
894
|
-
// (e.g. emit-side annotation or rep-derived whitelist).
|
|
895
|
-
return
|
|
896
893
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
897
894
|
const bodyStart = findBodyStart(fn)
|
|
898
895
|
if (bodyStart < 0) return
|
|
899
896
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
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
|
|
908
902
|
|
|
909
903
|
let snapId = 0
|
|
910
904
|
while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__cs${snapId}`)) snapId++
|
|
@@ -919,10 +913,10 @@ export function cseScalarLoad(fn) {
|
|
|
919
913
|
}
|
|
920
914
|
}
|
|
921
915
|
|
|
922
|
-
// 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.
|
|
923
917
|
const collectGets = (node, out) => {
|
|
924
918
|
if (!Array.isArray(node)) return
|
|
925
|
-
if (node[0] === 'local.get' && typeof node[1] === 'string' &&
|
|
919
|
+
if (node[0] === 'local.get' && typeof node[1] === 'string' && bases.has(node[1])) {
|
|
926
920
|
out.add(node[1])
|
|
927
921
|
return
|
|
928
922
|
}
|
|
@@ -1009,7 +1003,7 @@ export function cseScalarLoad(fn) {
|
|
|
1009
1003
|
const lp = parseLoad(node)
|
|
1010
1004
|
if (lp) {
|
|
1011
1005
|
const addr = node[lp.addrIdx]
|
|
1012
|
-
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])) {
|
|
1013
1007
|
const X = addr[1]
|
|
1014
1008
|
const key = `${X}|${lp.K}`
|
|
1015
1009
|
const entry = table.get(key)
|
|
@@ -1068,8 +1062,16 @@ export function csePureExpr(fn) {
|
|
|
1068
1062
|
const bodyStart = findBodyStart(fn)
|
|
1069
1063
|
if (bodyStart < 0) return
|
|
1070
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.
|
|
1071
1069
|
let snapId = 0
|
|
1072
|
-
|
|
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
|
+
}
|
|
1073
1075
|
const newLocals = []
|
|
1074
1076
|
|
|
1075
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'])
|
|
@@ -1284,14 +1286,17 @@ export function deadStoreElim(fn) {
|
|
|
1284
1286
|
if (!Array.isArray(node)) continue
|
|
1285
1287
|
const op = node[0]
|
|
1286
1288
|
|
|
1287
|
-
// 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.
|
|
1288
1292
|
const reads = new Set()
|
|
1289
1293
|
collectGets(node, reads)
|
|
1290
|
-
if (op === 'local.tee') reads.delete(node[1])
|
|
1291
1294
|
for (const name of reads) lastWrite.delete(name)
|
|
1292
1295
|
|
|
1293
|
-
// Drop of pure expr → dead
|
|
1294
|
-
|
|
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])) {
|
|
1295
1300
|
dead.push({ parent: items, idx: i, drop: true })
|
|
1296
1301
|
}
|
|
1297
1302
|
|
|
@@ -1349,6 +1354,36 @@ export function deadStoreElim(fn) {
|
|
|
1349
1354
|
}
|
|
1350
1355
|
}
|
|
1351
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
|
+
|
|
1352
1387
|
/**
|
|
1353
1388
|
* Promote read-only globals to locals within each function.
|
|
1354
1389
|
*
|
|
@@ -1363,17 +1398,26 @@ export function deadStoreElim(fn) {
|
|
|
1363
1398
|
* also written (global.set) are left untouched — the promotion would be unsound if
|
|
1364
1399
|
* the global changes between reads.
|
|
1365
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
|
+
*
|
|
1366
1407
|
* @param {Array} fn - Function IR (WAT-as-array)
|
|
1367
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)
|
|
1368
1410
|
*/
|
|
1369
|
-
export function promoteGlobals(fn, globalTypes) {
|
|
1411
|
+
export function promoteGlobals(fn, globalTypes, volatileGlobals) {
|
|
1370
1412
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
1371
1413
|
const bodyStart = findBodyStart(fn)
|
|
1372
1414
|
if (bodyStart < 0) return
|
|
1373
1415
|
|
|
1374
|
-
// 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).
|
|
1375
1418
|
const getCounts = new Map() // globalName → count
|
|
1376
1419
|
const written = new Set()
|
|
1420
|
+
let hasCall = false
|
|
1377
1421
|
|
|
1378
1422
|
const scan = (node) => {
|
|
1379
1423
|
if (!Array.isArray(node)) return
|
|
@@ -1387,6 +1431,7 @@ export function promoteGlobals(fn, globalTypes) {
|
|
|
1387
1431
|
if (node[2]) scan(node[2])
|
|
1388
1432
|
return
|
|
1389
1433
|
}
|
|
1434
|
+
if (op === 'call' || op === 'call_indirect') hasCall = true
|
|
1390
1435
|
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
1391
1436
|
}
|
|
1392
1437
|
|
|
@@ -1406,6 +1451,8 @@ export function promoteGlobals(fn, globalTypes) {
|
|
|
1406
1451
|
const replacements = new Map()
|
|
1407
1452
|
for (const [gName, count] of getCounts) {
|
|
1408
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
|
|
1409
1456
|
// Determine type: use provided map, or infer from context
|
|
1410
1457
|
const type = globalTypes?.get(gName) || inferTypeFromContext(fn, gName, bodyStart)
|
|
1411
1458
|
if (!type) continue // can't determine type, skip
|
|
@@ -1872,8 +1919,10 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
|
|
|
1872
1919
|
*
|
|
1873
1920
|
* @param fn func IR node
|
|
1874
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)
|
|
1875
1924
|
*/
|
|
1876
|
-
export function optimizeFunc(fn, cfg, globalTypes) {
|
|
1925
|
+
export function optimizeFunc(fn, cfg, globalTypes, volatileGlobals) {
|
|
1877
1926
|
if (cfg && cfg.hoistPtrType === false &&
|
|
1878
1927
|
cfg.hoistInvariantPtrOffset === false &&
|
|
1879
1928
|
cfg.hoistInvariantPtrOffsetLoop === false &&
|
|
@@ -1898,7 +1947,7 @@ export function optimizeFunc(fn, cfg, globalTypes) {
|
|
|
1898
1947
|
if (!cfg || cfg.csePureExpr !== false) csePureExpr(fn)
|
|
1899
1948
|
if (!cfg || cfg.dropDeadZeroInit !== false) dropDeadZeroInit(fn)
|
|
1900
1949
|
if (!cfg || cfg.deadStoreElim !== false) deadStoreElim(fn)
|
|
1901
|
-
if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes)
|
|
1950
|
+
if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes, volatileGlobals)
|
|
1902
1951
|
// Vectorizer runs PRE-watr unless full watr is enabled (`watr: true`). For full watr,
|
|
1903
1952
|
// defer to post — full passes (notably `inlineOnce` + the post-inline `propagate`
|
|
1904
1953
|
// sweep) reshape the IR so much that pre-watr SIMD patterns get scrambled. Light
|
|
@@ -1966,11 +2015,18 @@ function walkRewrite(node, doInline, counts) {
|
|
|
1966
2015
|
['i64.eq', node[2], ['i64.const', NULL_BITS]],
|
|
1967
2016
|
['i64.eq', node[2], ['i64.const', UNDEF_BITS]]]
|
|
1968
2017
|
if (fname === '$__is_truthy' && Array.isArray(node[2]) && node[2][0] === 'i64.reinterpret_f64'
|
|
1969
|
-
&& Array.isArray(node[2][1]) && node[2][1][0] === 'local.get') {
|
|
1970
|
-
|
|
1971
|
-
|
|
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]
|
|
1972
2028
|
return ['if', ['result', 'i32'],
|
|
1973
|
-
['f64.eq',
|
|
2029
|
+
['f64.eq', first, lget],
|
|
1974
2030
|
['then', ['f64.ne', lget, ['f64.const', 0]]],
|
|
1975
2031
|
['else', ['i32.and',
|
|
1976
2032
|
['i32.and',
|
|
@@ -2008,70 +2064,18 @@ function walkRewrite(node, doInline, counts) {
|
|
|
2008
2064
|
if (Array.isArray(a) && a[0] === 'f64.convert_i32_s' && a.length === 2) return ['i64.extend_i32_s', a[1]]
|
|
2009
2065
|
if (Array.isArray(a) && a[0] === 'f64.convert_i32_u' && a.length === 2) return ['i64.extend_i32_u', a[1]]
|
|
2010
2066
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
if (op === 'f64.reinterpret_i64'
|
|
2016
|
-
const
|
|
2017
|
-
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
|
|
2018
2074
|
}
|
|
2019
2075
|
if (op === 'i32.wrap_i64' && node.length === 2) {
|
|
2020
2076
|
const a = node[1]
|
|
2021
2077
|
if (Array.isArray(a) && (a[0] === 'i64.extend_i32_u' || a[0] === 'i64.extend_i32_s') && a.length === 2)
|
|
2022
2078
|
return a[1]
|
|
2023
|
-
// (i32.wrap_i64 (i64.reinterpret_f64 (f64.load ADDR ?offset))) → (i32.load ADDR ?offset).
|
|
2024
|
-
// Wasm is little-endian; the low 32 bits of the f64 at ADDR are exactly
|
|
2025
|
-
// i32.load(ADDR). Saves two ops on every NaN-box pointer extraction from
|
|
2026
|
-
// an array slot or struct field.
|
|
2027
|
-
if (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) {
|
|
2028
|
-
const inner = a[1]
|
|
2029
|
-
if (Array.isArray(inner) && inner[0] === 'f64.load') {
|
|
2030
|
-
const out = ['i32.load']
|
|
2031
|
-
for (let i = 1; i < inner.length; i++) out.push(inner[i])
|
|
2032
|
-
return out
|
|
2033
|
-
}
|
|
2034
|
-
// (i32.wrap_i64 (i64.reinterpret_f64 (call $__mkptr* … offset))) → offset.
|
|
2035
|
-
// A NaN-boxed pointer keeps type/aux in the high bits and the i32 offset in
|
|
2036
|
-
// the low 32, so the round-trip through f64 is pure overhead whenever the
|
|
2037
|
-
// consumer only wants the offset (typical when the pointer feeds an unboxed
|
|
2038
|
-
// i32 local). Covers the generic 3-arg `$__mkptr` and the specialized
|
|
2039
|
-
// single-arg `$__mkptr_T_A_d` trampolines alike — offset is the last arg.
|
|
2040
|
-
const isMkptr = n => Array.isArray(n) && n[0] === 'call' && typeof n[1] === 'string'
|
|
2041
|
-
&& (n[1] === '$__mkptr' || n[1].startsWith('$__mkptr_'))
|
|
2042
|
-
if (isMkptr(inner)) return inner[inner.length - 1]
|
|
2043
|
-
// …and reach through a `(block (result f64) …stmts (call $__mkptr …))` —
|
|
2044
|
-
// `new TypedArray(n)` lowers to exactly this shape — by retyping the block
|
|
2045
|
-
// to i32 and dropping the box on its tail.
|
|
2046
|
-
if (Array.isArray(inner) && inner[0] === 'block' && isMkptr(inner[inner.length - 1])) {
|
|
2047
|
-
let ri = -1
|
|
2048
|
-
for (let i = 1; i <= 2 && i < inner.length; i++)
|
|
2049
|
-
if (Array.isArray(inner[i]) && inner[i][0] === 'result') { ri = i; break }
|
|
2050
|
-
if (ri >= 0 && inner[ri][1] === 'f64') {
|
|
2051
|
-
const tail = inner[inner.length - 1]
|
|
2052
|
-
const nb = inner.slice()
|
|
2053
|
-
nb[ri] = ['result', 'i32']
|
|
2054
|
-
nb[nb.length - 1] = tail[tail.length - 1]
|
|
2055
|
-
return nb
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
if (Array.isArray(a) && a[0] === 'i64.or' && a.length === 3) {
|
|
2060
|
-
const l = a[1], r = a[2]
|
|
2061
|
-
const isHighOnly = (n) => {
|
|
2062
|
-
if (!Array.isArray(n) || n[0] !== 'i64.const') return false
|
|
2063
|
-
const v = n[1]
|
|
2064
|
-
let bi
|
|
2065
|
-
if (typeof v === 'number') bi = BigInt(v)
|
|
2066
|
-
else if (typeof v === 'string') {
|
|
2067
|
-
try { bi = v.startsWith('-') ? -BigInt(v.slice(1)) : BigInt(v) } catch { return false }
|
|
2068
|
-
} else return false
|
|
2069
|
-
return (bi & 0xFFFFFFFFn) === 0n
|
|
2070
|
-
}
|
|
2071
|
-
const isExtend = (n) => Array.isArray(n) && (n[0] === 'i64.extend_i32_u' || n[0] === 'i64.extend_i32_s') && n.length === 2
|
|
2072
|
-
if (isHighOnly(l) && isExtend(r)) return r[1]
|
|
2073
|
-
if (isHighOnly(r) && isExtend(l)) return l[1]
|
|
2074
|
-
}
|
|
2075
2079
|
}
|
|
2076
2080
|
|
|
2077
2081
|
// shl-distribute-over-add: (i32.shl (i32.add x (i32.const K)) (i32.const S))
|