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