watr 4.3.3 → 4.4.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/dist/watr.js +1198 -432
- package/dist/watr.min.js +6 -6
- package/package.json +1 -1
- package/src/optimize.js +1427 -532
- package/types/src/optimize.d.ts +94 -5
- package/types/src/optimize.d.ts.map +1 -1
- package/watr.js +2 -2
package/src/optimize.js
CHANGED
|
@@ -18,11 +18,40 @@ const OPTS = {
|
|
|
18
18
|
branch: true, // simplify constant branches
|
|
19
19
|
propagate: true, // constant propagation through locals
|
|
20
20
|
inline: true, // inline tiny functions
|
|
21
|
+
vacuum: true, // remove nops, drop-of-pure, empty branches
|
|
22
|
+
peephole: true, // x-x→0, x&0→0, etc.
|
|
23
|
+
globals: true, // propagate immutable global constants
|
|
24
|
+
offset: true, // fold add+const into load/store offset
|
|
25
|
+
unbranch: true, // remove redundant br at end of own block
|
|
26
|
+
stripmut: true, // strip mut from never-written globals
|
|
27
|
+
brif: true, // if-then-br → br_if
|
|
28
|
+
foldarms: true, // merge identical trailing if arms
|
|
29
|
+
// minify: true, // NOTE: disabled — renaming $ids has no binary-size effect
|
|
30
|
+
// without a names section, and risks local-name collisions.
|
|
31
|
+
dedupe: true, // eliminate duplicate functions
|
|
32
|
+
reorder: true, // put hot functions first for smaller LEBs
|
|
33
|
+
dedupTypes: true, // merge identical type definitions
|
|
34
|
+
packData: true, // trim trailing zeros, merge adjacent data segments
|
|
35
|
+
minifyImports: false, // shorten import names — enable only when you control the host
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
/** All optimization names */
|
|
24
39
|
const ALL = Object.keys(OPTS)
|
|
25
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Fast structural equality of two AST nodes.
|
|
43
|
+
* Stops at first difference. Handles BigInt without stringification.
|
|
44
|
+
*/
|
|
45
|
+
const equal = (a, b) => {
|
|
46
|
+
if (a === b) return true
|
|
47
|
+
if (typeof a !== typeof b) return false
|
|
48
|
+
if (typeof a === 'bigint') return a === b
|
|
49
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
|
50
|
+
if (a.length !== b.length) return false
|
|
51
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
/**
|
|
27
56
|
* Normalize options to { opt: bool } map.
|
|
28
57
|
* @param {boolean|string|Object} opts
|
|
@@ -33,7 +62,8 @@ const normalize = (opts) => {
|
|
|
33
62
|
if (opts === false) return {}
|
|
34
63
|
if (typeof opts === 'string') {
|
|
35
64
|
const set = new Set(opts.split(/\s+/).filter(Boolean))
|
|
36
|
-
//
|
|
65
|
+
// Special case: a single explicit pass name (e.g. 'fold') enables only that pass,
|
|
66
|
+
// rather than treating it as a sparse map where everything else is disabled.
|
|
37
67
|
if (set.size === 1 && ALL.includes([...set][0])) {
|
|
38
68
|
return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
|
|
39
69
|
}
|
|
@@ -83,6 +113,34 @@ const walkPost = (node, fn, parent, idx) => {
|
|
|
83
113
|
return result !== undefined ? result : node
|
|
84
114
|
}
|
|
85
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Locate the parts of an `(if ...)` node:
|
|
118
|
+
* condIdx → index of the condition expression
|
|
119
|
+
* cond → the condition expression itself
|
|
120
|
+
* thenBranch / elseBranch → the (then ...) / (else ...) sub-arrays, or null
|
|
121
|
+
* The condition sits after any leading `param`/`result` annotations and before
|
|
122
|
+
* the `then`/`else` arms.
|
|
123
|
+
*/
|
|
124
|
+
const parseIf = (node) => {
|
|
125
|
+
let condIdx = 1
|
|
126
|
+
while (condIdx < node.length) {
|
|
127
|
+
const c = node[condIdx]
|
|
128
|
+
if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else' || c[0] === 'result' || c[0] === 'param')) {
|
|
129
|
+
condIdx++
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
let thenBranch = null, elseBranch = null
|
|
135
|
+
for (let i = condIdx + 1; i < node.length; i++) {
|
|
136
|
+
const c = node[i]
|
|
137
|
+
if (!Array.isArray(c)) continue
|
|
138
|
+
if (c[0] === 'then') thenBranch = c
|
|
139
|
+
else if (c[0] === 'else') elseBranch = c
|
|
140
|
+
}
|
|
141
|
+
return { condIdx, cond: node[condIdx], thenBranch, elseBranch }
|
|
142
|
+
}
|
|
143
|
+
|
|
86
144
|
// ==================== TREESHAKE ====================
|
|
87
145
|
|
|
88
146
|
/**
|
|
@@ -94,283 +152,247 @@ const walkPost = (node, fn, parent, idx) => {
|
|
|
94
152
|
const treeshake = (ast) => {
|
|
95
153
|
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
96
154
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const types = new Map()
|
|
101
|
-
const tables = new Map()
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
155
|
+
// Index spaces. Each entry is shared between its $name key and its numeric idx
|
|
156
|
+
// key, so name/index lookups hit the same record. nodeMap covers reverse lookup
|
|
157
|
+
// (used during the filtering pass for unnamed definitions).
|
|
158
|
+
const funcs = new Map(), globals = new Map(), types = new Map()
|
|
159
|
+
const tables = new Map(), memories = new Map()
|
|
160
|
+
const nodeMap = new Map() // node → entry
|
|
161
|
+
|
|
162
|
+
const register = (map, node, idx, isImport = false) => {
|
|
163
|
+
const named = typeof node[1] === 'string' && node[1][0] === '$'
|
|
164
|
+
const name = named ? node[1] : idx
|
|
165
|
+
const inlineExported = !isImport && node.some(s => Array.isArray(s) && s[0] === 'export')
|
|
166
|
+
const entry = { node, idx, used: inlineExported, isImport }
|
|
167
|
+
map.set(name, entry)
|
|
168
|
+
if (named) map.set(idx, entry)
|
|
169
|
+
nodeMap.set(node, entry)
|
|
170
|
+
return entry
|
|
171
|
+
}
|
|
105
172
|
|
|
106
|
-
let funcIdx = 0, globalIdx = 0, typeIdx = 0, tableIdx = 0, memIdx = 0
|
|
173
|
+
let funcIdx = 0, globalIdx = 0, typeIdx = 0, tableIdx = 0, memIdx = 0
|
|
174
|
+
const elems = [], exports = [], starts = []
|
|
107
175
|
|
|
108
176
|
for (const node of ast.slice(1)) {
|
|
109
177
|
if (!Array.isArray(node)) continue
|
|
110
178
|
const kind = node[0]
|
|
111
|
-
|
|
112
|
-
if (kind === '
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
typeIdx++
|
|
117
|
-
}
|
|
118
|
-
else if (kind === 'func') {
|
|
119
|
-
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : funcIdx
|
|
120
|
-
// Check for inline export: (func $name (export "...") ...)
|
|
121
|
-
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
122
|
-
funcs.set(name, { node, idx: funcIdx, used: hasInlineExport })
|
|
123
|
-
if (typeof name === 'string') funcs.set(funcIdx, funcs.get(name))
|
|
124
|
-
funcIdx++
|
|
125
|
-
}
|
|
126
|
-
else if (kind === 'global') {
|
|
127
|
-
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : globalIdx
|
|
128
|
-
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
129
|
-
globals.set(name, { node, idx: globalIdx, used: hasInlineExport })
|
|
130
|
-
if (typeof name === 'string') globals.set(globalIdx, globals.get(name))
|
|
131
|
-
globalIdx++
|
|
132
|
-
}
|
|
133
|
-
else if (kind === 'table') {
|
|
134
|
-
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : tableIdx
|
|
135
|
-
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
136
|
-
tables.set(name, { node, idx: tableIdx, used: hasInlineExport })
|
|
137
|
-
if (typeof name === 'string') tables.set(tableIdx, tables.get(name))
|
|
138
|
-
tableIdx++
|
|
139
|
-
}
|
|
140
|
-
else if (kind === 'memory') {
|
|
141
|
-
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : memIdx
|
|
142
|
-
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
143
|
-
memories.set(name, { node, idx: memIdx, used: hasInlineExport })
|
|
144
|
-
if (typeof name === 'string') memories.set(memIdx, memories.get(name))
|
|
145
|
-
memIdx++
|
|
146
|
-
}
|
|
179
|
+
if (kind === 'type') register(types, node, typeIdx++)
|
|
180
|
+
else if (kind === 'func') register(funcs, node, funcIdx++)
|
|
181
|
+
else if (kind === 'global') register(globals, node, globalIdx++)
|
|
182
|
+
else if (kind === 'table') register(tables, node, tableIdx++)
|
|
183
|
+
else if (kind === 'memory') register(memories, node, memIdx++)
|
|
147
184
|
else if (kind === 'import') {
|
|
148
|
-
//
|
|
185
|
+
// Each import sub-item occupies its own slot in the relevant index space.
|
|
149
186
|
for (const sub of node) {
|
|
150
|
-
if (Array.isArray(sub)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
funcIdx++
|
|
156
|
-
}
|
|
187
|
+
if (!Array.isArray(sub)) continue
|
|
188
|
+
if (sub[0] === 'func') register(funcs, sub, funcIdx++, true)
|
|
189
|
+
else if (sub[0] === 'global') register(globals, sub, globalIdx++, true)
|
|
190
|
+
else if (sub[0] === 'table') register(tables, sub, tableIdx++, true)
|
|
191
|
+
else if (sub[0] === 'memory') register(memories, sub, memIdx++, true)
|
|
157
192
|
}
|
|
158
193
|
}
|
|
159
|
-
else if (kind === 'export')
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
else if (kind === 'export') exports.push(node)
|
|
195
|
+
else if (kind === 'start') starts.push(node)
|
|
196
|
+
else if (kind === 'elem') elems.push(node)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Worklist: function entries whose body still needs to be scanned for refs.
|
|
200
|
+
const work = []
|
|
201
|
+
const enqueue = (entry) => { if (entry && !entry.scanned) work.push(entry) }
|
|
202
|
+
const markFunc = (ref) => {
|
|
203
|
+
const e = funcs.get(ref); if (!e) return
|
|
204
|
+
if (!e.used) e.used = true
|
|
205
|
+
enqueue(e)
|
|
165
206
|
}
|
|
207
|
+
const markGlobal = (ref) => { const e = globals.get(ref); if (e) e.used = true }
|
|
208
|
+
const markTable = (ref) => { const e = tables.get(ref); if (e) e.used = true }
|
|
209
|
+
const markMemory = (ref) => { const e = memories.get(ref); if (e) e.used = true }
|
|
210
|
+
const markType = (ref) => { const e = types.get(ref); if (e) e.used = true }
|
|
166
211
|
|
|
167
|
-
//
|
|
212
|
+
// Roots: explicit exports, start funcs, elem-referenced funcs, inline-exported items.
|
|
168
213
|
for (const exp of exports) {
|
|
169
214
|
for (const sub of exp) {
|
|
170
215
|
if (!Array.isArray(sub)) continue
|
|
171
216
|
const [kind, ref] = sub
|
|
172
|
-
if (kind === 'func'
|
|
173
|
-
else if (kind === 'global'
|
|
174
|
-
else if (kind === 'table'
|
|
175
|
-
else if (kind === 'memory'
|
|
217
|
+
if (kind === 'func') markFunc(ref)
|
|
218
|
+
else if (kind === 'global') markGlobal(ref)
|
|
219
|
+
else if (kind === 'table') markTable(ref)
|
|
220
|
+
else if (kind === 'memory') markMemory(ref)
|
|
176
221
|
}
|
|
177
222
|
}
|
|
178
|
-
|
|
179
|
-
// Mark start function as used
|
|
180
223
|
for (const start of starts) {
|
|
181
224
|
let ref = start[1]
|
|
182
|
-
// Convert numeric string refs to numbers
|
|
183
225
|
if (typeof ref === 'string' && ref[0] !== '$') ref = +ref
|
|
184
|
-
|
|
226
|
+
markFunc(ref)
|
|
185
227
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (!hasExports) for (const [, entry] of globals) if (entry.used) { hasExports = true; break }
|
|
192
|
-
if (!hasExports) for (const [, entry] of tables) if (entry.used) { hasExports = true; break }
|
|
193
|
-
if (!hasExports) for (const [, entry] of memories) if (entry.used) { hasExports = true; break }
|
|
228
|
+
for (const elem of elems) {
|
|
229
|
+
walk(elem, n => {
|
|
230
|
+
if (Array.isArray(n) && n[0] === 'ref.func') markFunc(n[1])
|
|
231
|
+
else if (typeof n === 'string' && n[0] === '$') markFunc(n)
|
|
232
|
+
})
|
|
194
233
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const [,
|
|
234
|
+
for (const m of [funcs, globals, tables, memories]) for (const e of m.values()) if (e.used) enqueue(e)
|
|
235
|
+
|
|
236
|
+
// If nothing anchors the module (no exports, start, elem, or inline exports),
|
|
237
|
+
// assume the module is consumed elsewhere and keep everything.
|
|
238
|
+
const hasAnchor = exports.length > 0 || starts.length > 0 || elems.length > 0 || work.length > 0
|
|
239
|
+
if (!hasAnchor) {
|
|
240
|
+
for (const m of [funcs, globals, tables, memories]) for (const e of m.values()) e.used = true
|
|
241
|
+
return ast
|
|
202
242
|
}
|
|
203
243
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
// Drain worklist: each function body gets walked exactly once.
|
|
245
|
+
while (work.length) {
|
|
246
|
+
const entry = work.pop()
|
|
247
|
+
if (entry.scanned) continue
|
|
248
|
+
entry.scanned = true
|
|
249
|
+
if (entry.isImport) continue
|
|
250
|
+
walk(entry.node, n => {
|
|
251
|
+
if (!Array.isArray(n)) {
|
|
252
|
+
if (typeof n === 'string' && n[0] === '$') markFunc(n)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const [op, ref] = n
|
|
256
|
+
if (op === 'call' || op === 'return_call' || op === 'ref.func') markFunc(ref)
|
|
257
|
+
else if (op === 'global.get' || op === 'global.set') markGlobal(ref)
|
|
258
|
+
else if (op === 'type') markType(ref)
|
|
259
|
+
else if (op === 'call_indirect' || op === 'return_call_indirect') {
|
|
260
|
+
for (const sub of n) if (typeof sub === 'string' && sub[0] === '$') markTable(sub)
|
|
211
261
|
}
|
|
212
|
-
// Also plain func refs in elem
|
|
213
|
-
if (typeof n === 'string' && n[0] === '$' && funcs.has(n)) funcs.get(n).used = true
|
|
214
262
|
})
|
|
215
263
|
}
|
|
216
264
|
|
|
217
|
-
//
|
|
218
|
-
let changed = true
|
|
219
|
-
while (changed) {
|
|
220
|
-
changed = false
|
|
221
|
-
for (const [, entry] of funcs) {
|
|
222
|
-
if (!entry.used || entry.isImport) continue
|
|
223
|
-
walk(entry.node, n => {
|
|
224
|
-
if (!Array.isArray(n)) {
|
|
225
|
-
// Direct func reference
|
|
226
|
-
if (typeof n === 'string' && n[0] === '$' && funcs.has(n) && !funcs.get(n).used) {
|
|
227
|
-
funcs.get(n).used = true
|
|
228
|
-
changed = true
|
|
229
|
-
}
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
const [op, ref] = n
|
|
233
|
-
if ((op === 'call' || op === 'return_call' || op === 'ref.func') && funcs.has(ref) && !funcs.get(ref).used) {
|
|
234
|
-
funcs.get(ref).used = true
|
|
235
|
-
changed = true
|
|
236
|
-
}
|
|
237
|
-
if ((op === 'global.get' || op === 'global.set') && globals.has(ref) && !globals.get(ref).used) {
|
|
238
|
-
globals.get(ref).used = true
|
|
239
|
-
changed = true
|
|
240
|
-
}
|
|
241
|
-
if (op === 'call_indirect' || op === 'return_call_indirect') {
|
|
242
|
-
// Tables used by call_indirect
|
|
243
|
-
for (const sub of n) {
|
|
244
|
-
if (typeof sub === 'string' && sub[0] === '$' && tables.has(sub) && !tables.get(sub).used) {
|
|
245
|
-
tables.get(sub).used = true
|
|
246
|
-
changed = true
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (op === 'type' && types.has(ref) && !types.get(ref).used) {
|
|
251
|
-
types.get(ref).used = true
|
|
252
|
-
changed = true
|
|
253
|
-
}
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Filter AST keeping only used items
|
|
265
|
+
// Filter: keep used definitions. nodeMap handles unnamed entries directly.
|
|
259
266
|
const result = ['module']
|
|
260
267
|
for (const node of ast.slice(1)) {
|
|
261
268
|
if (!Array.isArray(node)) { result.push(node); continue }
|
|
262
269
|
const kind = node[0]
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
else
|
|
275
|
-
// Keep all types for now (complex to treeshake due to inline types)
|
|
276
|
-
result.push(node)
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
270
|
+
if (kind === 'func' || kind === 'global' || kind === 'type') {
|
|
271
|
+
if (nodeMap.get(node)?.used) result.push(node)
|
|
272
|
+
} else if (kind === 'import') {
|
|
273
|
+
// Keep import only if any of its sub-items is used.
|
|
274
|
+
let used = false
|
|
275
|
+
for (const sub of node) {
|
|
276
|
+
if (!Array.isArray(sub)) continue
|
|
277
|
+
const e = nodeMap.get(sub)
|
|
278
|
+
if (e?.used) { used = true; break }
|
|
279
|
+
}
|
|
280
|
+
if (used) result.push(node)
|
|
281
|
+
} else {
|
|
279
282
|
result.push(node)
|
|
280
283
|
}
|
|
281
284
|
}
|
|
282
|
-
|
|
283
285
|
return result
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
// ==================== CONSTANT FOLDING ====================
|
|
287
289
|
|
|
288
|
-
/**
|
|
290
|
+
/** IEEE 754 roundTiesToEven (bankers' rounding) */
|
|
291
|
+
const roundEven = (x) => x - Math.floor(x) !== 0.5 ? Math.round(x) : 2 * Math.round(x / 2)
|
|
292
|
+
|
|
293
|
+
/** Build i32 comparison folder: returns 1/0 */
|
|
294
|
+
const i32c = (fn) => (a, b) => fn(a, b) ? 1 : 0
|
|
295
|
+
/** Build unsigned i32 comparison folder */
|
|
296
|
+
const u32c = (fn) => (a, b) => fn(a >>> 0, b >>> 0) ? 1 : 0
|
|
297
|
+
/** Build i64 comparison folder */
|
|
298
|
+
const i64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
|
|
299
|
+
/** Build unsigned i64 comparison folder */
|
|
300
|
+
const u64c = (fn) => (a, b) => fn(BigInt.asUintN(64, a), BigInt.asUintN(64, b)) ? 1 : 0
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Constant folders, keyed by op. Each entry is [fn, resultType].
|
|
304
|
+
* Comparisons return i32, conversions return their named output type.
|
|
305
|
+
*/
|
|
289
306
|
const FOLDABLE = {
|
|
290
|
-
// i32
|
|
291
|
-
'i32.add': (a, b) => (a + b) | 0,
|
|
292
|
-
'i32.sub': (a, b) => (a - b) | 0,
|
|
293
|
-
'i32.mul': (a, b) => Math.imul(a, b),
|
|
294
|
-
'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
|
|
295
|
-
'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
|
|
296
|
-
'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
|
|
297
|
-
'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
|
|
298
|
-
'i32.and': (a, b) => a & b,
|
|
299
|
-
'i32.or':
|
|
300
|
-
'i32.xor': (a, b) => a ^ b,
|
|
301
|
-
'i32.shl':
|
|
302
|
-
'i32.shr_s': (a, b) => a >> (b & 31),
|
|
303
|
-
'i32.shr_u': (a, b) => a >>> (b & 31),
|
|
304
|
-
'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
|
|
305
|
-
'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
|
|
306
|
-
'i32.eq':
|
|
307
|
-
'i32.ne':
|
|
308
|
-
'i32.lt_s': (a, b) => a < b
|
|
309
|
-
'i32.lt_u': (a, b) =>
|
|
310
|
-
'i32.gt_s': (a, b) => a > b
|
|
311
|
-
'i32.gt_u': (a, b) =>
|
|
312
|
-
'i32.le_s': (a, b) => a <= b
|
|
313
|
-
'i32.le_u': (a, b) =>
|
|
314
|
-
'i32.ge_s': (a, b) => a >= b
|
|
315
|
-
'i32.ge_u': (a, b) =>
|
|
316
|
-
'i32.eqz':
|
|
317
|
-
'i32.clz':
|
|
318
|
-
'i32.ctz':
|
|
319
|
-
'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
|
|
320
|
-
'i32.wrap_i64':
|
|
307
|
+
// i32 arithmetic
|
|
308
|
+
'i32.add': [(a, b) => (a + b) | 0, 'i32'],
|
|
309
|
+
'i32.sub': [(a, b) => (a - b) | 0, 'i32'],
|
|
310
|
+
'i32.mul': [(a, b) => Math.imul(a, b), 'i32'],
|
|
311
|
+
'i32.div_s': [(a, b) => b !== 0 ? (a / b) | 0 : null, 'i32'],
|
|
312
|
+
'i32.div_u': [(a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null, 'i32'],
|
|
313
|
+
'i32.rem_s': [(a, b) => b !== 0 ? (a % b) | 0 : null, 'i32'],
|
|
314
|
+
'i32.rem_u': [(a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null, 'i32'],
|
|
315
|
+
'i32.and': [(a, b) => a & b, 'i32'],
|
|
316
|
+
'i32.or': [(a, b) => a | b, 'i32'],
|
|
317
|
+
'i32.xor': [(a, b) => a ^ b, 'i32'],
|
|
318
|
+
'i32.shl': [(a, b) => a << (b & 31), 'i32'],
|
|
319
|
+
'i32.shr_s': [(a, b) => a >> (b & 31), 'i32'],
|
|
320
|
+
'i32.shr_u': [(a, b) => a >>> (b & 31), 'i32'],
|
|
321
|
+
'i32.rotl': [(a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 }, 'i32'],
|
|
322
|
+
'i32.rotr': [(a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 }, 'i32'],
|
|
323
|
+
'i32.eq': [i32c((a, b) => a === b), 'i32'],
|
|
324
|
+
'i32.ne': [i32c((a, b) => a !== b), 'i32'],
|
|
325
|
+
'i32.lt_s': [i32c((a, b) => a < b), 'i32'],
|
|
326
|
+
'i32.lt_u': [u32c((a, b) => a < b), 'i32'],
|
|
327
|
+
'i32.gt_s': [i32c((a, b) => a > b), 'i32'],
|
|
328
|
+
'i32.gt_u': [u32c((a, b) => a > b), 'i32'],
|
|
329
|
+
'i32.le_s': [i32c((a, b) => a <= b), 'i32'],
|
|
330
|
+
'i32.le_u': [u32c((a, b) => a <= b), 'i32'],
|
|
331
|
+
'i32.ge_s': [i32c((a, b) => a >= b), 'i32'],
|
|
332
|
+
'i32.ge_u': [u32c((a, b) => a >= b), 'i32'],
|
|
333
|
+
'i32.eqz': [(a) => a === 0 ? 1 : 0, 'i32'],
|
|
334
|
+
'i32.clz': [(a) => Math.clz32(a), 'i32'],
|
|
335
|
+
'i32.ctz': [(a) => a === 0 ? 32 : 31 - Math.clz32(a & -a), 'i32'],
|
|
336
|
+
'i32.popcnt': [(a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c }, 'i32'],
|
|
337
|
+
'i32.wrap_i64': [(a) => Number(BigInt.asIntN(32, a)), 'i32'],
|
|
338
|
+
'i32.extend8_s': [(a) => (a << 24) >> 24, 'i32'],
|
|
339
|
+
'i32.extend16_s': [(a) => (a << 16) >> 16, 'i32'],
|
|
321
340
|
|
|
322
341
|
// i64 (using BigInt)
|
|
323
|
-
'i64.add': (a, b) => BigInt.asIntN(64, a + b),
|
|
324
|
-
'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
|
|
325
|
-
'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
|
|
326
|
-
'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
|
|
327
|
-
'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
|
|
328
|
-
'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
|
|
329
|
-
'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
|
|
330
|
-
'i64.and': (a, b) => BigInt.asIntN(64, a & b),
|
|
331
|
-
'i64.or':
|
|
332
|
-
'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
|
|
333
|
-
'i64.shl':
|
|
334
|
-
'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
|
|
335
|
-
'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
|
|
336
|
-
'i64.eq':
|
|
337
|
-
'i64.ne':
|
|
338
|
-
'i64.lt_s': (a, b) => a < b
|
|
339
|
-
'i64.lt_u': (a, b) =>
|
|
340
|
-
'i64.gt_s': (a, b) => a > b
|
|
341
|
-
'i64.gt_u': (a, b) =>
|
|
342
|
-
'i64.le_s': (a, b) => a <= b
|
|
343
|
-
'i64.le_u': (a, b) =>
|
|
344
|
-
'i64.ge_s': (a, b) => a >= b
|
|
345
|
-
'i64.ge_u': (a, b) =>
|
|
346
|
-
'i64.eqz': (a) => a === 0n ? 1 : 0,
|
|
347
|
-
'i64.extend_i32_s': (a) => BigInt(a),
|
|
348
|
-
'i64.extend_i32_u': (a) => BigInt(a >>> 0),
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
'
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
'f32.
|
|
355
|
-
'f32.
|
|
356
|
-
'f32.
|
|
357
|
-
'f32.
|
|
358
|
-
'f32.
|
|
359
|
-
'f32.
|
|
360
|
-
'f32.
|
|
361
|
-
'f32.
|
|
362
|
-
|
|
363
|
-
'
|
|
364
|
-
'
|
|
365
|
-
|
|
366
|
-
'f64.
|
|
367
|
-
'f64.
|
|
368
|
-
'f64.
|
|
369
|
-
'f64.
|
|
370
|
-
'f64.
|
|
371
|
-
'f64.
|
|
372
|
-
'f64.
|
|
373
|
-
'f64.
|
|
342
|
+
'i64.add': [(a, b) => BigInt.asIntN(64, a + b), 'i64'],
|
|
343
|
+
'i64.sub': [(a, b) => BigInt.asIntN(64, a - b), 'i64'],
|
|
344
|
+
'i64.mul': [(a, b) => BigInt.asIntN(64, a * b), 'i64'],
|
|
345
|
+
'i64.div_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null, 'i64'],
|
|
346
|
+
'i64.div_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null, 'i64'],
|
|
347
|
+
'i64.rem_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null, 'i64'],
|
|
348
|
+
'i64.rem_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null, 'i64'],
|
|
349
|
+
'i64.and': [(a, b) => BigInt.asIntN(64, a & b), 'i64'],
|
|
350
|
+
'i64.or': [(a, b) => BigInt.asIntN(64, a | b), 'i64'],
|
|
351
|
+
'i64.xor': [(a, b) => BigInt.asIntN(64, a ^ b), 'i64'],
|
|
352
|
+
'i64.shl': [(a, b) => BigInt.asIntN(64, a << (b & 63n)), 'i64'],
|
|
353
|
+
'i64.shr_s': [(a, b) => BigInt.asIntN(64, a >> (b & 63n)), 'i64'],
|
|
354
|
+
'i64.shr_u': [(a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)), 'i64'],
|
|
355
|
+
'i64.eq': [i64c((a, b) => a === b), 'i32'],
|
|
356
|
+
'i64.ne': [i64c((a, b) => a !== b), 'i32'],
|
|
357
|
+
'i64.lt_s': [i64c((a, b) => a < b), 'i32'],
|
|
358
|
+
'i64.lt_u': [u64c((a, b) => a < b), 'i32'],
|
|
359
|
+
'i64.gt_s': [i64c((a, b) => a > b), 'i32'],
|
|
360
|
+
'i64.gt_u': [u64c((a, b) => a > b), 'i32'],
|
|
361
|
+
'i64.le_s': [i64c((a, b) => a <= b), 'i32'],
|
|
362
|
+
'i64.le_u': [u64c((a, b) => a <= b), 'i32'],
|
|
363
|
+
'i64.ge_s': [i64c((a, b) => a >= b), 'i32'],
|
|
364
|
+
'i64.ge_u': [u64c((a, b) => a >= b), 'i32'],
|
|
365
|
+
'i64.eqz': [(a) => a === 0n ? 1 : 0, 'i32'],
|
|
366
|
+
'i64.extend_i32_s': [(a) => BigInt(a), 'i64'],
|
|
367
|
+
'i64.extend_i32_u': [(a) => BigInt(a >>> 0), 'i64'],
|
|
368
|
+
'i64.extend8_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(8, a)), 'i64'],
|
|
369
|
+
'i64.extend16_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(16, a)), 'i64'],
|
|
370
|
+
'i64.extend32_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(32, a)), 'i64'],
|
|
371
|
+
|
|
372
|
+
// f32/f64 (NaN/precision-aware via Math.fround)
|
|
373
|
+
'f32.add': [(a, b) => Math.fround(a + b), 'f32'],
|
|
374
|
+
'f32.sub': [(a, b) => Math.fround(a - b), 'f32'],
|
|
375
|
+
'f32.mul': [(a, b) => Math.fround(a * b), 'f32'],
|
|
376
|
+
'f32.div': [(a, b) => Math.fround(a / b), 'f32'],
|
|
377
|
+
'f32.neg': [(a) => Math.fround(-a), 'f32'],
|
|
378
|
+
'f32.abs': [(a) => Math.fround(Math.abs(a)), 'f32'],
|
|
379
|
+
'f32.sqrt': [(a) => Math.fround(Math.sqrt(a)), 'f32'],
|
|
380
|
+
'f32.ceil': [(a) => Math.fround(Math.ceil(a)), 'f32'],
|
|
381
|
+
'f32.floor': [(a) => Math.fround(Math.floor(a)), 'f32'],
|
|
382
|
+
'f32.trunc': [(a) => Math.fround(Math.trunc(a)), 'f32'],
|
|
383
|
+
'f32.nearest': [(a) => Math.fround(roundEven(a)), 'f32'],
|
|
384
|
+
|
|
385
|
+
'f64.add': [(a, b) => a + b, 'f64'],
|
|
386
|
+
'f64.sub': [(a, b) => a - b, 'f64'],
|
|
387
|
+
'f64.mul': [(a, b) => a * b, 'f64'],
|
|
388
|
+
'f64.div': [(a, b) => a / b, 'f64'],
|
|
389
|
+
'f64.neg': [(a) => -a, 'f64'],
|
|
390
|
+
'f64.abs': [Math.abs, 'f64'],
|
|
391
|
+
'f64.sqrt': [Math.sqrt, 'f64'],
|
|
392
|
+
'f64.ceil': [Math.ceil, 'f64'],
|
|
393
|
+
'f64.floor': [Math.floor, 'f64'],
|
|
394
|
+
'f64.trunc': [Math.trunc, 'f64'],
|
|
395
|
+
'f64.nearest': [roundEven, 'f64'],
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
/**
|
|
@@ -410,124 +432,80 @@ const makeConst = (type, value) => {
|
|
|
410
432
|
const fold = (ast) => {
|
|
411
433
|
return walkPost(clone(ast), (node) => {
|
|
412
434
|
if (!Array.isArray(node)) return
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
435
|
+
const entry = FOLDABLE[node[0]]
|
|
436
|
+
if (!entry) return
|
|
437
|
+
const [fn, t] = entry
|
|
416
438
|
|
|
417
|
-
// Unary
|
|
439
|
+
// Unary
|
|
418
440
|
if (fn.length === 1 && node.length === 2) {
|
|
419
441
|
const a = getConst(node[1])
|
|
420
442
|
if (!a) return
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
op.startsWith('f32.') ? 'f32' :
|
|
425
|
-
op.startsWith('f64.') ? 'f64' : 'i32'
|
|
426
|
-
return makeConst(resultType, result)
|
|
443
|
+
const r = fn(a.value)
|
|
444
|
+
if (r === null) return
|
|
445
|
+
return makeConst(t, r)
|
|
427
446
|
}
|
|
428
|
-
|
|
429
|
-
// Binary ops
|
|
447
|
+
// Binary
|
|
430
448
|
if (fn.length === 2 && node.length === 3) {
|
|
431
|
-
const a = getConst(node[1])
|
|
432
|
-
const b = getConst(node[2])
|
|
449
|
+
const a = getConst(node[1]), b = getConst(node[2])
|
|
433
450
|
if (!a || !b) return
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
const isCompare = /\.(eq|ne|[lg][te])/.test(op)
|
|
438
|
-
const resultType = isCompare ? 'i32' :
|
|
439
|
-
op.startsWith('i64.') ? 'i64' :
|
|
440
|
-
op.startsWith('f32.') ? 'f32' :
|
|
441
|
-
op.startsWith('f64.') ? 'f64' : 'i32'
|
|
442
|
-
return makeConst(resultType, result)
|
|
451
|
+
const r = fn(a.value, b.value)
|
|
452
|
+
if (r === null) return
|
|
453
|
+
return makeConst(t, r)
|
|
443
454
|
}
|
|
444
455
|
})
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
// ==================== IDENTITY REMOVAL ====================
|
|
448
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Create identity checker for commutative binary ops:
|
|
462
|
+
* neutral op x → x and x op neutral → x
|
|
463
|
+
*/
|
|
464
|
+
const commutativeIdentity = (neutral) => (a, b) => {
|
|
465
|
+
const ca = getConst(a), cb = getConst(b)
|
|
466
|
+
if (ca?.value === neutral) return b
|
|
467
|
+
if (cb?.value === neutral) return a
|
|
468
|
+
return null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Create identity checker for right-neutral binary ops:
|
|
473
|
+
* x op neutral → x
|
|
474
|
+
*/
|
|
475
|
+
const rightIdentity = (neutral) => (a, b) => getConst(b)?.value === neutral ? a : null
|
|
476
|
+
|
|
449
477
|
/** Identity operations that can be simplified */
|
|
450
478
|
const IDENTITIES = {
|
|
451
479
|
// x + 0 → x, 0 + x → x
|
|
452
|
-
'i32.add': (
|
|
453
|
-
|
|
454
|
-
if (ca?.value === 0) return b
|
|
455
|
-
if (cb?.value === 0) return a
|
|
456
|
-
return null
|
|
457
|
-
},
|
|
458
|
-
'i64.add': (a, b) => {
|
|
459
|
-
const ca = getConst(a), cb = getConst(b)
|
|
460
|
-
if (ca?.value === 0n) return b
|
|
461
|
-
if (cb?.value === 0n) return a
|
|
462
|
-
return null
|
|
463
|
-
},
|
|
480
|
+
'i32.add': commutativeIdentity(0),
|
|
481
|
+
'i64.add': commutativeIdentity(0n),
|
|
464
482
|
// x - 0 → x
|
|
465
|
-
'i32.sub': (
|
|
466
|
-
'i64.sub': (
|
|
483
|
+
'i32.sub': rightIdentity(0),
|
|
484
|
+
'i64.sub': rightIdentity(0n),
|
|
467
485
|
// x * 1 → x, 1 * x → x
|
|
468
|
-
'i32.mul': (
|
|
469
|
-
|
|
470
|
-
if (ca?.value === 1) return b
|
|
471
|
-
if (cb?.value === 1) return a
|
|
472
|
-
return null
|
|
473
|
-
},
|
|
474
|
-
'i64.mul': (a, b) => {
|
|
475
|
-
const ca = getConst(a), cb = getConst(b)
|
|
476
|
-
if (ca?.value === 1n) return b
|
|
477
|
-
if (cb?.value === 1n) return a
|
|
478
|
-
return null
|
|
479
|
-
},
|
|
486
|
+
'i32.mul': commutativeIdentity(1),
|
|
487
|
+
'i64.mul': commutativeIdentity(1n),
|
|
480
488
|
// x / 1 → x
|
|
481
|
-
'i32.div_s': (
|
|
482
|
-
'i32.div_u': (
|
|
483
|
-
'i64.div_s': (
|
|
484
|
-
'i64.div_u': (
|
|
489
|
+
'i32.div_s': rightIdentity(1),
|
|
490
|
+
'i32.div_u': rightIdentity(1),
|
|
491
|
+
'i64.div_s': rightIdentity(1n),
|
|
492
|
+
'i64.div_u': rightIdentity(1n),
|
|
485
493
|
// x & -1 → x, -1 & x → x (all bits set)
|
|
486
|
-
'i32.and': (
|
|
487
|
-
|
|
488
|
-
if (ca?.value === -1) return b
|
|
489
|
-
if (cb?.value === -1) return a
|
|
490
|
-
return null
|
|
491
|
-
},
|
|
492
|
-
'i64.and': (a, b) => {
|
|
493
|
-
const ca = getConst(a), cb = getConst(b)
|
|
494
|
-
if (ca?.value === -1n) return b
|
|
495
|
-
if (cb?.value === -1n) return a
|
|
496
|
-
return null
|
|
497
|
-
},
|
|
494
|
+
'i32.and': commutativeIdentity(-1),
|
|
495
|
+
'i64.and': commutativeIdentity(-1n),
|
|
498
496
|
// x | 0 → x, 0 | x → x
|
|
499
|
-
'i32.or': (
|
|
500
|
-
|
|
501
|
-
if (ca?.value === 0) return b
|
|
502
|
-
if (cb?.value === 0) return a
|
|
503
|
-
return null
|
|
504
|
-
},
|
|
505
|
-
'i64.or': (a, b) => {
|
|
506
|
-
const ca = getConst(a), cb = getConst(b)
|
|
507
|
-
if (ca?.value === 0n) return b
|
|
508
|
-
if (cb?.value === 0n) return a
|
|
509
|
-
return null
|
|
510
|
-
},
|
|
497
|
+
'i32.or': commutativeIdentity(0),
|
|
498
|
+
'i64.or': commutativeIdentity(0n),
|
|
511
499
|
// x ^ 0 → x, 0 ^ x → x
|
|
512
|
-
'i32.xor': (
|
|
513
|
-
|
|
514
|
-
if (ca?.value === 0) return b
|
|
515
|
-
if (cb?.value === 0) return a
|
|
516
|
-
return null
|
|
517
|
-
},
|
|
518
|
-
'i64.xor': (a, b) => {
|
|
519
|
-
const ca = getConst(a), cb = getConst(b)
|
|
520
|
-
if (ca?.value === 0n) return b
|
|
521
|
-
if (cb?.value === 0n) return a
|
|
522
|
-
return null
|
|
523
|
-
},
|
|
500
|
+
'i32.xor': commutativeIdentity(0),
|
|
501
|
+
'i64.xor': commutativeIdentity(0n),
|
|
524
502
|
// x << 0 → x, x >> 0 → x
|
|
525
|
-
'i32.shl': (
|
|
526
|
-
'i32.shr_s': (
|
|
527
|
-
'i32.shr_u': (
|
|
528
|
-
'i64.shl': (
|
|
529
|
-
'i64.shr_s': (
|
|
530
|
-
'i64.shr_u': (
|
|
503
|
+
'i32.shl': rightIdentity(0),
|
|
504
|
+
'i32.shr_s': rightIdentity(0),
|
|
505
|
+
'i32.shr_u': rightIdentity(0),
|
|
506
|
+
'i64.shl': rightIdentity(0n),
|
|
507
|
+
'i64.shr_s': rightIdentity(0n),
|
|
508
|
+
'i64.shr_u': rightIdentity(0n),
|
|
531
509
|
// f + 0 → x (careful with -0.0, skip for floats)
|
|
532
510
|
// f * 1 → x (careful with NaN, skip for floats)
|
|
533
511
|
}
|
|
@@ -633,50 +611,15 @@ const branch = (ast) => {
|
|
|
633
611
|
// (if (i32.const 0) then else) → else
|
|
634
612
|
// (if (i32.const N) then else) → then (N != 0)
|
|
635
613
|
if (op === 'if') {
|
|
636
|
-
|
|
637
|
-
let condIdx = 1
|
|
638
|
-
while (condIdx < node.length) {
|
|
639
|
-
const child = node[condIdx]
|
|
640
|
-
if (Array.isArray(child) && (child[0] === 'then' || child[0] === 'else' || child[0] === 'result' || child[0] === 'param')) {
|
|
641
|
-
condIdx++
|
|
642
|
-
continue
|
|
643
|
-
}
|
|
644
|
-
break
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const cond = node[condIdx]
|
|
614
|
+
const { cond, thenBranch, elseBranch } = parseIf(node)
|
|
648
615
|
const c = getConst(cond)
|
|
649
616
|
if (!c) return
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const child = node[i]
|
|
655
|
-
if (Array.isArray(child)) {
|
|
656
|
-
if (child[0] === 'then') thenBranch = child
|
|
657
|
-
else if (child[0] === 'else') elseBranch = child
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Condition is truthy → replace with then contents
|
|
662
|
-
if (c.value !== 0 && c.value !== 0n) {
|
|
663
|
-
if (thenBranch && thenBranch.length > 1) {
|
|
664
|
-
// Return block with then contents (or just contents if single)
|
|
665
|
-
const contents = thenBranch.slice(1)
|
|
666
|
-
if (contents.length === 1) return contents[0]
|
|
667
|
-
return ['block', ...contents]
|
|
668
|
-
}
|
|
669
|
-
return ['nop']
|
|
670
|
-
}
|
|
671
|
-
// Condition is falsy → replace with else contents
|
|
672
|
-
else {
|
|
673
|
-
if (elseBranch && elseBranch.length > 1) {
|
|
674
|
-
const contents = elseBranch.slice(1)
|
|
675
|
-
if (contents.length === 1) return contents[0]
|
|
676
|
-
return ['block', ...contents]
|
|
677
|
-
}
|
|
678
|
-
return ['nop']
|
|
617
|
+
const taken = c.value !== 0 && c.value !== 0n ? thenBranch : elseBranch
|
|
618
|
+
if (taken && taken.length > 1) {
|
|
619
|
+
const contents = taken.slice(1)
|
|
620
|
+
return contents.length === 1 ? contents[0] : ['block', ...contents]
|
|
679
621
|
}
|
|
622
|
+
return ['nop']
|
|
680
623
|
}
|
|
681
624
|
|
|
682
625
|
// (br_if $label (i32.const 0)) → nop
|
|
@@ -842,82 +785,233 @@ const localReuse = (ast) => {
|
|
|
842
785
|
return result
|
|
843
786
|
}
|
|
844
787
|
|
|
845
|
-
// ====================
|
|
788
|
+
// ==================== PROPAGATION & LOCAL ELIMINATION ====================
|
|
789
|
+
|
|
790
|
+
/** Operators with side effects: calls, mutators, control flow, exceptions, drops. */
|
|
791
|
+
const IMPURE_OPS = new Set([
|
|
792
|
+
'call', 'call_indirect', 'return_call', 'return_call_indirect',
|
|
793
|
+
'table.set', 'table.grow', 'table.fill', 'table.copy', 'table.init',
|
|
794
|
+
'struct.set', 'struct.new',
|
|
795
|
+
'array.set', 'array.new', 'array.new_fixed', 'array.new_data', 'array.new_elem',
|
|
796
|
+
'array.init_data', 'array.init_elem', 'ref.i31',
|
|
797
|
+
'global.set', 'local.set', 'local.tee',
|
|
798
|
+
'unreachable', 'return',
|
|
799
|
+
'br', 'br_if', 'br_table', 'br_on_null', 'br_on_non_null', 'br_on_cast', 'br_on_cast_fail',
|
|
800
|
+
'throw', 'rethrow', 'throw_ref', 'try_table',
|
|
801
|
+
'data.drop', 'elem.drop',
|
|
802
|
+
])
|
|
803
|
+
|
|
804
|
+
/** Substrings that flag an op as side-effecting (loads can trap, stores/atomics/memory ops mutate). */
|
|
805
|
+
const IMPURE_SUBSTRINGS = ['.store', 'memory.', '.atomic.']
|
|
846
806
|
|
|
847
807
|
/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* @param {Array} ast
|
|
851
|
-
* @returns {Array}
|
|
808
|
+
* Pure means: no side effects, no traps we care about, no control flow.
|
|
809
|
+
* Conservative — returns false for anything that might trap, mutate state, or branch.
|
|
852
810
|
*/
|
|
853
|
-
const
|
|
854
|
-
|
|
811
|
+
const isPure = (node) => {
|
|
812
|
+
if (!Array.isArray(node)) return true
|
|
813
|
+
const op = node[0]
|
|
814
|
+
if (typeof op !== 'string') return false
|
|
815
|
+
if (IMPURE_OPS.has(op)) return false
|
|
816
|
+
for (const sub of IMPURE_SUBSTRINGS) if (op.includes(sub)) return false
|
|
817
|
+
for (let i = 1; i < node.length; i++) if (Array.isArray(node[i]) && !isPure(node[i])) return false
|
|
818
|
+
return true
|
|
819
|
+
}
|
|
855
820
|
|
|
856
|
-
|
|
857
|
-
|
|
821
|
+
/** Count all local.get/set/tee occurrences in one walk */
|
|
822
|
+
const countLocalUses = (node) => {
|
|
823
|
+
const counts = new Map()
|
|
824
|
+
const ensure = name => { if (!counts.has(name)) counts.set(name, { gets: 0, sets: 0, tees: 0 }); return counts.get(name) }
|
|
825
|
+
walk(node, n => {
|
|
826
|
+
if (!Array.isArray(n) || n.length < 2 || typeof n[1] !== 'string') return
|
|
827
|
+
if (n[0] === 'local.get') ensure(n[1]).gets++
|
|
828
|
+
else if (n[0] === 'local.set') ensure(n[1]).sets++
|
|
829
|
+
else if (n[0] === 'local.tee') ensure(n[1]).tees++
|
|
830
|
+
})
|
|
831
|
+
return counts
|
|
832
|
+
}
|
|
858
833
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
const constLocals = new Map() // $name → const node
|
|
862
|
-
|
|
863
|
-
// Process function body in order
|
|
864
|
-
const processBlock = (block, startIdx = 1) => {
|
|
865
|
-
for (let i = startIdx; i < block.length; i++) {
|
|
866
|
-
const instr = block[i]
|
|
867
|
-
if (!Array.isArray(instr)) continue
|
|
868
|
-
|
|
869
|
-
const op = instr[0]
|
|
870
|
-
|
|
871
|
-
// local.set $x (const) → remember constant
|
|
872
|
-
if (op === 'local.set' && instr.length === 3) {
|
|
873
|
-
const local = instr[1]
|
|
874
|
-
const val = instr[2]
|
|
875
|
-
const c = getConst(val)
|
|
876
|
-
if (c && typeof local === 'string') {
|
|
877
|
-
constLocals.set(local, val)
|
|
878
|
-
} else if (typeof local === 'string') {
|
|
879
|
-
constLocals.delete(local) // invalidate if set to non-const
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
// local.tee also sets
|
|
883
|
-
else if (op === 'local.tee' && instr.length === 3) {
|
|
884
|
-
const local = instr[1]
|
|
885
|
-
const val = instr[2]
|
|
886
|
-
const c = getConst(val)
|
|
887
|
-
if (c && typeof local === 'string') {
|
|
888
|
-
constLocals.set(local, val)
|
|
889
|
-
} else if (typeof local === 'string') {
|
|
890
|
-
constLocals.delete(local)
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
// local.get $x → replace with const if known
|
|
894
|
-
else if (op === 'local.get' && instr.length === 2) {
|
|
895
|
-
const local = instr[1]
|
|
896
|
-
if (typeof local === 'string' && constLocals.has(local)) {
|
|
897
|
-
const constVal = constLocals.get(local)
|
|
898
|
-
// Replace in place
|
|
899
|
-
instr.length = 0
|
|
900
|
-
instr.push(...clone(constVal))
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
// Control flow invalidates all knowledge (conservative)
|
|
904
|
-
else if (op === 'block' || op === 'loop' || op === 'if' || op === 'call' || op === 'call_indirect') {
|
|
905
|
-
constLocals.clear()
|
|
906
|
-
}
|
|
834
|
+
/** Can this tracked value be substituted for a local.get? */
|
|
835
|
+
const canSubst = (k) => getConst(k.val) || (k.pure && k.singleUse)
|
|
907
836
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
837
|
+
/** Try substitute local.get nodes with known values */
|
|
838
|
+
const substGets = (node, known) => walkPost(node, n => {
|
|
839
|
+
if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
|
|
840
|
+
const k = typeof n[1] === 'string' && known.get(n[1])
|
|
841
|
+
if (k && canSubst(k)) return clone(k.val)
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Forward propagation pass: track local.set values and substitute local.gets.
|
|
846
|
+
* Returns true if any substitution was made.
|
|
847
|
+
* @param {Array} funcNode
|
|
848
|
+
* @param {Set<string>} params
|
|
849
|
+
* @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
|
|
850
|
+
*/
|
|
851
|
+
const forwardPropagate = (funcNode, params, useCounts) => {
|
|
852
|
+
let changed = false
|
|
853
|
+
const getUseCount = name => useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
|
|
854
|
+
const known = new Map()
|
|
855
|
+
|
|
856
|
+
for (let i = 1; i < funcNode.length; i++) {
|
|
857
|
+
const instr = funcNode[i]
|
|
858
|
+
if (!Array.isArray(instr)) continue
|
|
859
|
+
const op = instr[0]
|
|
860
|
+
|
|
861
|
+
if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
|
|
862
|
+
|
|
863
|
+
// Track local.set values
|
|
864
|
+
if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
|
|
865
|
+
substGets(instr[2], known) // substitute known values in RHS
|
|
866
|
+
const uses = getUseCount(instr[1])
|
|
867
|
+
known.set(instr[1], {
|
|
868
|
+
val: instr[2], pure: isPure(instr[2]),
|
|
869
|
+
singleUse: uses.gets <= 1 && uses.sets <= 1 && uses.tees === 0
|
|
870
|
+
})
|
|
871
|
+
continue
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Invalidate at control-flow boundaries
|
|
875
|
+
if (op === 'block' || op === 'loop' || op === 'if') known.clear()
|
|
876
|
+
// Calls only invalidate non-constant tracked values
|
|
877
|
+
if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect')
|
|
878
|
+
for (const [key, tracked] of known) if (!getConst(tracked.val)) known.delete(key)
|
|
879
|
+
|
|
880
|
+
// Substitute: standalone local.get (walkPost can't replace root)
|
|
881
|
+
if (op === 'local.get' && instr.length === 2 && typeof instr[1] === 'string') {
|
|
882
|
+
const tracked = known.get(instr[1])
|
|
883
|
+
if (tracked && canSubst(tracked)) {
|
|
884
|
+
const replacement = clone(tracked.val)
|
|
885
|
+
instr.length = 0; instr.push(...(Array.isArray(replacement) ? replacement : [replacement]))
|
|
886
|
+
changed = true; continue
|
|
917
887
|
}
|
|
918
888
|
}
|
|
919
889
|
|
|
920
|
-
|
|
890
|
+
// Substitute nested local.gets (skip control-flow nodes — locals may be reassigned inside)
|
|
891
|
+
if (op !== 'block' && op !== 'loop' && op !== 'if') {
|
|
892
|
+
const prev = clone(instr)
|
|
893
|
+
substGets(instr, known)
|
|
894
|
+
if (!equal(prev, instr)) changed = true
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return changed
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Remove adjacent (local.set $x expr) (local.get $x) pairs when $x has no other uses.
|
|
903
|
+
* Returns true if any pair was removed.
|
|
904
|
+
* @param {Array} funcNode
|
|
905
|
+
* @param {Set<string>} params
|
|
906
|
+
* @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
|
|
907
|
+
*/
|
|
908
|
+
const eliminateSetGetPairs = (funcNode, params, useCounts) => {
|
|
909
|
+
let changed = false
|
|
910
|
+
|
|
911
|
+
for (let i = 1; i < funcNode.length - 1; i++) {
|
|
912
|
+
const setNode = funcNode[i]
|
|
913
|
+
const getNode = funcNode[i + 1]
|
|
914
|
+
if (!Array.isArray(setNode) || setNode[0] !== 'local.set' || setNode.length !== 3) continue
|
|
915
|
+
if (!Array.isArray(getNode) || getNode[0] !== 'local.get' || getNode.length !== 2) continue
|
|
916
|
+
const name = setNode[1]
|
|
917
|
+
if (getNode[1] !== name || params.has(name)) continue
|
|
918
|
+
const uses = useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
|
|
919
|
+
// Must be exactly 1 set and 1 get (the pair), no tees
|
|
920
|
+
if (uses.sets !== 1 || uses.gets !== 1 || uses.tees !== 0) continue
|
|
921
|
+
// Replace the pair with just the expression
|
|
922
|
+
const expr = clone(setNode[2])
|
|
923
|
+
funcNode.splice(i, 2, ...(Array.isArray(expr) ? [expr] : [expr]))
|
|
924
|
+
changed = true
|
|
925
|
+
i-- // adjust index because we removed 2 and inserted 1
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return changed
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Convert (local.set $x expr) (local.get $x) to (local.tee $x expr)
|
|
933
|
+
* when $x has additional uses beyond this pair.
|
|
934
|
+
* @param {Array} funcNode
|
|
935
|
+
* @param {Set<string>} params
|
|
936
|
+
* @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
|
|
937
|
+
*/
|
|
938
|
+
const createLocalTees = (funcNode, params, useCounts) => {
|
|
939
|
+
let changed = false
|
|
940
|
+
|
|
941
|
+
for (let i = 1; i < funcNode.length - 1; i++) {
|
|
942
|
+
const setNode = funcNode[i]
|
|
943
|
+
const getNode = funcNode[i + 1]
|
|
944
|
+
if (!Array.isArray(setNode) || setNode[0] !== 'local.set' || setNode.length !== 3) continue
|
|
945
|
+
if (!Array.isArray(getNode) || getNode[0] !== 'local.get' || getNode.length !== 2) continue
|
|
946
|
+
const name = setNode[1]
|
|
947
|
+
if (getNode[1] !== name || params.has(name)) continue
|
|
948
|
+
const uses = useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
|
|
949
|
+
// Only if there's more than just this set+get pair
|
|
950
|
+
if (uses.sets + uses.gets + uses.tees <= 2) continue
|
|
951
|
+
// Replace with local.tee (set+get combined)
|
|
952
|
+
funcNode.splice(i, 2, ['local.tee', name, clone(setNode[2])])
|
|
953
|
+
changed = true
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return changed
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Remove dead stores and unused local declarations in a reverse pass.
|
|
961
|
+
* Returns true if anything was removed.
|
|
962
|
+
* @param {Array} funcNode
|
|
963
|
+
* @param {Set<string>} params
|
|
964
|
+
* @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
|
|
965
|
+
*/
|
|
966
|
+
const eliminateDeadStores = (funcNode, params, useCounts) => {
|
|
967
|
+
let changed = false
|
|
968
|
+
const getPostUseCount = name => useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
|
|
969
|
+
|
|
970
|
+
for (let i = funcNode.length - 1; i >= 1; i--) {
|
|
971
|
+
const sub = funcNode[i]
|
|
972
|
+
if (!Array.isArray(sub)) continue
|
|
973
|
+
const name = typeof sub[1] === 'string' ? sub[1] : null
|
|
974
|
+
if (!name || params.has(name)) continue
|
|
975
|
+
const uses = getPostUseCount(name)
|
|
976
|
+
// Dead store: set but never read, pure RHS
|
|
977
|
+
if (sub[0] === 'local.set' && uses.gets === 0 && uses.tees === 0 && isPure(sub[2])) {
|
|
978
|
+
funcNode.splice(i, 1); changed = true
|
|
979
|
+
}
|
|
980
|
+
// Unused local declaration
|
|
981
|
+
else if (sub[0] === 'local' && name[0] === '$' && uses.gets === 0 && uses.sets === 0 && uses.tees === 0) {
|
|
982
|
+
funcNode.splice(i, 1); changed = true
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return changed
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Propagate values through locals and eliminate single-use/dead locals.
|
|
991
|
+
* Constants propagate to all uses; pure single-use exprs inline into get site.
|
|
992
|
+
* Multi-pass with batch counting for convergence.
|
|
993
|
+
*/
|
|
994
|
+
const propagate = (ast) => {
|
|
995
|
+
const result = clone(ast)
|
|
996
|
+
|
|
997
|
+
walk(result, (funcNode) => {
|
|
998
|
+
if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
|
|
999
|
+
|
|
1000
|
+
const params = new Set()
|
|
1001
|
+
for (const sub of funcNode)
|
|
1002
|
+
if (Array.isArray(sub) && sub[0] === 'param' && typeof sub[1] === 'string') params.add(sub[1])
|
|
1003
|
+
|
|
1004
|
+
// useCounts must be refreshed before every sub-pass: each mutation
|
|
1005
|
+
// (substitution, set/get pair removal, tee creation, dead-store removal)
|
|
1006
|
+
// changes the gets/sets/tees totals that downstream sub-passes rely on.
|
|
1007
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
1008
|
+
let changed = false
|
|
1009
|
+
if (forwardPropagate(funcNode, params, countLocalUses(funcNode))) changed = true
|
|
1010
|
+
if (eliminateSetGetPairs(funcNode, params, countLocalUses(funcNode))) changed = true
|
|
1011
|
+
if (createLocalTees(funcNode, params, countLocalUses(funcNode))) changed = true
|
|
1012
|
+
if (eliminateDeadStores(funcNode, params, countLocalUses(funcNode))) changed = true
|
|
1013
|
+
if (!changed) break
|
|
1014
|
+
}
|
|
921
1015
|
})
|
|
922
1016
|
|
|
923
1017
|
return result
|
|
@@ -970,8 +1064,8 @@ const inline = (ast) => {
|
|
|
970
1064
|
}
|
|
971
1065
|
}
|
|
972
1066
|
|
|
973
|
-
//
|
|
974
|
-
if (params && !hasLocals && !hasExport && params.length <=
|
|
1067
|
+
// Inline: no locals, <= 4 params, single expression body, not exported
|
|
1068
|
+
if (params && !hasLocals && !hasExport && params.length <= 4 && body.length === 1) {
|
|
975
1069
|
// Check if function mutates any of its params (local.set/tee on param)
|
|
976
1070
|
const paramNames = new Set(params.map(p => p.name))
|
|
977
1071
|
let mutatesParam = false
|
|
@@ -1020,135 +1114,915 @@ const inline = (ast) => {
|
|
|
1020
1114
|
return result
|
|
1021
1115
|
}
|
|
1022
1116
|
|
|
1023
|
-
// ====================
|
|
1117
|
+
// ==================== VACUUM ====================
|
|
1024
1118
|
|
|
1025
1119
|
/**
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
* @
|
|
1120
|
+
* Remove no-op code: nops, drop of pure expressions, empty branches,
|
|
1121
|
+
* and select with identical arms.
|
|
1122
|
+
* @param {Array} ast
|
|
1123
|
+
* @returns {Array}
|
|
1029
1124
|
*/
|
|
1030
|
-
const
|
|
1125
|
+
const vacuum = (ast) => {
|
|
1126
|
+
return walkPost(clone(ast), (node) => {
|
|
1127
|
+
if (!Array.isArray(node)) return
|
|
1128
|
+
const op = node[0]
|
|
1129
|
+
|
|
1130
|
+
// Remove nop entirely (return array marker; parent or post-pass cleans it)
|
|
1131
|
+
if (op === 'nop') return ['nop']
|
|
1132
|
+
|
|
1133
|
+
// (drop PURE) → nop
|
|
1134
|
+
if (op === 'drop' && node.length === 2 && isPure(node[1])) {
|
|
1135
|
+
return ['nop']
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// (select x x cond) → x
|
|
1139
|
+
if (op === 'select' && node.length >= 4 && equal(node[1], node[2])) return node[1]
|
|
1140
|
+
|
|
1141
|
+
if (op === 'if') {
|
|
1142
|
+
const { cond, thenBranch, elseBranch } = parseIf(node)
|
|
1143
|
+
const thenEmpty = !thenBranch || thenBranch.length <= 1
|
|
1144
|
+
const elseEmpty = !elseBranch || elseBranch.length <= 1
|
|
1145
|
+
|
|
1146
|
+
// (if cond () ()) → nop or (drop cond)
|
|
1147
|
+
if (thenEmpty && elseEmpty) return isPure(cond) ? ['nop'] : ['drop', cond]
|
|
1148
|
+
|
|
1149
|
+
// (if cond (then X) (else)) → drop the empty else
|
|
1150
|
+
if (elseBranch && elseEmpty && !thenEmpty) {
|
|
1151
|
+
return node.filter(c => c !== elseBranch)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Clean out nops, drop-of-pure sequences, and empty annotations from blocks
|
|
1156
|
+
if (op === 'func' || op === 'block' || op === 'loop' || op === 'then' || op === 'else') {
|
|
1157
|
+
const cleaned = [op]
|
|
1158
|
+
for (let i = 1; i < node.length; i++) {
|
|
1159
|
+
const child = node[i]
|
|
1160
|
+
if (child === 'nop' || (Array.isArray(child) && child[0] === 'nop')) continue
|
|
1161
|
+
// Pure expression followed by standalone drop → remove both
|
|
1162
|
+
const next = node[i + 1]
|
|
1163
|
+
const isDrop = next === 'drop' || (Array.isArray(next) && next[0] === 'drop' && next.length === 1)
|
|
1164
|
+
if (Array.isArray(child) && isPure(child) && isDrop) {
|
|
1165
|
+
i++ // skip the drop too
|
|
1166
|
+
continue
|
|
1167
|
+
}
|
|
1168
|
+
cleaned.push(child)
|
|
1169
|
+
}
|
|
1170
|
+
if (cleaned.length !== node.length) return cleaned
|
|
1171
|
+
}
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ==================== PEEPHOLE ====================
|
|
1176
|
+
|
|
1177
|
+
/** Peephole optimizations: simple algebraic identities */
|
|
1178
|
+
const PEEPHOLE = {
|
|
1179
|
+
// Self-cancelling / tautological binary ops
|
|
1180
|
+
'i32.sub': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1181
|
+
'i64.sub': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
|
|
1182
|
+
'i32.xor': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1183
|
+
'i64.xor': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
|
|
1184
|
+
'i32.and': (a, b) => equal(a, b) ? a : null,
|
|
1185
|
+
'i64.and': (a, b) => equal(a, b) ? a : null,
|
|
1186
|
+
'i32.or': (a, b) => equal(a, b) ? a : null,
|
|
1187
|
+
'i64.or': (a, b) => equal(a, b) ? a : null,
|
|
1188
|
+
'i32.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1189
|
+
'i64.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1190
|
+
'i32.ne': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1191
|
+
'i64.ne': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1192
|
+
'i32.lt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1193
|
+
'i32.lt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1194
|
+
'i32.gt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1195
|
+
'i32.gt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1196
|
+
'i32.le_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1197
|
+
'i32.le_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1198
|
+
'i32.ge_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1199
|
+
'i32.ge_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1200
|
+
'i64.lt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1201
|
+
'i64.lt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1202
|
+
'i64.gt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1203
|
+
'i64.gt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
|
|
1204
|
+
'i64.le_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1205
|
+
'i64.le_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1206
|
+
'i64.ge_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1207
|
+
'i64.ge_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
|
|
1208
|
+
|
|
1209
|
+
// Zero/all-bits absorption
|
|
1210
|
+
'i32.mul': (a, b) => {
|
|
1211
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1212
|
+
if (ca?.value === 0 || cb?.value === 0) return ['i32.const', 0]
|
|
1213
|
+
return null
|
|
1214
|
+
},
|
|
1215
|
+
'i64.mul': (a, b) => {
|
|
1216
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1217
|
+
if (ca?.value === 0n || cb?.value === 0n) return ['i64.const', 0n]
|
|
1218
|
+
return null
|
|
1219
|
+
},
|
|
1220
|
+
'i32.and': (a, b) => {
|
|
1221
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1222
|
+
if (ca?.value === 0 || cb?.value === 0) return ['i32.const', 0]
|
|
1223
|
+
// x & x → x handled above in self-operands, but null here lets that win
|
|
1224
|
+
return null
|
|
1225
|
+
},
|
|
1226
|
+
'i64.and': (a, b) => {
|
|
1227
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1228
|
+
if (ca?.value === 0n || cb?.value === 0n) return ['i64.const', 0n]
|
|
1229
|
+
return null
|
|
1230
|
+
},
|
|
1231
|
+
'i32.or': (a, b) => {
|
|
1232
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1233
|
+
if (ca?.value === -1 || cb?.value === -1) return ['i32.const', -1]
|
|
1234
|
+
return null
|
|
1235
|
+
},
|
|
1236
|
+
'i64.or': (a, b) => {
|
|
1237
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1238
|
+
if (ca?.value === -1n || cb?.value === -1n) return ['i64.const', -1n]
|
|
1239
|
+
return null
|
|
1240
|
+
},
|
|
1241
|
+
|
|
1242
|
+
// (local.set $x (local.get $x)) → nop
|
|
1243
|
+
'local.set': (a, b) => Array.isArray(b) && b[0] === 'local.get' && b[1] === a ? ['nop'] : null,
|
|
1244
|
+
}
|
|
1031
1245
|
|
|
1032
1246
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
* Limited to pure expressions within a function.
|
|
1247
|
+
* Apply peephole optimizations.
|
|
1035
1248
|
* @param {Array} ast
|
|
1036
1249
|
* @returns {Array}
|
|
1037
1250
|
*/
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1251
|
+
const peephole = (ast) => {
|
|
1252
|
+
return walkPost(clone(ast), (node) => {
|
|
1253
|
+
if (!Array.isArray(node) || node.length !== 3) return
|
|
1254
|
+
const fn = PEEPHOLE[node[0]]
|
|
1255
|
+
if (!fn) return
|
|
1256
|
+
const result = fn(node[1], node[2])
|
|
1257
|
+
if (result !== null) return result
|
|
1258
|
+
})
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// ==================== GLOBAL CONSTANT PROPAGATION ====================
|
|
1042
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* Replace global.get of immutable globals with their constant init values.
|
|
1265
|
+
* @param {Array} ast
|
|
1266
|
+
* @returns {Array}
|
|
1267
|
+
*/
|
|
1268
|
+
const globals = (ast) => {
|
|
1269
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1270
|
+
const result = clone(ast)
|
|
1271
|
+
|
|
1272
|
+
// Find immutable globals with const init
|
|
1273
|
+
const constGlobals = new Map() // name → const node
|
|
1274
|
+
const mutableGlobals = new Set()
|
|
1275
|
+
|
|
1276
|
+
for (const node of result.slice(1)) {
|
|
1277
|
+
if (!Array.isArray(node) || node[0] !== 'global') continue
|
|
1278
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1279
|
+
if (!name) continue
|
|
1280
|
+
|
|
1281
|
+
// Check mutability: (global $g (mut i32) init) vs (global $g i32 init)
|
|
1282
|
+
const hasName = typeof node[1] === 'string' && node[1][0] === '$'
|
|
1283
|
+
const initIdx = hasName ? 3 : 2
|
|
1284
|
+
|
|
1285
|
+
// Skip mutable globals
|
|
1286
|
+
const typeSlot = hasName ? node[2] : node[1]
|
|
1287
|
+
if (Array.isArray(typeSlot) && typeSlot[0] === 'mut') continue
|
|
1288
|
+
|
|
1289
|
+
const init = node[initIdx]
|
|
1290
|
+
if (getConst(init)) constGlobals.set(name, init)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Also mark any global that is ever written as mutable
|
|
1294
|
+
walk(result, (n) => {
|
|
1295
|
+
if (!Array.isArray(n) || n[0] !== 'global.set') return
|
|
1296
|
+
const ref = n[1]
|
|
1297
|
+
if (typeof ref === 'string' && ref[0] === '$') mutableGlobals.add(ref)
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
// Remove mutable ones from propagation set
|
|
1301
|
+
for (const name of mutableGlobals) constGlobals.delete(name)
|
|
1302
|
+
if (constGlobals.size === 0) return result
|
|
1303
|
+
|
|
1304
|
+
// Substitute global.get with const
|
|
1305
|
+
return walkPost(result, (node) => {
|
|
1306
|
+
if (!Array.isArray(node) || node[0] !== 'global.get' || node.length !== 2) return
|
|
1307
|
+
const ref = node[1]
|
|
1308
|
+
if (constGlobals.has(ref)) return clone(constGlobals.get(ref))
|
|
1309
|
+
})
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// ==================== LOAD/STORE OFFSET FOLDING ====================
|
|
1313
|
+
|
|
1314
|
+
/** Match (type.load/store (i32.add ptr (type.const N))) and fold offset */
|
|
1315
|
+
const offset = (ast) => {
|
|
1316
|
+
return walkPost(clone(ast), (node) => {
|
|
1317
|
+
if (!Array.isArray(node)) return
|
|
1318
|
+
const op = node[0]
|
|
1319
|
+
if (typeof op !== 'string' || (!op.endsWith('load') && !op.endsWith('store'))) return
|
|
1320
|
+
|
|
1321
|
+
// Memory ops have memarg as first immediate after optional memoryidx, then operands
|
|
1322
|
+
// In AST form from parse: (i32.load offset=4 align=8 ptr) or (i32.load ptr)
|
|
1323
|
+
// Store: (i32.store offset=4 ptr val) — ptr is second-to-last, val is last
|
|
1324
|
+
// Load: (i32.load offset=4 ptr) — ptr is last
|
|
1325
|
+
const isStore = op.endsWith('store')
|
|
1326
|
+
|
|
1327
|
+
// Find current offset from memparams
|
|
1328
|
+
let currentOffset = 0
|
|
1329
|
+
let memIdx = null
|
|
1330
|
+
let argStart = 1
|
|
1331
|
+
|
|
1332
|
+
// Check for memory index
|
|
1333
|
+
if (typeof node[1] === 'string' && (node[1][0] === '$' || !isNaN(node[1]))) {
|
|
1334
|
+
memIdx = node[1]
|
|
1335
|
+
argStart = 2
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Check for memparams (offset=, align=)
|
|
1339
|
+
while (argStart < node.length && typeof node[argStart] === 'string' &&
|
|
1340
|
+
(node[argStart].startsWith('offset=') || node[argStart].startsWith('align='))) {
|
|
1341
|
+
if (node[argStart].startsWith('offset=')) {
|
|
1342
|
+
currentOffset = +node[argStart].slice(7)
|
|
1343
|
+
}
|
|
1344
|
+
argStart++
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Determine pointer index
|
|
1348
|
+
const ptrIdx = isStore ? node.length - 2 : node.length - 1
|
|
1349
|
+
const valIdx = isStore ? node.length - 1 : -1
|
|
1350
|
+
if (ptrIdx < argStart) return
|
|
1351
|
+
|
|
1352
|
+
const ptr = node[ptrIdx]
|
|
1353
|
+
if (!Array.isArray(ptr) || ptr[0] !== 'i32.add' || ptr.length !== 3) return
|
|
1354
|
+
|
|
1355
|
+
const a = ptr[1], b = ptr[2]
|
|
1356
|
+
const ca = getConst(a), cb = getConst(b)
|
|
1357
|
+
|
|
1358
|
+
let base = null, addend = null
|
|
1359
|
+
if (ca && ca.type === 'i32') { addend = ca.value; base = b }
|
|
1360
|
+
else if (cb && cb.type === 'i32') { addend = cb.value; base = a }
|
|
1361
|
+
if (base === null || addend === null) return
|
|
1362
|
+
|
|
1363
|
+
const newOffset = currentOffset + addend
|
|
1364
|
+
const newNode = [op]
|
|
1365
|
+
if (memIdx !== null) newNode.push(memIdx)
|
|
1366
|
+
newNode.push(`offset=${newOffset}`)
|
|
1367
|
+
// Preserve align if present
|
|
1368
|
+
let alignParam = null
|
|
1369
|
+
for (let i = argStart; i < ptrIdx; i++) {
|
|
1370
|
+
if (typeof node[i] === 'string' && node[i].startsWith('align=')) {
|
|
1371
|
+
alignParam = node[i]
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (alignParam) newNode.push(alignParam)
|
|
1375
|
+
newNode.push(base)
|
|
1376
|
+
if (isStore) newNode.push(node[valIdx])
|
|
1377
|
+
return newNode
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// ==================== REDUNDANT BR REMOVAL ====================
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Remove br to a block's own label when it is the last instruction.
|
|
1385
|
+
* @param {Array} ast
|
|
1386
|
+
* @returns {Array}
|
|
1387
|
+
*/
|
|
1388
|
+
const unbranch = (ast) => {
|
|
1043
1389
|
const result = clone(ast)
|
|
1044
1390
|
|
|
1045
1391
|
walk(result, (node) => {
|
|
1046
|
-
if (!Array.isArray(node)
|
|
1392
|
+
if (!Array.isArray(node)) return
|
|
1393
|
+
const op = node[0]
|
|
1394
|
+
if (op !== 'block' && op !== 'loop') return
|
|
1395
|
+
|
|
1396
|
+
// Get the block's label
|
|
1397
|
+
let labelIdx = 1
|
|
1398
|
+
let label = null
|
|
1399
|
+
if (typeof node[1] === 'string' && node[1][0] === '$') {
|
|
1400
|
+
label = node[1]
|
|
1401
|
+
labelIdx = 2
|
|
1402
|
+
}
|
|
1403
|
+
if (!label) return
|
|
1404
|
+
|
|
1405
|
+
// Find the last executable instruction (skip result/type annotations)
|
|
1406
|
+
let lastIdx = -1
|
|
1407
|
+
for (let i = node.length - 1; i >= labelIdx; i--) {
|
|
1408
|
+
const child = node[i]
|
|
1409
|
+
if (!Array.isArray(child)) {
|
|
1410
|
+
if (child !== 'nop' && child !== 'end') lastIdx = i
|
|
1411
|
+
continue
|
|
1412
|
+
}
|
|
1413
|
+
const cop = child[0]
|
|
1414
|
+
if (cop === 'param' || cop === 'result' || cop === 'local' || cop === 'type' || cop === 'export') continue
|
|
1415
|
+
lastIdx = i
|
|
1416
|
+
break
|
|
1417
|
+
}
|
|
1418
|
+
if (lastIdx < 0) return
|
|
1419
|
+
|
|
1420
|
+
const last = node[lastIdx]
|
|
1421
|
+
if (Array.isArray(last) && last[0] === 'br' && last[1] === label) {
|
|
1422
|
+
node.splice(lastIdx, 1)
|
|
1423
|
+
}
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
return result
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// ==================== STRIP MUT FROM GLOBALS ====================
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Strip mutability from globals that are never written.
|
|
1433
|
+
* Enables globals constant-propagation for more globals.
|
|
1434
|
+
* @param {Array} ast
|
|
1435
|
+
* @returns {Array}
|
|
1436
|
+
*/
|
|
1437
|
+
const stripmut = (ast) => {
|
|
1438
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1439
|
+
const result = clone(ast)
|
|
1440
|
+
|
|
1441
|
+
const written = new Set()
|
|
1442
|
+
walk(result, (n) => {
|
|
1443
|
+
if (Array.isArray(n) && n[0] === 'global.set' && typeof n[1] === 'string') written.add(n[1])
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
return walkPost(result, (node) => {
|
|
1447
|
+
if (!Array.isArray(node) || node[0] !== 'global') return
|
|
1448
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1449
|
+
if (!name || written.has(name)) return
|
|
1450
|
+
|
|
1451
|
+
const hasName = typeof node[1] === 'string' && node[1][0] === '$'
|
|
1452
|
+
const typeSlot = hasName ? node[2] : node[1]
|
|
1453
|
+
if (Array.isArray(typeSlot) && typeSlot[0] === 'mut') {
|
|
1454
|
+
const newNode = [...node]
|
|
1455
|
+
newNode[hasName ? 2 : 1] = typeSlot[1] // replace (mut T) with T
|
|
1456
|
+
return newNode
|
|
1457
|
+
}
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1047
1460
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1461
|
+
// ==================== IF-THEN-BR → BR_IF ====================
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Simplify (if cond (then (br $label))) → (br_if $label cond)
|
|
1465
|
+
* and (if cond (then) (else (br $label))) → (br_if $label (i32.eqz cond))
|
|
1466
|
+
* Only when the br is the sole instruction in the arm.
|
|
1467
|
+
* @param {Array} ast
|
|
1468
|
+
* @returns {Array}
|
|
1469
|
+
*/
|
|
1470
|
+
const brif = (ast) => {
|
|
1471
|
+
return walkPost(clone(ast), (node) => {
|
|
1472
|
+
if (!Array.isArray(node) || node[0] !== 'if') return
|
|
1473
|
+
const { cond, thenBranch, elseBranch } = parseIf(node)
|
|
1474
|
+
const thenEmpty = !thenBranch || thenBranch.length <= 1
|
|
1475
|
+
const elseEmpty = !elseBranch || elseBranch.length <= 1
|
|
1476
|
+
|
|
1477
|
+
// (if cond (then (br $l))) → (br_if $l cond)
|
|
1478
|
+
if (!thenEmpty && elseEmpty && thenBranch.length === 2) {
|
|
1479
|
+
const t = thenBranch[1]
|
|
1480
|
+
if (Array.isArray(t) && t[0] === 'br' && t.length === 2) return ['br_if', t[1], cond]
|
|
1481
|
+
}
|
|
1050
1482
|
|
|
1483
|
+
// (if cond (then) (else (br $l))) → (br_if $l (i32.eqz cond))
|
|
1484
|
+
if (thenEmpty && !elseEmpty && elseBranch.length === 2) {
|
|
1485
|
+
const e = elseBranch[1]
|
|
1486
|
+
if (Array.isArray(e) && e[0] === 'br' && e.length === 2) return ['br_if', e[1], ['i32.eqz', cond]]
|
|
1487
|
+
}
|
|
1488
|
+
})
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ==================== MERGE IDENTICAL IF ARMS ====================
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Fold identical trailing code out of if/else arms.
|
|
1495
|
+
* (if cond (then A X) (else B X)) → (if cond (then A) (else B)) X
|
|
1496
|
+
* @param {Array} ast
|
|
1497
|
+
* @returns {Array}
|
|
1498
|
+
*/
|
|
1499
|
+
const foldarms = (ast) => {
|
|
1500
|
+
return walkPost(clone(ast), (node) => {
|
|
1501
|
+
if (!Array.isArray(node) || node[0] !== 'if') return
|
|
1502
|
+
const { thenBranch, elseBranch } = parseIf(node)
|
|
1503
|
+
if (!thenBranch || !elseBranch) return
|
|
1504
|
+
if (thenBranch.length <= 1 || elseBranch.length <= 1) return
|
|
1505
|
+
|
|
1506
|
+
let common = 0
|
|
1507
|
+
const minLen = Math.min(thenBranch.length, elseBranch.length)
|
|
1508
|
+
for (let i = 1; i < minLen; i++) {
|
|
1509
|
+
if (!equal(thenBranch[thenBranch.length - i], elseBranch[elseBranch.length - i])) break
|
|
1510
|
+
common++
|
|
1511
|
+
}
|
|
1512
|
+
if (common === 0) return
|
|
1513
|
+
|
|
1514
|
+
const hoisted = thenBranch.slice(thenBranch.length - common)
|
|
1515
|
+
const newThen = thenBranch.slice(0, thenBranch.length - common)
|
|
1516
|
+
const newElse = elseBranch.slice(0, elseBranch.length - common)
|
|
1517
|
+
|
|
1518
|
+
const block = ['block']
|
|
1519
|
+
for (let i = 1; i < node.length; i++) {
|
|
1520
|
+
const c = node[i]
|
|
1521
|
+
if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
|
|
1522
|
+
if (Array.isArray(c) && (c[0] === 'result' || c[0] === 'type')) block.push(c)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const newIf = ['if']
|
|
1526
|
+
for (let i = 1; i < node.length; i++) {
|
|
1527
|
+
const c = node[i]
|
|
1528
|
+
if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
|
|
1529
|
+
newIf.push(c)
|
|
1530
|
+
}
|
|
1531
|
+
newIf.push(newThen.length > 1 ? newThen : ['then'])
|
|
1532
|
+
newIf.push(newElse.length > 1 ? newElse : ['else'])
|
|
1533
|
+
|
|
1534
|
+
block.push(newIf, ...hoisted)
|
|
1535
|
+
return block
|
|
1536
|
+
})
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// ==================== DUPLICATE FUNCTION ELIMINATION ====================
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Fast structural hash for a function node, normalizing local names.
|
|
1543
|
+
* Uses a stack-based walk to avoid expensive JSON.stringify.
|
|
1544
|
+
*/
|
|
1545
|
+
const hashFunc = (node, localNames) => {
|
|
1546
|
+
const parts = []
|
|
1547
|
+
const stack = [node]
|
|
1548
|
+
while (stack.length) {
|
|
1549
|
+
const v = stack.pop()
|
|
1550
|
+
if (Array.isArray(v)) {
|
|
1551
|
+
stack.push('|')
|
|
1552
|
+
for (let i = v.length - 1; i >= 0; i--) stack.push(v[i])
|
|
1553
|
+
stack.push('[')
|
|
1554
|
+
} else if (typeof v === 'string') {
|
|
1555
|
+
parts.push(localNames.has(v) ? '$__L' : v)
|
|
1556
|
+
} else if (typeof v === 'bigint') {
|
|
1557
|
+
parts.push(v.toString() + 'n')
|
|
1558
|
+
} else if (typeof v === 'number') {
|
|
1559
|
+
parts.push(v.toString())
|
|
1560
|
+
} else if (v === null) {
|
|
1561
|
+
parts.push('null')
|
|
1562
|
+
} else if (v === true) {
|
|
1563
|
+
parts.push('t')
|
|
1564
|
+
} else if (v === false) {
|
|
1565
|
+
parts.push('f')
|
|
1566
|
+
} else {
|
|
1567
|
+
parts.push(String(v))
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return parts.join(',')
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Eliminate duplicate functions by hashing bodies.
|
|
1575
|
+
* Keeps the first occurrence and redirects all references to it.
|
|
1576
|
+
* @param {Array} ast
|
|
1577
|
+
* @returns {Array}
|
|
1578
|
+
*/
|
|
1579
|
+
const dedupe = (ast) => {
|
|
1580
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1581
|
+
const result = clone(ast)
|
|
1582
|
+
|
|
1583
|
+
// Hash function bodies (normalize local/param names to avoid false negatives)
|
|
1584
|
+
const signatures = new Map() // hash → canonical $name
|
|
1585
|
+
const redirects = new Map() // duplicate $name → canonical $name
|
|
1586
|
+
|
|
1587
|
+
for (const node of result.slice(1)) {
|
|
1588
|
+
if (!Array.isArray(node) || node[0] !== 'func') continue
|
|
1589
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1590
|
+
if (!name) continue
|
|
1591
|
+
|
|
1592
|
+
// Collect names that are internal to this function: the func name itself,
|
|
1593
|
+
// its params, locals, and any block/loop labels nested in the body. All of
|
|
1594
|
+
// these get normalized to a single token in the hash so that two funcs
|
|
1595
|
+
// differing only in identifier choices still dedupe.
|
|
1596
|
+
const localNames = new Set()
|
|
1597
|
+
if (typeof node[1] === 'string' && node[1][0] === '$') localNames.add(node[1])
|
|
1051
1598
|
walk(node, (n) => {
|
|
1052
|
-
if (!Array.isArray(n)) return
|
|
1599
|
+
if (!Array.isArray(n) || typeof n[1] !== 'string' || n[1][0] !== '$') return
|
|
1053
1600
|
const op = n[0]
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (op.startsWith('i32.') || op.startsWith('i64.') || op.startsWith('f32.') || op.startsWith('f64.')) {
|
|
1057
|
-
// Skip simple consts
|
|
1058
|
-
if (op.endsWith('.const')) return
|
|
1059
|
-
// Skip if has side effects (calls, memory ops)
|
|
1060
|
-
let hasSideEffects = false
|
|
1061
|
-
walk(n, (sub) => {
|
|
1062
|
-
if (Array.isArray(sub) && (sub[0] === 'call' || sub[0]?.includes('load') || sub[0]?.includes('store'))) {
|
|
1063
|
-
hasSideEffects = true
|
|
1064
|
-
}
|
|
1065
|
-
})
|
|
1066
|
-
if (hasSideEffects) return
|
|
1067
|
-
|
|
1068
|
-
const hash = exprHash(n)
|
|
1069
|
-
if (seen.has(hash)) {
|
|
1070
|
-
seen.get(hash).count++
|
|
1071
|
-
} else {
|
|
1072
|
-
seen.set(hash, { node: n, count: 1 })
|
|
1073
|
-
}
|
|
1601
|
+
if (op === 'param' || op === 'local' || op === 'block' || op === 'loop' || op === 'if') {
|
|
1602
|
+
localNames.add(n[1])
|
|
1074
1603
|
}
|
|
1075
1604
|
})
|
|
1076
1605
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1606
|
+
const hash = hashFunc(node, localNames)
|
|
1607
|
+
|
|
1608
|
+
if (signatures.has(hash)) {
|
|
1609
|
+
redirects.set(name, signatures.get(hash))
|
|
1610
|
+
} else {
|
|
1611
|
+
signatures.set(hash, name)
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (redirects.size === 0) return result
|
|
1616
|
+
|
|
1617
|
+
// Rewrite all references: calls, ref.func, elem segments, call_indirect type
|
|
1618
|
+
walkPost(result, (node) => {
|
|
1619
|
+
if (!Array.isArray(node)) return
|
|
1620
|
+
const op = node[0]
|
|
1621
|
+
if ((op === 'call' || op === 'return_call') && redirects.has(node[1])) {
|
|
1622
|
+
return [op, redirects.get(node[1]), ...node.slice(2)]
|
|
1623
|
+
}
|
|
1624
|
+
if (op === 'ref.func' && redirects.has(node[1])) {
|
|
1625
|
+
return ['ref.func', redirects.get(node[1])]
|
|
1626
|
+
}
|
|
1627
|
+
if (op === 'elem') {
|
|
1628
|
+
const funcs = node[node.length - 1]
|
|
1629
|
+
if (Array.isArray(funcs)) {
|
|
1630
|
+
return [...node.slice(0, -1), funcs.map(f => redirects.get(f) || f)]
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (op === 'call_indirect' && node.length >= 3) {
|
|
1634
|
+
const typeRef = node[1]
|
|
1635
|
+
if (typeof typeRef === 'string' && redirects.has(typeRef)) {
|
|
1636
|
+
return ['call_indirect', redirects.get(typeRef), ...node.slice(2)]
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1079
1639
|
})
|
|
1080
1640
|
|
|
1081
1641
|
return result
|
|
1082
1642
|
}
|
|
1083
1643
|
|
|
1084
|
-
// ====================
|
|
1644
|
+
// ==================== TYPE DEDUPLICATION ====================
|
|
1085
1645
|
|
|
1086
1646
|
/**
|
|
1087
|
-
*
|
|
1647
|
+
* Merge structurally identical (type ...) definitions.
|
|
1648
|
+
* Keeps the first occurrence and redirects all references.
|
|
1088
1649
|
* @param {Array} ast
|
|
1089
1650
|
* @returns {Array}
|
|
1090
1651
|
*/
|
|
1091
|
-
const
|
|
1652
|
+
const dedupTypes = (ast) => {
|
|
1653
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1092
1654
|
const result = clone(ast)
|
|
1093
1655
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1656
|
+
const signatures = new Map() // hash → canonical $name
|
|
1657
|
+
const redirects = new Map() // duplicate $name → canonical $name
|
|
1096
1658
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1659
|
+
for (const node of result.slice(1)) {
|
|
1660
|
+
if (!Array.isArray(node) || node[0] !== 'type') continue
|
|
1661
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1662
|
+
if (!name) continue
|
|
1100
1663
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
walk(loopNode, (n) => {
|
|
1104
|
-
if (!Array.isArray(n)) return
|
|
1105
|
-
if (n[0] === 'local.set' || n[0] === 'local.tee') {
|
|
1106
|
-
if (typeof n[1] === 'string') modifiedLocals.add(n[1])
|
|
1107
|
-
}
|
|
1108
|
-
})
|
|
1664
|
+
// Hash the type body, normalizing only the type's own name
|
|
1665
|
+
const hash = hashFunc(node, new Set([name]))
|
|
1109
1666
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1667
|
+
if (signatures.has(hash)) {
|
|
1668
|
+
redirects.set(name, signatures.get(hash))
|
|
1669
|
+
} else {
|
|
1670
|
+
signatures.set(hash, name)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1112
1673
|
|
|
1113
|
-
|
|
1114
|
-
const instr = loopNode[i]
|
|
1115
|
-
if (!Array.isArray(instr)) continue
|
|
1674
|
+
if (redirects.size === 0) return result
|
|
1116
1675
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1676
|
+
// Remove duplicate type nodes
|
|
1677
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
1678
|
+
const node = result[i]
|
|
1679
|
+
if (Array.isArray(node) && node[0] === 'type') {
|
|
1680
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
1681
|
+
if (name && redirects.has(name)) result.splice(i, 1)
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1120
1684
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1685
|
+
walkPost(result, (node) => {
|
|
1686
|
+
if (!Array.isArray(node)) return
|
|
1687
|
+
const op = node[0]
|
|
1124
1688
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
isInvariant = false
|
|
1135
|
-
}
|
|
1136
|
-
})
|
|
1689
|
+
// (func $f (type $t) ...)
|
|
1690
|
+
if (op === 'func') {
|
|
1691
|
+
for (let i = 1; i < node.length; i++) {
|
|
1692
|
+
const sub = node[i]
|
|
1693
|
+
if (Array.isArray(sub) && sub[0] === 'type' && typeof sub[1] === 'string' && redirects.has(sub[1])) {
|
|
1694
|
+
node[i] = ['type', redirects.get(sub[1])]
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1137
1698
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1699
|
+
// (import "m" "n" (func (type $t)))
|
|
1700
|
+
if (op === 'import') {
|
|
1701
|
+
for (let i = 1; i < node.length; i++) {
|
|
1702
|
+
const sub = node[i]
|
|
1703
|
+
if (Array.isArray(sub)) {
|
|
1704
|
+
for (let j = 1; j < sub.length; j++) {
|
|
1705
|
+
const inner = sub[j]
|
|
1706
|
+
if (Array.isArray(inner) && inner[0] === 'type' && typeof inner[1] === 'string' && redirects.has(inner[1])) {
|
|
1707
|
+
sub[j] = ['type', redirects.get(inner[1])]
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1141
1710
|
}
|
|
1142
1711
|
}
|
|
1712
|
+
}
|
|
1143
1713
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1714
|
+
// call_indirect $t or (call_indirect (type $t) ...)
|
|
1715
|
+
if (op === 'call_indirect' || op === 'return_call_indirect') {
|
|
1716
|
+
if (typeof node[1] === 'string' && redirects.has(node[1])) {
|
|
1717
|
+
return [op, redirects.get(node[1]), ...node.slice(2)]
|
|
1718
|
+
}
|
|
1719
|
+
if (Array.isArray(node[1]) && node[1][0] === 'type' && typeof node[1][1] === 'string' && redirects.has(node[1][1])) {
|
|
1720
|
+
return [op, ['type', redirects.get(node[1][1])], ...node.slice(2)]
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1147
1723
|
})
|
|
1148
1724
|
|
|
1149
1725
|
return result
|
|
1150
1726
|
}
|
|
1151
1727
|
|
|
1728
|
+
// ==================== DATA SEGMENT PACKING ====================
|
|
1729
|
+
|
|
1730
|
+
/** Parse a WAT data string literal into Uint8Array */
|
|
1731
|
+
const parseDataString = (str) => {
|
|
1732
|
+
if (typeof str !== 'string' || str.length < 2 || str[0] !== '"') return new Uint8Array()
|
|
1733
|
+
const inner = str.slice(1, -1)
|
|
1734
|
+
const bytes = []
|
|
1735
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1736
|
+
if (inner[i] === '\\') {
|
|
1737
|
+
const next = inner[++i]
|
|
1738
|
+
if (next === 'x' || next === 'X') {
|
|
1739
|
+
bytes.push(parseInt(inner.slice(i + 1, i + 3), 16))
|
|
1740
|
+
i += 2
|
|
1741
|
+
} else if (/[0-9a-fA-F]/.test(next) && /[0-9a-fA-F]/.test(inner[i + 1])) {
|
|
1742
|
+
bytes.push(parseInt(inner.slice(i, i + 2), 16))
|
|
1743
|
+
i++
|
|
1744
|
+
} else if (next === 'n') bytes.push(10)
|
|
1745
|
+
else if (next === 't') bytes.push(9)
|
|
1746
|
+
else if (next === 'r') bytes.push(13)
|
|
1747
|
+
else if (next === '\\') bytes.push(92)
|
|
1748
|
+
else if (next === '"') bytes.push(34)
|
|
1749
|
+
else bytes.push(next.charCodeAt(0))
|
|
1750
|
+
} else {
|
|
1751
|
+
bytes.push(inner.charCodeAt(i))
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return new Uint8Array(bytes)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** Encode Uint8Array as WAT data string literal */
|
|
1758
|
+
const encodeDataString = (bytes) => {
|
|
1759
|
+
let str = '"'
|
|
1760
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1761
|
+
const b = bytes[i]
|
|
1762
|
+
if (b >= 32 && b < 127 && b !== 34 && b !== 92) {
|
|
1763
|
+
str += String.fromCharCode(b)
|
|
1764
|
+
} else {
|
|
1765
|
+
str += '\\' + b.toString(16).padStart(2, '0')
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return str + '"'
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/** Trim trailing zeros from data content items */
|
|
1772
|
+
const trimTrailingZeros = (items) => {
|
|
1773
|
+
const bytes = []
|
|
1774
|
+
for (const item of items) {
|
|
1775
|
+
if (typeof item === 'string') {
|
|
1776
|
+
bytes.push(...parseDataString(item))
|
|
1777
|
+
} else if (Array.isArray(item) && item[0] === 'i8') {
|
|
1778
|
+
for (let i = 1; i < item.length; i++) bytes.push(Number(item[i]) & 0xff)
|
|
1779
|
+
} else {
|
|
1780
|
+
return items // non-trimmable item
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
let end = bytes.length
|
|
1784
|
+
while (end > 0 && bytes[end - 1] === 0) end--
|
|
1785
|
+
if (end === bytes.length) return items
|
|
1786
|
+
if (end === 0) return []
|
|
1787
|
+
return [encodeDataString(new Uint8Array(bytes.slice(0, end)))]
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/** Extract { memidx, offset } from an active data segment with constant offset */
|
|
1791
|
+
const getDataOffset = (node) => {
|
|
1792
|
+
let idx = 1
|
|
1793
|
+
if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
|
|
1794
|
+
if (Array.isArray(node[idx]) && node[idx][0] === 'memory') {
|
|
1795
|
+
const mem = node[idx][1]
|
|
1796
|
+
idx++
|
|
1797
|
+
const off = node[idx]
|
|
1798
|
+
if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
|
|
1799
|
+
return { memidx: mem, offset: Number(off[1]) }
|
|
1800
|
+
}
|
|
1801
|
+
return null
|
|
1802
|
+
}
|
|
1803
|
+
const off = node[idx]
|
|
1804
|
+
if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
|
|
1805
|
+
return { memidx: 0, offset: Number(off[1]) }
|
|
1806
|
+
}
|
|
1807
|
+
return null
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/** Get byte length of data segment content */
|
|
1811
|
+
const getDataLength = (node) => {
|
|
1812
|
+
let idx = 1
|
|
1813
|
+
if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
|
|
1814
|
+
if (Array.isArray(node[idx]) && node[idx][0] === 'memory') idx++
|
|
1815
|
+
if (Array.isArray(node[idx]) && typeof node[idx][0] === 'string' && !node[idx][0].startsWith('"')) idx++
|
|
1816
|
+
let len = 0
|
|
1817
|
+
for (let i = idx; i < node.length; i++) {
|
|
1818
|
+
const item = node[i]
|
|
1819
|
+
if (typeof item === 'string') len += parseDataString(item).length
|
|
1820
|
+
else if (Array.isArray(item) && item[0] === 'i8') len += item.length - 1
|
|
1821
|
+
else return null
|
|
1822
|
+
}
|
|
1823
|
+
return len
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/** Merge segment b into a (consecutive offsets, same memory) */
|
|
1827
|
+
const mergeDataSegments = (a, b) => {
|
|
1828
|
+
let aIdx = 1
|
|
1829
|
+
if (typeof a[aIdx] === 'string' && a[aIdx][0] === '$') aIdx++
|
|
1830
|
+
if (Array.isArray(a[aIdx]) && a[aIdx][0] === 'memory') aIdx++
|
|
1831
|
+
if (Array.isArray(a[aIdx]) && typeof a[aIdx][0] === 'string' && !a[aIdx][0].startsWith('"')) aIdx++
|
|
1832
|
+
|
|
1833
|
+
let bIdx = 1
|
|
1834
|
+
if (typeof b[bIdx] === 'string' && b[bIdx][0] === '$') bIdx++
|
|
1835
|
+
if (Array.isArray(b[bIdx]) && b[bIdx][0] === 'memory') bIdx++
|
|
1836
|
+
if (Array.isArray(b[bIdx]) && typeof b[bIdx][0] === 'string' && !b[bIdx][0].startsWith('"')) bIdx++
|
|
1837
|
+
|
|
1838
|
+
const aContent = a.slice(aIdx)
|
|
1839
|
+
const bContent = b.slice(bIdx)
|
|
1840
|
+
|
|
1841
|
+
if (aContent.length === 1 && bContent.length === 1 &&
|
|
1842
|
+
typeof aContent[0] === 'string' && typeof bContent[0] === 'string') {
|
|
1843
|
+
const aBytes = parseDataString(aContent[0])
|
|
1844
|
+
const bBytes = parseDataString(bContent[0])
|
|
1845
|
+
const merged = new Uint8Array(aBytes.length + bBytes.length)
|
|
1846
|
+
merged.set(aBytes)
|
|
1847
|
+
merged.set(bBytes, aBytes.length)
|
|
1848
|
+
a.length = aIdx
|
|
1849
|
+
a.push(encodeDataString(merged))
|
|
1850
|
+
return true
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
a.length = aIdx
|
|
1854
|
+
a.push(...aContent, ...bContent)
|
|
1855
|
+
return true
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Pack data segments: trim trailing zeros and merge adjacent constant-offset segments.
|
|
1860
|
+
* @param {Array} ast
|
|
1861
|
+
* @returns {Array}
|
|
1862
|
+
*/
|
|
1863
|
+
const packData = (ast) => {
|
|
1864
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1865
|
+
let result = clone(ast)
|
|
1866
|
+
|
|
1867
|
+
// Trim trailing zeros
|
|
1868
|
+
for (const node of result) {
|
|
1869
|
+
if (!Array.isArray(node) || node[0] !== 'data') continue
|
|
1870
|
+
let contentStart = 1
|
|
1871
|
+
if (typeof node[1] === 'string' && node[1][0] === '$') contentStart = 2
|
|
1872
|
+
if (contentStart < node.length && Array.isArray(node[contentStart]) &&
|
|
1873
|
+
typeof node[contentStart][0] === 'string' && !node[contentStart][0].startsWith('"')) {
|
|
1874
|
+
contentStart++
|
|
1875
|
+
}
|
|
1876
|
+
const content = node.slice(contentStart)
|
|
1877
|
+
if (content.length === 0) continue
|
|
1878
|
+
const trimmed = trimTrailingZeros(content)
|
|
1879
|
+
if (trimmed.length !== content.length || (trimmed.length > 0 && trimmed[0] !== content[0])) {
|
|
1880
|
+
node.length = contentStart
|
|
1881
|
+
node.push(...trimmed)
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Merge adjacent active segments with same memory and consecutive offsets
|
|
1886
|
+
const dataNodes = []
|
|
1887
|
+
for (let i = 0; i < result.length; i++) {
|
|
1888
|
+
const node = result[i]
|
|
1889
|
+
if (Array.isArray(node) && node[0] === 'data') {
|
|
1890
|
+
const info = getDataOffset(node)
|
|
1891
|
+
if (info) {
|
|
1892
|
+
const len = getDataLength(node)
|
|
1893
|
+
if (len !== null) dataNodes.push({ ...info, node, index: i, len })
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
dataNodes.sort((a, b) => {
|
|
1899
|
+
const ma = String(a.memidx), mb = String(b.memidx)
|
|
1900
|
+
if (ma !== mb) return ma.localeCompare(mb)
|
|
1901
|
+
return a.offset - b.offset
|
|
1902
|
+
})
|
|
1903
|
+
|
|
1904
|
+
const toRemove = new Set()
|
|
1905
|
+
for (let i = 0; i < dataNodes.length - 1; i++) {
|
|
1906
|
+
const a = dataNodes[i]
|
|
1907
|
+
const b = dataNodes[i + 1]
|
|
1908
|
+
if (toRemove.has(a.index) || String(a.memidx) !== String(b.memidx)) continue
|
|
1909
|
+
if (a.offset + a.len !== b.offset) continue
|
|
1910
|
+
if (mergeDataSegments(a.node, b.node)) {
|
|
1911
|
+
toRemove.add(b.index)
|
|
1912
|
+
a.len = getDataLength(a.node)
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (toRemove.size > 0) {
|
|
1917
|
+
result = result.filter((_, i) => !toRemove.has(i))
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return result
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// ==================== IMPORT FIELD MINIFICATION ====================
|
|
1924
|
+
|
|
1925
|
+
/** Create a shortener that maps names to a, b, ..., z, aa, ab, ... */
|
|
1926
|
+
const makeShortener = () => {
|
|
1927
|
+
const map = new Map()
|
|
1928
|
+
let n = 0
|
|
1929
|
+
return (name) => {
|
|
1930
|
+
if (!map.has(name)) {
|
|
1931
|
+
let id = '', x = n++
|
|
1932
|
+
do {
|
|
1933
|
+
id = String.fromCharCode(97 + (x % 26)) + id
|
|
1934
|
+
x = Math.floor(x / 26) - 1
|
|
1935
|
+
} while (x >= 0)
|
|
1936
|
+
map.set(name, id || 'a')
|
|
1937
|
+
}
|
|
1938
|
+
return map.get(name)
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/**
|
|
1943
|
+
* Minify import module and field names for smaller binaries.
|
|
1944
|
+
* Only safe when you control the host environment.
|
|
1945
|
+
* @param {Array} ast
|
|
1946
|
+
* @returns {Array}
|
|
1947
|
+
*/
|
|
1948
|
+
const minifyImports = (ast) => {
|
|
1949
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1950
|
+
const result = clone(ast)
|
|
1951
|
+
const shortMod = makeShortener()
|
|
1952
|
+
const shortField = makeShortener()
|
|
1953
|
+
|
|
1954
|
+
for (const node of result) {
|
|
1955
|
+
if (!Array.isArray(node) || node[0] !== 'import') continue
|
|
1956
|
+
if (typeof node[1] === 'string' && node[1][0] === '"') {
|
|
1957
|
+
node[1] = '"' + shortMod(node[1].slice(1, -1)) + '"'
|
|
1958
|
+
}
|
|
1959
|
+
if (typeof node[2] === 'string' && node[2][0] === '"') {
|
|
1960
|
+
node[2] = '"' + shortField(node[2].slice(1, -1)) + '"'
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return result
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// ==================== REORDER FUNCTIONS ====================
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Count direct calls and sort functions so hot ones come first.
|
|
1971
|
+
* Smaller LEB128 indices for frequent calls reduce binary size.
|
|
1972
|
+
* Imports must stay before defined functions to preserve the index space.
|
|
1973
|
+
* @param {Array} ast
|
|
1974
|
+
* @returns {Array}
|
|
1975
|
+
*/
|
|
1976
|
+
/** True iff every defined func has a $name and every func reference is by $name */
|
|
1977
|
+
const reorderSafe = (ast) => {
|
|
1978
|
+
let safe = true
|
|
1979
|
+
walk(ast, (n) => {
|
|
1980
|
+
if (!safe || !Array.isArray(n)) return
|
|
1981
|
+
const op = n[0]
|
|
1982
|
+
if (op === 'func' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
|
|
1983
|
+
else if ((op === 'call' || op === 'return_call' || op === 'ref.func') &&
|
|
1984
|
+
(typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
|
|
1985
|
+
else if (op === 'start' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
|
|
1986
|
+
else if (op === 'elem') {
|
|
1987
|
+
// Numeric func indices in elem segments would break too
|
|
1988
|
+
for (const sub of n) {
|
|
1989
|
+
if (typeof sub === 'string' && sub[0] !== '$' && /^\d/.test(sub)) { safe = false; break }
|
|
1990
|
+
if (Array.isArray(sub) && sub[0] === 'ref.func' &&
|
|
1991
|
+
(typeof sub[1] !== 'string' || sub[1][0] !== '$')) { safe = false; break }
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
})
|
|
1995
|
+
return safe
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
const reorder = (ast) => {
|
|
1999
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
2000
|
+
// Sorting changes the function index space. Skip if any reference is numeric,
|
|
2001
|
+
// since we'd silently retarget unnamed callers/start/elem entries.
|
|
2002
|
+
if (!reorderSafe(ast)) return ast
|
|
2003
|
+
const result = clone(ast)
|
|
2004
|
+
|
|
2005
|
+
const callCounts = new Map()
|
|
2006
|
+
walk(result, (n) => {
|
|
2007
|
+
if (!Array.isArray(n)) return
|
|
2008
|
+
if (n[0] === 'call' || n[0] === 'return_call') {
|
|
2009
|
+
callCounts.set(n[1], (callCounts.get(n[1]) || 0) + 1)
|
|
2010
|
+
}
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
// Imports must precede defined funcs (compile.js assigns indices in AST order).
|
|
2014
|
+
const imports = [], funcs = [], others = []
|
|
2015
|
+
for (const node of result.slice(1)) {
|
|
2016
|
+
if (!Array.isArray(node)) { others.push(node); continue }
|
|
2017
|
+
if (node[0] === 'import') imports.push(node)
|
|
2018
|
+
else if (node[0] === 'func') funcs.push(node)
|
|
2019
|
+
else others.push(node)
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
funcs.sort((a, b) => (callCounts.get(b[1]) || 0) - (callCounts.get(a[1]) || 0))
|
|
2023
|
+
return ['module', ...imports, ...funcs, ...others]
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1152
2026
|
// ==================== MAIN ====================
|
|
1153
2027
|
|
|
1154
2028
|
/**
|
|
@@ -1168,17 +2042,38 @@ export default function optimize(ast, opts = true) {
|
|
|
1168
2042
|
ast = clone(ast)
|
|
1169
2043
|
opts = normalize(opts)
|
|
1170
2044
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
2045
|
+
// Each pass clones its input before mutating, so the original `before`
|
|
2046
|
+
// reference stays untouched and can be used for the convergence check
|
|
2047
|
+
// without an extra deep clone.
|
|
2048
|
+
for (let round = 0; round < 6; round++) {
|
|
2049
|
+
const before = ast
|
|
2050
|
+
|
|
2051
|
+
if (opts.stripmut) ast = stripmut(ast)
|
|
2052
|
+
if (opts.globals) ast = globals(ast)
|
|
2053
|
+
if (opts.fold) ast = fold(ast)
|
|
2054
|
+
if (opts.identity) ast = identity(ast)
|
|
2055
|
+
if (opts.peephole) ast = peephole(ast)
|
|
2056
|
+
if (opts.strength) ast = strength(ast)
|
|
2057
|
+
if (opts.branch) ast = branch(ast)
|
|
2058
|
+
if (opts.propagate) ast = propagate(ast)
|
|
2059
|
+
if (opts.inline) ast = inline(ast)
|
|
2060
|
+
if (opts.offset) ast = offset(ast)
|
|
2061
|
+
if (opts.unbranch) ast = unbranch(ast)
|
|
2062
|
+
if (opts.brif) ast = brif(ast)
|
|
2063
|
+
if (opts.foldarms) ast = foldarms(ast)
|
|
2064
|
+
if (opts.deadcode) ast = deadcode(ast)
|
|
2065
|
+
if (opts.vacuum) ast = vacuum(ast)
|
|
2066
|
+
if (opts.locals) ast = localReuse(ast)
|
|
2067
|
+
if (opts.dedupe) ast = dedupe(ast)
|
|
2068
|
+
if (opts.dedupTypes) ast = dedupTypes(ast)
|
|
2069
|
+
if (opts.packData) ast = packData(ast)
|
|
2070
|
+
if (opts.reorder) ast = reorder(ast)
|
|
2071
|
+
if (opts.treeshake) ast = treeshake(ast)
|
|
2072
|
+
if (opts.minifyImports) ast = minifyImports(ast)
|
|
2073
|
+
if (equal(before, ast)) break
|
|
2074
|
+
}
|
|
1180
2075
|
|
|
1181
2076
|
return ast
|
|
1182
2077
|
}
|
|
1183
2078
|
|
|
1184
|
-
export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS }
|
|
2079
|
+
export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS, vacuum, peephole, globals, offset, unbranch, stripmut, brif, foldarms, dedupe, reorder, dedupTypes, packData, minifyImports }
|