watr 4.6.10 → 4.7.1
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 +1138 -383
- package/dist/watr.min.js +6 -6
- package/dist/watr.wasm +0 -0
- package/package.json +8 -4
- package/src/compile.js +93 -26
- package/src/optimize.js +1358 -395
- package/types/src/compile.d.ts +5 -1
- package/types/src/compile.d.ts.map +1 -1
- package/types/src/optimize.d.ts +34 -20
- package/types/src/optimize.d.ts.map +1 -1
package/src/optimize.js
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AST
|
|
3
|
-
* Reduces code size and improves runtime performance.
|
|
2
|
+
* WAT AST optimizer — size/runtime passes over watr's s-expression IR.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* jz owns its optimizer; watr is used *only* as the WAT→binary encoder.
|
|
5
|
+
* Pairs with src/optimize/ (jz-IR-level) — folder context disambiguates.
|
|
6
|
+
*
|
|
7
|
+
* @module wat/optimize
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import parse from './parse.js'
|
|
9
10
|
import compile from './compile.js'
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import parse from './parse.js'
|
|
12
|
+
|
|
13
|
+
// Fixpoint round caps — empirical convergence bounds, not correctness limits.
|
|
14
|
+
// Each pass only makes monotonic progress, so hitting a cap merely leaves a few
|
|
15
|
+
// residual simplifications for the next compile rather than producing wrong output.
|
|
16
|
+
const MAX_PROP_ROUNDS = 6 // forward-prop / set-get / tee fixpoint per scope
|
|
17
|
+
const MAX_INLINE_ROUNDS = 16 // single-caller inline-chain depth (deep generated stdlib)
|
|
18
|
+
|
|
19
|
+
// === WAT optimizer passes ===
|
|
20
|
+
|
|
21
|
+
// — AST helpers (formerly watr/util.js) — every node is an s-expression
|
|
22
|
+
// array `[head, ...args]`; non-arrays are immediates.
|
|
23
|
+
const clone = (node) => Array.isArray(node) ? node.map(clone) : node
|
|
24
|
+
|
|
25
|
+
/** Walk depth-first pre-order, read-only. fn(node, parent, idx). */
|
|
26
|
+
const walk = (node, fn, parent, idx) => {
|
|
27
|
+
fn(node, parent, idx)
|
|
28
|
+
if (Array.isArray(node)) for (let i = 0; i < node.length; i++) walk(node[i], fn, node, i)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Walk depth-first post-order. fn may return a replacement node or mutate in place. */
|
|
32
|
+
const walkPost = (node, fn, parent, idx) => {
|
|
33
|
+
if (Array.isArray(node)) for (let i = 0; i < node.length; i++) walkPost(node[i], fn, node, i)
|
|
34
|
+
const result = fn(node, parent, idx)
|
|
35
|
+
if (result !== undefined && parent) parent[idx] = result
|
|
36
|
+
return result !== undefined ? result : node
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Result value type of an op from its name prefix (formerly watr/const.js).
|
|
40
|
+
* Comparisons/eqz on scalar int/float collapse to i32; else the name prefix. */
|
|
41
|
+
const resultType = (op) => {
|
|
42
|
+
if (typeof op !== 'string') return null
|
|
43
|
+
const dot = op.indexOf('.')
|
|
44
|
+
if (dot < 0) return null
|
|
45
|
+
const prefix = op.slice(0, dot)
|
|
46
|
+
const scalar = prefix === 'i32' || prefix === 'i64' || prefix === 'f32' || prefix === 'f64'
|
|
47
|
+
if (scalar && /^(eqz?|ne|[lg][te])(_[su])?$/.test(op.slice(dot + 1))) return 'i32'
|
|
48
|
+
if (scalar || prefix === 'v128') return prefix
|
|
49
|
+
if (op === 'memory.size' || op === 'memory.grow') return 'i32'
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
13
52
|
|
|
14
53
|
/**
|
|
15
54
|
* Recursively count AST nodes — fast size heuristic without compiling.
|
|
@@ -234,14 +273,86 @@ const treeshake = (ast) => {
|
|
|
234
273
|
const roundEven = (x) => x - Math.floor(x) !== 0.5 ? Math.round(x) : 2 * Math.round(x / 2)
|
|
235
274
|
|
|
236
275
|
// Bit-exact reinterpret helpers (preserve NaN payloads).
|
|
276
|
+
//
|
|
277
|
+
// SELF-HOST CONTRACT: this file runs inside the jz kernel, whose BigInt is a
|
|
278
|
+
// raw mod-2^64 i64 carrier — BigInt64Array views are a legacy f64-value shim
|
|
279
|
+
// (reads return the FLOAT, not the bits), decimal stringification of >2^53
|
|
280
|
+
// values and asIntN/asUintN are unfaithful, and adding 2^64 wraps to +0.
|
|
281
|
+
// Everything here therefore sticks to the verified-faithful surface:
|
|
282
|
+
// Uint32Array aliasing, hex toString(16)/parseInt(,16)/padStart, BigInt('0x…')
|
|
283
|
+
// construction, BigInt ===/< comparison, and string arithmetic for two's
|
|
284
|
+
// complement. The signed canonicalization `v > MAX_I64 → v − 2^64` is exact
|
|
285
|
+
// natively AND a no-op in-kernel (2^64 ≡ 0 there) — correct in both worlds.
|
|
237
286
|
const _rb8 = new ArrayBuffer(8)
|
|
238
287
|
const _rf64 = new Float64Array(_rb8)
|
|
239
|
-
const
|
|
288
|
+
const _ru32 = new Uint32Array(_rb8) // LE halves: [0]=lo, [1]=hi
|
|
240
289
|
const _rb4 = new ArrayBuffer(4)
|
|
241
290
|
const _rf32 = new Float32Array(_rb4)
|
|
242
291
|
const _ri32 = new Int32Array(_rb4)
|
|
243
|
-
const
|
|
244
|
-
|
|
292
|
+
const _hex8 = (u) => (u >>> 0).toString(16).padStart(8, '0')
|
|
293
|
+
/** Two's complement of a 16-digit hex magnitude — pure string math. */
|
|
294
|
+
const _twosComp16 = (mag) => {
|
|
295
|
+
let out = '', carry = 1
|
|
296
|
+
for (let i = 15; i >= 0; i--) {
|
|
297
|
+
const d = (15 - parseInt(mag[i], 16)) + carry
|
|
298
|
+
out = (d & 15).toString(16) + out
|
|
299
|
+
carry = d >> 4
|
|
300
|
+
}
|
|
301
|
+
return out
|
|
302
|
+
}
|
|
303
|
+
/** Bits of an i64 BigInt (any sign) as a 16-digit hex string. Takes BigInt,
|
|
304
|
+
* returns STRING — safe to call across kernel function boundaries (strings
|
|
305
|
+
* are tagged; raw BigInts lose their kind at returns/polymorphic slots). */
|
|
306
|
+
const _i64Hex16 = (v) => {
|
|
307
|
+
const h = v.toString(16)
|
|
308
|
+
return h[0] === '-' ? _twosComp16(h.slice(1).padStart(16, '0')) : h.padStart(16, '0')
|
|
309
|
+
}
|
|
310
|
+
// ============================== i64 VALUE CONTRACT ==========================
|
|
311
|
+
// Within this optimizer an i64 const VALUE is the canonical STRING
|
|
312
|
+
// '0x' + 16 lowercase hex digits (the raw bits). Strings survive every kernel
|
|
313
|
+
// boundary; a BigInt held in a polymorphic slot ({type,value}), an untyped
|
|
314
|
+
// param, or a return value is kind-erased in-kernel and every subsequent
|
|
315
|
+
// BigInt op on it misdispatches. BigInt math is constructed AND consumed
|
|
316
|
+
// inside single folder bodies only; folders return hex strings (or null).
|
|
317
|
+
const ZERO64 = '0x0000000000000000', ONE64 = '0x0000000000000001', NEG164 = '0xffffffffffffffff'
|
|
318
|
+
/** Canonicalize any i64.const node value (number | decimal/hex string | bigint).
|
|
319
|
+
* EVERY _i64Hex16 argument here is a freshly-constructed BigInt: passing the
|
|
320
|
+
* raw polymorphic `val` through would poison _i64Hex16's param kind for ALL
|
|
321
|
+
* callers in-kernel (param types are per-function — one kind-erased call site
|
|
322
|
+
* degrades v.toString(16) to dynamic dispatch on raw bits everywhere). */
|
|
323
|
+
const _i64Canon = (val) => {
|
|
324
|
+
if (typeof val === 'string') {
|
|
325
|
+
const s = val.replaceAll('_', '')
|
|
326
|
+
if (s.length === 18 && s[1] === 'x') return '0x' + s.slice(2).toLowerCase()
|
|
327
|
+
// BigInt() rejects signed-hex ('-0x1' / '+0x2'); split the sign off the magnitude.
|
|
328
|
+
const neg = s[0] === '-', mag = (s[0] === '-' || s[0] === '+') ? s.slice(1) : s
|
|
329
|
+
return '0x' + _i64Hex16(neg ? -BigInt(mag) : BigInt(mag))
|
|
330
|
+
}
|
|
331
|
+
// bigint stragglers (native-only defensive) route through String → fresh BigInt.
|
|
332
|
+
if (typeof val === 'bigint') return '0x' + _i64Hex16(BigInt(String(val)))
|
|
333
|
+
return '0x' + _i64Hex16(BigInt(Math.trunc(val) || 0))
|
|
334
|
+
}
|
|
335
|
+
/** Signed-order key: flip the sign bit, then equal-length hex compares
|
|
336
|
+
* lexicographically in signed order. Pure strings — kernel-safe. */
|
|
337
|
+
const _sb = (h) => (parseInt(h[2], 16) ^ 8).toString(16) + h.slice(3)
|
|
338
|
+
/** Hex-string i64 ops used by several folders — all pure string/number math. */
|
|
339
|
+
const _i64Lo = (h) => parseInt(h.slice(10), 16) | 0
|
|
340
|
+
const _i64HiU = (h) => parseInt(h.slice(2, 10), 16) >>> 0
|
|
341
|
+
/** Hex-encode an i64 fold result (BigInt, any sign/world — see folder note). */
|
|
342
|
+
const _i64Arith = (r) => r == null ? null : '0x' + _i64Hex16(r)
|
|
343
|
+
/** SIGNED i64 BigInt from canon hex — exact natively; the subtract arm is
|
|
344
|
+
* dead in-kernel, where BigInt('0x…') already arrives as the signed carrier. */
|
|
345
|
+
const _sgn = (h) => {
|
|
346
|
+
let v = BigInt(h)
|
|
347
|
+
if (v > 0x7fffffffffffffffn) v = v - 0x8000000000000000n - 0x8000000000000000n
|
|
348
|
+
return v
|
|
349
|
+
}
|
|
350
|
+
const i64FromF64 = (x) => { _rf64[0] = x; return '0x' + _hex8(_ru32[1]) + _hex8(_ru32[0]) }
|
|
351
|
+
const f64FromI64 = (h) => {
|
|
352
|
+
_ru32[1] = parseInt(h.slice(2, 10), 16)
|
|
353
|
+
_ru32[0] = parseInt(h.slice(10), 16)
|
|
354
|
+
return _rf64[0]
|
|
355
|
+
}
|
|
245
356
|
const i32FromF32 = (x) => { _rf32[0] = x; return _ri32[0] }
|
|
246
357
|
const f32FromI32 = (x) => { _ri32[0] = x | 0; return _rf32[0] }
|
|
247
358
|
|
|
@@ -249,10 +360,10 @@ const f32FromI32 = (x) => { _ri32[0] = x | 0; return _rf32[0] }
|
|
|
249
360
|
const i32c = (fn) => (a, b) => fn(a, b) ? 1 : 0
|
|
250
361
|
/** Build unsigned i32 comparison folder */
|
|
251
362
|
const u32c = (fn) => (a, b) => fn(a >>> 0, b >>> 0) ? 1 : 0
|
|
252
|
-
/**
|
|
253
|
-
const i64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
|
|
254
|
-
/**
|
|
255
|
-
const u64c = (fn) => (a, b) => fn(
|
|
363
|
+
/** Signed i64 comparison folder — biased-hex lexicographic (kernel-safe). */
|
|
364
|
+
const i64c = (fn) => (a, b) => fn(_sb(a), _sb(b)) ? 1 : 0
|
|
365
|
+
/** Unsigned i64 comparison folder — canonical hex compares lexicographically. */
|
|
366
|
+
const u64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
|
|
256
367
|
|
|
257
368
|
/**
|
|
258
369
|
* Constant folders, keyed by op. Each entry is the fold function; the result
|
|
@@ -289,26 +400,48 @@ const FOLDABLE = {
|
|
|
289
400
|
'i32.clz': (a) => Math.clz32(a),
|
|
290
401
|
'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
|
|
291
402
|
'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
|
|
292
|
-
'i32.wrap_i64': (a) =>
|
|
403
|
+
'i32.wrap_i64': (a) => _i64Lo(a),
|
|
293
404
|
'i32.extend8_s': (a) => (a << 24) >> 24,
|
|
294
405
|
'i32.extend16_s': (a) => (a << 16) >> 16,
|
|
295
406
|
|
|
296
|
-
// i64
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
'
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
'i64.
|
|
310
|
-
'i64.
|
|
311
|
-
'i64.
|
|
407
|
+
// i64 — hex-string in, hex-string out, BOTH-WORLDS-EXACT arithmetic.
|
|
408
|
+
// BigInts construct locally (in-expression — kernel kind erasure never
|
|
409
|
+
// applies), but two further kernel facts shape every folder:
|
|
410
|
+
// (1) the kernel's BigInt is the mod-2^64 i64 CARRIER: BigInt('0xffff…')
|
|
411
|
+
// arrives NEGATIVE there, so sign-sensitive ops (>>, /, %, unsigned
|
|
412
|
+
// division) diverge unless the value is sign-canonicalized first;
|
|
413
|
+
// (2) BigInt.asIntN/asUintN are unfaithful in-kernel — never used.
|
|
414
|
+
// Ring ops {+,−,×,&,|,^,<<} are mod-2^64-compatible: compute then mask with
|
|
415
|
+
// `& 0xffffffffffffffffn` (native: the wrap; kernel: AND with −1 ≡ no-op).
|
|
416
|
+
// `_sgn` yields the SIGNED value in both worlds (the subtract arm is dead
|
|
417
|
+
// in-kernel — same dead-arm trick as slebSize). shr_u is pure u32-half
|
|
418
|
+
// number math. div_u/rem_u fold only below 2^63 (signed==unsigned there);
|
|
419
|
+
// above, they skip — sound degradation, never a wrong constant.
|
|
420
|
+
'i64.add': (a, b) => _i64Arith((BigInt(a) + BigInt(b)) & 0xffffffffffffffffn),
|
|
421
|
+
'i64.sub': (a, b) => _i64Arith((BigInt(a) - BigInt(b)) & 0xffffffffffffffffn),
|
|
422
|
+
'i64.mul': (a, b) => _i64Arith((BigInt(a) * BigInt(b)) & 0xffffffffffffffffn),
|
|
423
|
+
'i64.div_s': (a, b) => b !== ZERO64 && !(a === '0x8000000000000000' && b === NEG164)
|
|
424
|
+
? _i64Arith((_sgn(a) / _sgn(b)) & 0xffffffffffffffffn) : null,
|
|
425
|
+
'i64.div_u': (a, b) => b !== ZERO64 && !(_i64HiU(a) >>> 31) && !(_i64HiU(b) >>> 31)
|
|
426
|
+
? _i64Arith(BigInt(a) / BigInt(b)) : null,
|
|
427
|
+
'i64.rem_s': (a, b) => b !== ZERO64
|
|
428
|
+
? _i64Arith((_sgn(a) % _sgn(b)) & 0xffffffffffffffffn) : null,
|
|
429
|
+
'i64.rem_u': (a, b) => b !== ZERO64 && !(_i64HiU(a) >>> 31) && !(_i64HiU(b) >>> 31)
|
|
430
|
+
? _i64Arith(BigInt(a) % BigInt(b)) : null,
|
|
431
|
+
'i64.and': (a, b) => _i64Arith(BigInt(a) & BigInt(b) & 0xffffffffffffffffn),
|
|
432
|
+
'i64.or': (a, b) => _i64Arith((BigInt(a) | BigInt(b)) & 0xffffffffffffffffn),
|
|
433
|
+
'i64.xor': (a, b) => _i64Arith((BigInt(a) ^ BigInt(b)) & 0xffffffffffffffffn),
|
|
434
|
+
'i64.shl': (a, b) => _i64Arith((BigInt(a) << (BigInt(b) & 63n)) & 0xffffffffffffffffn),
|
|
435
|
+
'i64.shr_s': (a, b) => _i64Arith((_sgn(a) >> (BigInt(b) & 63n)) & 0xffffffffffffffffn),
|
|
436
|
+
'i64.shr_u': (a, b) => {
|
|
437
|
+
const s = parseInt(b.slice(10), 16) & 63
|
|
438
|
+
const hi = _i64HiU(a), lo = parseInt(a.slice(10), 16) >>> 0
|
|
439
|
+
const rh = s >= 32 ? 0 : hi >>> s
|
|
440
|
+
const rl = s === 0 ? lo : s >= 32 ? hi >>> (s - 32) : ((lo >>> s) | (hi << (32 - s))) >>> 0
|
|
441
|
+
return '0x' + _hex8(rh) + _hex8(rl)
|
|
442
|
+
},
|
|
443
|
+
'i64.eq': (a, b) => a === b ? 1 : 0,
|
|
444
|
+
'i64.ne': (a, b) => a !== b ? 1 : 0,
|
|
312
445
|
'i64.lt_s': i64c((a, b) => a < b),
|
|
313
446
|
'i64.lt_u': u64c((a, b) => a < b),
|
|
314
447
|
'i64.gt_s': i64c((a, b) => a > b),
|
|
@@ -317,12 +450,12 @@ const FOLDABLE = {
|
|
|
317
450
|
'i64.le_u': u64c((a, b) => a <= b),
|
|
318
451
|
'i64.ge_s': i64c((a, b) => a >= b),
|
|
319
452
|
'i64.ge_u': u64c((a, b) => a >= b),
|
|
320
|
-
'i64.eqz': (a) => a ===
|
|
321
|
-
'i64.extend_i32_s': (a) =>
|
|
322
|
-
'i64.extend_i32_u': (a) =>
|
|
323
|
-
'i64.extend8_s':
|
|
324
|
-
'i64.extend16_s':
|
|
325
|
-
'i64.extend32_s':
|
|
453
|
+
'i64.eqz': (a) => a === ZERO64 ? 1 : 0,
|
|
454
|
+
'i64.extend_i32_s': (a) => '0x' + _hex8(a >> 31) + _hex8(a),
|
|
455
|
+
'i64.extend_i32_u': (a) => '0x00000000' + _hex8(a),
|
|
456
|
+
'i64.extend8_s': (a) => { const v = (_i64Lo(a) << 24) >> 24; return '0x' + _hex8(v >> 31) + _hex8(v) },
|
|
457
|
+
'i64.extend16_s': (a) => { const v = (_i64Lo(a) << 16) >> 16; return '0x' + _hex8(v >> 31) + _hex8(v) },
|
|
458
|
+
'i64.extend32_s': (a) => { const v = _i64Lo(a); return '0x' + _hex8(v >> 31) + _hex8(v) },
|
|
326
459
|
|
|
327
460
|
// f32/f64 (NaN/precision-aware via Math.fround)
|
|
328
461
|
'f32.add': (a, b) => Math.fround(a + b),
|
|
@@ -358,12 +491,14 @@ const FOLDABLE = {
|
|
|
358
491
|
// Numeric conversions (value-preserving where representable)
|
|
359
492
|
'f32.convert_i32_s': (a) => Math.fround(a | 0),
|
|
360
493
|
'f32.convert_i32_u': (a) => Math.fround(a >>> 0),
|
|
361
|
-
|
|
362
|
-
|
|
494
|
+
// (hi|0)·2^32 + lo is the exact signed value with ONE rounding at the add —
|
|
495
|
+
// correct f64 conversion semantics, pure number math (kernel-safe).
|
|
496
|
+
'f32.convert_i64_s': (a) => Math.fround((_i64HiU(a) | 0) * 4294967296 + parseInt(a.slice(10), 16)),
|
|
497
|
+
'f32.convert_i64_u': (a) => Math.fround(_i64HiU(a) * 4294967296 + parseInt(a.slice(10), 16)),
|
|
363
498
|
'f64.convert_i32_s': (a) => (a | 0),
|
|
364
499
|
'f64.convert_i32_u': (a) => (a >>> 0),
|
|
365
|
-
'f64.convert_i64_s': (a) =>
|
|
366
|
-
'f64.convert_i64_u': (a) =>
|
|
500
|
+
'f64.convert_i64_s': (a) => (_i64HiU(a) | 0) * 4294967296 + parseInt(a.slice(10), 16),
|
|
501
|
+
'f64.convert_i64_u': (a) => _i64HiU(a) * 4294967296 + parseInt(a.slice(10), 16),
|
|
367
502
|
'f32.demote_f64': (a) => Math.fround(a),
|
|
368
503
|
'f64.promote_f32': (a) => Math.fround(a),
|
|
369
504
|
}
|
|
@@ -375,11 +510,29 @@ const FOLDABLE = {
|
|
|
375
510
|
* (jz, etc.) encode their pointer/sentinel bits into. Returns null if `s` is
|
|
376
511
|
* not a NaN literal so callers can fall through to plain Number parsing.
|
|
377
512
|
*/
|
|
513
|
+
/** Full 64-bit hex of a WAT f64 NaN literal — pure string/number math, the
|
|
514
|
+
* payload double is NEVER materialized. In the self-host kernel a NaN-box bit
|
|
515
|
+
* pattern held as a raw f64 VALUE is indistinguishable from a live pointer
|
|
516
|
+
* (String / property reads misread it), so reinterpret folding must move the
|
|
517
|
+
* bits as TEXT. Returns '0x…' (16 digits) or null when `s` isn't a NaN literal. */
|
|
518
|
+
const _nanBitsHex = (s) => {
|
|
519
|
+
const i = s?.indexOf?.('nan')
|
|
520
|
+
if (i < 0 || i == null) return null
|
|
521
|
+
const tail = s.slice(i + 4).replaceAll('_', '')
|
|
522
|
+
const payload = (s[i + 3] === ':' && tail !== 'canonical' && tail !== 'arithmetic' ? BigInt(tail) : 0x8000000000000n)
|
|
523
|
+
const h = payload.toString(16).padStart(16, '0')
|
|
524
|
+
const hi = (parseInt(h.slice(0, 8), 16) | 0x7ff00000 | (s[0] === '-' ? 0x80000000 : 0)) >>> 0
|
|
525
|
+
return '0x' + _hex8(hi) + h.slice(8)
|
|
526
|
+
}
|
|
527
|
+
|
|
378
528
|
const _parseNanF64 = (s, i = s?.indexOf?.('nan')) => {
|
|
379
529
|
if (i < 0 || i == null) return null
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
530
|
+
const tail = s.slice(i + 4).replaceAll('_', '')
|
|
531
|
+
const payload = (s[i + 3] === ':' && tail !== 'canonical' && tail !== 'arithmetic' ? BigInt(tail) : 0x8000000000000n)
|
|
532
|
+
// Assemble exponent/sign on the u32 halves — kernel-safe (BigInt <</| are not).
|
|
533
|
+
const h = payload.toString(16).padStart(16, '0')
|
|
534
|
+
_ru32[1] = (parseInt(h.slice(0, 8), 16) | 0x7ff00000 | (s[0] === '-' ? 0x80000000 : 0)) >>> 0
|
|
535
|
+
_ru32[0] = parseInt(h.slice(8), 16)
|
|
383
536
|
return _rf64[0]
|
|
384
537
|
}
|
|
385
538
|
const _parseNanF32 = (s, i = s?.indexOf?.('nan')) => {
|
|
@@ -398,15 +551,25 @@ const _parseNanF32 = (s, i = s?.indexOf?.('nan')) => {
|
|
|
398
551
|
const getConst = (node) => {
|
|
399
552
|
if (!Array.isArray(node) || node.length !== 2) return null
|
|
400
553
|
const [op, val] = node
|
|
401
|
-
if (op === 'i32.const') return { type: 'i32', value: (typeof val === 'string' ?
|
|
402
|
-
if (op === 'i64.const') return { type: 'i64', value:
|
|
554
|
+
if (op === 'i32.const') return { type: 'i32', value: (typeof val === 'string' ? parseInt(val.replaceAll('_', '')) : val) | 0 }
|
|
555
|
+
if (op === 'i64.const') return { type: 'i64', value: _i64Canon(val) }
|
|
403
556
|
if (op === 'f32.const') {
|
|
404
557
|
const n = _parseNanF32(val)
|
|
405
558
|
return { type: 'f32', value: n !== null ? n : Math.fround(Number(val)) }
|
|
406
559
|
}
|
|
407
560
|
if (op === 'f64.const') {
|
|
408
561
|
const n = _parseNanF64(val)
|
|
409
|
-
|
|
562
|
+
const v = n !== null ? n : Number(val)
|
|
563
|
+
// Normalize ANY NaN to the literal NaN — Number.isNaN, NOT `v !== v`:
|
|
564
|
+
// in-kernel `!==` routes through __eq's bit-equality, where a sign-set
|
|
565
|
+
// qNaN (what x64 wasm arithmetic produces) compares EQUAL to itself (the
|
|
566
|
+
// arm that keeps negative i64-carrier BigInts working), so the !== guard
|
|
567
|
+
// misses it. Number.isNaN unboxes to f64 and uses f64.ne — catches every
|
|
568
|
+
// payload. The literal-NaN assignment rewrites the carrier to the
|
|
569
|
+
// canonical atom, so the value can ride kind-erased slots safely (the
|
|
570
|
+
// linux-x64-only selfhost OOB; arm64 arithmetic NaNs are already
|
|
571
|
+
// canonical). Native no-op.
|
|
572
|
+
return { type: 'f64', value: Number.isNaN(v) ? NaN : v }
|
|
410
573
|
}
|
|
411
574
|
return null
|
|
412
575
|
}
|
|
@@ -419,9 +582,14 @@ const getConst = (node) => {
|
|
|
419
582
|
*/
|
|
420
583
|
const makeConst = (type, value) => {
|
|
421
584
|
if (type === 'i32') return ['i32.const', value | 0]
|
|
422
|
-
if (type === 'i64') return ['i64.const', value]
|
|
423
|
-
|
|
424
|
-
|
|
585
|
+
if (type === 'i64') return ['i64.const', typeof value === 'number' ? value : _i64Canon(value)] // canonical hex: kernel-safe print, exact round-trip
|
|
586
|
+
// NaN travels as the `nan` TOKEN, never a raw number: the canonical-NaN bit
|
|
587
|
+
// pattern (0x7FF8…) IS the NaN-box ATOM prefix, so a raw NaN node value
|
|
588
|
+
// inside the self-host kernel reads as a pointer and dereferences OOB
|
|
589
|
+
// (same contract as emitNum — folding Math.sqrt(-1) used to trap the
|
|
590
|
+
// kernel's L2 compile). ±Infinity is outside the box space — safe raw.
|
|
591
|
+
if (type === 'f32') { const v = Math.fround(value); return ['f32.const', Number.isNaN(v) ? 'nan' : v] }
|
|
592
|
+
if (type === 'f64') return ['f64.const', Number.isNaN(value) ? 'nan' : value]
|
|
425
593
|
return null
|
|
426
594
|
}
|
|
427
595
|
|
|
@@ -436,20 +604,47 @@ const fold = (ast) => {
|
|
|
436
604
|
const fn = FOLDABLE[node[0]]
|
|
437
605
|
if (!fn) return
|
|
438
606
|
|
|
607
|
+
// Arity comes from the NODE — every WAT op is fixed-arity, so node.length
|
|
608
|
+
// fully determines unary vs binary. NEVER from Function.length: the
|
|
609
|
+
// self-host kernel's closures don't carry a faithful `.length`, and the
|
|
610
|
+
// old `fn.length === 1/2` checks silently disabled ALL folding in-kernel
|
|
611
|
+
// (the L2 self-host divergence — unfolded consts fed the vectorizer
|
|
612
|
+
// shapes native never produces).
|
|
439
613
|
// Unary
|
|
440
|
-
if (
|
|
614
|
+
if (node.length === 2) {
|
|
615
|
+
// NaN-payload reinterprets fold at the TEXT level — the payload double
|
|
616
|
+
// must never ride as a raw f64 value (see _nanBitsHex). Applies in both
|
|
617
|
+
// directions: nan: literal → i64 bits, and NaN-pattern i64 → nan: literal.
|
|
618
|
+
if (node[0] === 'i64.reinterpret_f64') {
|
|
619
|
+
const inner = node[1]
|
|
620
|
+
if (Array.isArray(inner) && inner.length === 2 && inner[0] === 'f64.const' && typeof inner[1] === 'string') {
|
|
621
|
+
const bits = _nanBitsHex(inner[1])
|
|
622
|
+
if (bits) return ['i64.const', bits]
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (node[0] === 'f64.reinterpret_i64') {
|
|
626
|
+
const c = getConst(node[1])
|
|
627
|
+
if (c && c.type === 'i64') {
|
|
628
|
+
const h = c.value.slice(2)
|
|
629
|
+
const hi = parseInt(h.slice(0, 8), 16) >>> 0
|
|
630
|
+
const lo = parseInt(h.slice(8), 16) >>> 0
|
|
631
|
+
const isNaN64 = (hi & 0x7ff00000) === 0x7ff00000 && ((hi & 0xfffff) !== 0 || lo !== 0)
|
|
632
|
+
if (isNaN64) return ['f64.const',
|
|
633
|
+
((hi & 0x80000000) !== 0 ? '-' : '') + 'nan:0x' + (hi & 0xfffff).toString(16).padStart(5, '0') + h.slice(8)]
|
|
634
|
+
}
|
|
635
|
+
}
|
|
441
636
|
const a = getConst(node[1])
|
|
442
637
|
if (!a) return
|
|
443
638
|
const r = fn(a.value)
|
|
444
|
-
if (r === null) return
|
|
639
|
+
if (r === null || r === undefined) return
|
|
445
640
|
return makeConst(resultType(node[0]), r)
|
|
446
641
|
}
|
|
447
642
|
// Binary
|
|
448
|
-
if (
|
|
643
|
+
if (node.length === 3) {
|
|
449
644
|
const a = getConst(node[1]), b = getConst(node[2])
|
|
450
645
|
if (!a || !b) return
|
|
451
646
|
const r = fn(a.value, b.value)
|
|
452
|
-
if (r === null) return
|
|
647
|
+
if (r === null || r === undefined) return
|
|
453
648
|
return makeConst(resultType(node[0]), r)
|
|
454
649
|
}
|
|
455
650
|
})
|
|
@@ -478,34 +673,34 @@ const rightIdentity = (neutral) => (a, b) => getConst(b)?.value === neutral ? a
|
|
|
478
673
|
const IDENTITIES = {
|
|
479
674
|
// x + 0 → x, 0 + x → x
|
|
480
675
|
'i32.add': commutativeIdentity(0),
|
|
481
|
-
'i64.add': commutativeIdentity(
|
|
676
|
+
'i64.add': commutativeIdentity(ZERO64),
|
|
482
677
|
// x - 0 → x
|
|
483
678
|
'i32.sub': rightIdentity(0),
|
|
484
|
-
'i64.sub': rightIdentity(
|
|
679
|
+
'i64.sub': rightIdentity(ZERO64),
|
|
485
680
|
// x * 1 → x, 1 * x → x
|
|
486
681
|
'i32.mul': commutativeIdentity(1),
|
|
487
|
-
'i64.mul': commutativeIdentity(
|
|
682
|
+
'i64.mul': commutativeIdentity(ONE64),
|
|
488
683
|
// x / 1 → x
|
|
489
684
|
'i32.div_s': rightIdentity(1),
|
|
490
685
|
'i32.div_u': rightIdentity(1),
|
|
491
|
-
'i64.div_s': rightIdentity(
|
|
492
|
-
'i64.div_u': rightIdentity(
|
|
686
|
+
'i64.div_s': rightIdentity(ONE64),
|
|
687
|
+
'i64.div_u': rightIdentity(ONE64),
|
|
493
688
|
// x & -1 → x, -1 & x → x (all bits set)
|
|
494
689
|
'i32.and': commutativeIdentity(-1),
|
|
495
|
-
'i64.and': commutativeIdentity(
|
|
690
|
+
'i64.and': commutativeIdentity(NEG164),
|
|
496
691
|
// x | 0 → x, 0 | x → x
|
|
497
692
|
'i32.or': commutativeIdentity(0),
|
|
498
|
-
'i64.or': commutativeIdentity(
|
|
693
|
+
'i64.or': commutativeIdentity(ZERO64),
|
|
499
694
|
// x ^ 0 → x, 0 ^ x → x
|
|
500
695
|
'i32.xor': commutativeIdentity(0),
|
|
501
|
-
'i64.xor': commutativeIdentity(
|
|
696
|
+
'i64.xor': commutativeIdentity(ZERO64),
|
|
502
697
|
// x << 0 → x, x >> 0 → x
|
|
503
698
|
'i32.shl': rightIdentity(0),
|
|
504
699
|
'i32.shr_s': rightIdentity(0),
|
|
505
700
|
'i32.shr_u': rightIdentity(0),
|
|
506
|
-
'i64.shl': rightIdentity(
|
|
507
|
-
'i64.shr_s': rightIdentity(
|
|
508
|
-
'i64.shr_u': rightIdentity(
|
|
701
|
+
'i64.shl': rightIdentity(ZERO64),
|
|
702
|
+
'i64.shr_s': rightIdentity(ZERO64),
|
|
703
|
+
'i64.shr_u': rightIdentity(ZERO64),
|
|
509
704
|
// f + 0 → x (careful with -0.0, skip for floats)
|
|
510
705
|
// f * 1 → x (careful with NaN, skip for floats)
|
|
511
706
|
}
|
|
@@ -552,16 +747,14 @@ const strength = (ast) => {
|
|
|
552
747
|
}
|
|
553
748
|
}
|
|
554
749
|
if (op === 'i64.mul') {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const ca = getConst(a)
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
return ['i64.shl', b, ['i64.const', shift]]
|
|
564
|
-
}
|
|
750
|
+
// hex value → LOCAL BigInt (in-expression construction is kernel-safe);
|
|
751
|
+
// shift counts emit as plain numbers.
|
|
752
|
+
const cb = getConst(b), vb = cb ? BigInt(cb.value) : null
|
|
753
|
+
if (vb != null && vb > 0n && (vb & (vb - 1n)) === 0n)
|
|
754
|
+
return ['i64.shl', a, ['i64.const', vb.toString(2).length - 1]]
|
|
755
|
+
const ca = getConst(a), va = ca ? BigInt(ca.value) : null
|
|
756
|
+
if (va != null && va > 0n && (va & (va - 1n)) === 0n)
|
|
757
|
+
return ['i64.shl', b, ['i64.const', va.toString(2).length - 1]]
|
|
565
758
|
}
|
|
566
759
|
|
|
567
760
|
// x / 2^n → x >> n (unsigned only, signed division is more complex)
|
|
@@ -573,11 +766,9 @@ const strength = (ast) => {
|
|
|
573
766
|
}
|
|
574
767
|
}
|
|
575
768
|
if (op === 'i64.div_u') {
|
|
576
|
-
const cb = getConst(b)
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
return ['i64.shr_u', a, ['i64.const', shift]]
|
|
580
|
-
}
|
|
769
|
+
const cb = getConst(b), vb = cb ? BigInt(cb.value) : null
|
|
770
|
+
if (vb != null && vb > 0n && (vb & (vb - 1n)) === 0n)
|
|
771
|
+
return ['i64.shr_u', a, ['i64.const', vb.toString(2).length - 1]]
|
|
581
772
|
}
|
|
582
773
|
|
|
583
774
|
// x % 2^n → x & (2^n - 1) (unsigned only)
|
|
@@ -588,10 +779,9 @@ const strength = (ast) => {
|
|
|
588
779
|
}
|
|
589
780
|
}
|
|
590
781
|
if (op === 'i64.rem_u') {
|
|
591
|
-
const cb = getConst(b)
|
|
592
|
-
if (
|
|
593
|
-
return ['i64.and', a, ['i64.const',
|
|
594
|
-
}
|
|
782
|
+
const cb = getConst(b), vb = cb ? BigInt(cb.value) : null
|
|
783
|
+
if (vb != null && vb > 0n && (vb & (vb - 1n)) === 0n)
|
|
784
|
+
return ['i64.and', a, ['i64.const', '0x' + _i64Hex16(vb - 1n)]]
|
|
595
785
|
}
|
|
596
786
|
})
|
|
597
787
|
}
|
|
@@ -611,12 +801,18 @@ const branch = (ast) => {
|
|
|
611
801
|
// (if (i32.const 0) then else) → else
|
|
612
802
|
// (if (i32.const N) then else) → then (N != 0)
|
|
613
803
|
if (op === 'if') {
|
|
614
|
-
const { cond, thenBranch, elseBranch } = parseIf(node)
|
|
804
|
+
const { condIdx, cond, thenBranch, elseBranch } = parseIf(node)
|
|
615
805
|
const c = getConst(cond)
|
|
616
806
|
if (!c) return
|
|
617
|
-
const taken = c.value !== 0 && c.value !==
|
|
807
|
+
const taken = c.value !== 0 && c.value !== ZERO64 ? thenBranch : elseBranch
|
|
618
808
|
if (taken && taken.length > 1) {
|
|
619
809
|
const contents = taken.slice(1)
|
|
810
|
+
// Preserve the if's block type (result/param). A typed `if` leaves a value
|
|
811
|
+
// on the stack; collapsing it to the taken branch must keep that branch's
|
|
812
|
+
// value in a same-typed block, else the contents land in a void context and
|
|
813
|
+
// the value is left dangling → "expected 0 elements on the stack for fallthru".
|
|
814
|
+
const blockType = node.slice(1, condIdx).filter(p => Array.isArray(p) && (p[0] === 'result' || p[0] === 'param'))
|
|
815
|
+
if (blockType.length) return ['block', ...blockType, ...contents]
|
|
620
816
|
return contents.length === 1 ? contents[0] : ['block', ...contents]
|
|
621
817
|
}
|
|
622
818
|
return ['nop']
|
|
@@ -628,7 +824,7 @@ const branch = (ast) => {
|
|
|
628
824
|
const cond = node[node.length - 1]
|
|
629
825
|
const c = getConst(cond)
|
|
630
826
|
if (!c) return
|
|
631
|
-
if (c.value === 0 || c.value ===
|
|
827
|
+
if (c.value === 0 || c.value === ZERO64) return ['nop']
|
|
632
828
|
return ['br', node[1]]
|
|
633
829
|
}
|
|
634
830
|
|
|
@@ -638,12 +834,209 @@ const branch = (ast) => {
|
|
|
638
834
|
const cond = node[node.length - 1]
|
|
639
835
|
const c = getConst(cond)
|
|
640
836
|
if (!c) return
|
|
641
|
-
if (c.value === 0 || c.value ===
|
|
837
|
+
if (c.value === 0 || c.value === ZERO64) return node[2] // b
|
|
642
838
|
return node[1] // a
|
|
643
839
|
}
|
|
644
840
|
})
|
|
645
841
|
}
|
|
646
842
|
|
|
843
|
+
// ==================== GUARD-AWARE TAG REFINEMENT ====================
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Fold NaN-box tag reads under dominating tag guards (jz-domain knowledge).
|
|
847
|
+
*
|
|
848
|
+
* jz reads a value's 4-bit NaN-box tag in three equivalent forms:
|
|
849
|
+
* A. (i32.and (i32.wrap_i64 (i64.shr_u PTR (i64.const 47))) (i32.const 15))
|
|
850
|
+
* B. (i32.wrap_i64 (i64.and (i64.shr_u PTR (i64.const 47)) (i64.const 15)))
|
|
851
|
+
* C. (call $__ptr_type PTR)
|
|
852
|
+
* where PTR is (i64.reinterpret_f64 (local.get $X)) or an i64 local copy of it.
|
|
853
|
+
*
|
|
854
|
+
* After `inlineOnce` splices a generic helper (e.g. $__len's 5-way tag
|
|
855
|
+
* dispatch) into an arm already guarded by `tag(X) == K`, the recomputed tag
|
|
856
|
+
* is a known constant — but no structural pass can see it: forms A and B
|
|
857
|
+
* differ shape-wise, and the value flows through reinterpret/copy locals.
|
|
858
|
+
* This pass tracks tag-of-X facts through if-arms and folds tag reads to
|
|
859
|
+
* constants; the regular fold/branch/vacuum passes then delete the dead
|
|
860
|
+
* dispatch arms. This is the single biggest source of wasm-opt's remaining
|
|
861
|
+
* slack on jz output (~10% on typed-array modules).
|
|
862
|
+
*
|
|
863
|
+
* Soundness model — facts and aliases are keyed by the f64 SOURCE local $X
|
|
864
|
+
* (tags live in the value's bits, so only local writes can invalidate, never
|
|
865
|
+
* calls/stores):
|
|
866
|
+
* - any local.set/tee of $X kills its fact and every alias derived from it
|
|
867
|
+
* - leaving a block kills facts/aliases for locals written inside it
|
|
868
|
+
* (a br may have skipped the write)
|
|
869
|
+
* - entering a loop kills facts for locals written anywhere in it
|
|
870
|
+
* (the back edge re-enters after the write)
|
|
871
|
+
* - then/else facts are layered over a snapshot and restored on exit;
|
|
872
|
+
* writes inside either arm kill outer facts afterward
|
|
873
|
+
* - within straight-line code, sequential registration is exact: wasm has
|
|
874
|
+
* no goto, so execution between branch points is linear
|
|
875
|
+
*/
|
|
876
|
+
const guardRefine = (ast) => {
|
|
877
|
+
if (Array.isArray(ast)) for (const node of ast) if (Array.isArray(node) && node[0] === 'func') refineGuards(node)
|
|
878
|
+
return ast
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const EMPTY_SET = new Set()
|
|
882
|
+
|
|
883
|
+
const refineGuards = (fn) => {
|
|
884
|
+
const ptrAlias = new Map() // i64 local → f64 source local (reinterpret copy)
|
|
885
|
+
const tagAlias = new Map() // i32 local → f64 source local (holds tag(X))
|
|
886
|
+
const eqFact = new Map() // f64 local → known tag K
|
|
887
|
+
const neFact = new Map() // f64 local → Set of excluded tags
|
|
888
|
+
|
|
889
|
+
const intVal = (n) => {
|
|
890
|
+
if (!Array.isArray(n) || n.length !== 2 || (n[0] !== 'i32.const' && n[0] !== 'i64.const')) return null
|
|
891
|
+
const v = typeof n[1] === 'string' ? Number(n[1].replaceAll('_', '')) : Number(n[1])
|
|
892
|
+
return Number.isFinite(v) ? v : null
|
|
893
|
+
}
|
|
894
|
+
const i32Val = (n) => Array.isArray(n) && n[0] === 'i32.const' ? intVal(n) : null
|
|
895
|
+
|
|
896
|
+
// PTR node → f64 source local, or null.
|
|
897
|
+
const ptrSrc = (n) => {
|
|
898
|
+
if (!Array.isArray(n)) return null
|
|
899
|
+
if (n[0] === 'i64.reinterpret_f64' && Array.isArray(n[1]) && n[1][0] === 'local.get' && typeof n[1][1] === 'string') return n[1][1]
|
|
900
|
+
if (n[0] === 'local.get' && typeof n[1] === 'string') return ptrAlias.get(n[1]) ?? null
|
|
901
|
+
return null
|
|
902
|
+
}
|
|
903
|
+
// tag-of-X node (forms A/B/C or a tag-alias local read) → X, or null.
|
|
904
|
+
const tagSrc = (n) => {
|
|
905
|
+
if (!Array.isArray(n)) return null
|
|
906
|
+
const op = n[0]
|
|
907
|
+
if (op === 'local.get' && typeof n[1] === 'string') return tagAlias.get(n[1]) ?? null
|
|
908
|
+
if (op === 'call' && n[1] === '$__ptr_type' && n.length === 3) return ptrSrc(n[2])
|
|
909
|
+
const shifted = (m) => Array.isArray(m) && m[0] === 'i64.shr_u' && intVal(m[2]) === 47 ? ptrSrc(m[1]) : null
|
|
910
|
+
if (op === 'i32.and' && n.length === 3) { // form A (mask either side)
|
|
911
|
+
const [a, b] = i32Val(n[2]) === 15 ? [n[1], null] : i32Val(n[1]) === 15 ? [n[2], null] : [null, null]
|
|
912
|
+
if (a && Array.isArray(a) && a[0] === 'i32.wrap_i64') return shifted(a[1])
|
|
913
|
+
}
|
|
914
|
+
if (op === 'i32.wrap_i64' && Array.isArray(n[1]) && n[1][0] === 'i64.and') { // form B
|
|
915
|
+
const m = n[1]
|
|
916
|
+
if (intVal(m[2]) === 15) return shifted(m[1])
|
|
917
|
+
if (intVal(m[1]) === 15) return shifted(m[2])
|
|
918
|
+
}
|
|
919
|
+
return null
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const killLocal = (name) => {
|
|
923
|
+
ptrAlias.delete(name); tagAlias.delete(name); eqFact.delete(name); neFact.delete(name)
|
|
924
|
+
for (const [p, x] of ptrAlias) if (x === name) ptrAlias.delete(p)
|
|
925
|
+
for (const [t, x] of tagAlias) if (x === name) tagAlias.delete(t)
|
|
926
|
+
}
|
|
927
|
+
// Write-sets are queried per if/loop/block; memoize bottom-up so each node is
|
|
928
|
+
// visited once per function, not once per enclosing construct. Keyed by node
|
|
929
|
+
// identity — sound because walkSeq only ever *replaces* whole subtrees
|
|
930
|
+
// (parent[idx] = const), never adds writes to an existing one.
|
|
931
|
+
const writesMemo = new Map()
|
|
932
|
+
const writesOf = (n) => {
|
|
933
|
+
if (!Array.isArray(n)) return EMPTY_SET
|
|
934
|
+
let s = writesMemo.get(n)
|
|
935
|
+
if (s) return s
|
|
936
|
+
s = new Set()
|
|
937
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string') s.add(n[1])
|
|
938
|
+
for (let i = 1; i < n.length; i++) for (const w of writesOf(n[i])) s.add(w)
|
|
939
|
+
writesMemo.set(n, s)
|
|
940
|
+
return s
|
|
941
|
+
}
|
|
942
|
+
const snap = () => [new Map(eqFact), new Map([...neFact].map(([k, s]) => [k, new Set(s)])), new Map(ptrAlias), new Map(tagAlias)]
|
|
943
|
+
const reset = (m, src) => { m.clear(); for (const [k, v] of src) m.set(k, v) }
|
|
944
|
+
const restore = ([e, n, p, t]) => { reset(eqFact, e); reset(neFact, n); reset(ptrAlias, p); reset(tagAlias, t) }
|
|
945
|
+
|
|
946
|
+
// Facts implied by `cond` being truthy (sense=true) / falsy (sense=false).
|
|
947
|
+
const condFacts = (cond, sense, out) => {
|
|
948
|
+
if (!Array.isArray(cond)) return out
|
|
949
|
+
const op = cond[0]
|
|
950
|
+
if (op === 'i32.eqz') return condFacts(cond[1], !sense, out)
|
|
951
|
+
if (op === 'i32.and' && sense && cond.length === 3) { condFacts(cond[1], true, out); condFacts(cond[2], true, out); return out }
|
|
952
|
+
if (op === 'i32.or' && !sense && cond.length === 3) { condFacts(cond[1], false, out); condFacts(cond[2], false, out); return out }
|
|
953
|
+
if ((op === 'i32.eq' || op === 'i32.ne') && cond.length === 3) {
|
|
954
|
+
let x = tagSrc(cond[1]), k = i32Val(cond[2])
|
|
955
|
+
if (x == null || k == null) { x = tagSrc(cond[2]); k = i32Val(cond[1]) }
|
|
956
|
+
if (x != null && k != null) out.push({ x, k, eq: (op === 'i32.eq') === sense })
|
|
957
|
+
return out
|
|
958
|
+
}
|
|
959
|
+
const x = tagSrc(cond) // bare tag as condition: truthy ⇒ tag≠0, falsy ⇒ tag==0
|
|
960
|
+
if (x != null) out.push({ x, k: 0, eq: !sense })
|
|
961
|
+
return out
|
|
962
|
+
}
|
|
963
|
+
const addFacts = (fs) => {
|
|
964
|
+
for (const { x, k, eq } of fs) {
|
|
965
|
+
if (eq) eqFact.set(x, k)
|
|
966
|
+
else { let s = neFact.get(x); if (!s) neFact.set(x, s = new Set()); s.add(k) }
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const walkSeq = (node, parent, idx) => {
|
|
971
|
+
if (!Array.isArray(node)) return
|
|
972
|
+
const op = node[0]
|
|
973
|
+
|
|
974
|
+
if (op === 'local.set' || op === 'local.tee') {
|
|
975
|
+
if (Array.isArray(node[2])) walkSeq(node[2], node, 2)
|
|
976
|
+
const name = node[1]
|
|
977
|
+
if (typeof name !== 'string') return
|
|
978
|
+
killLocal(name)
|
|
979
|
+
const v = node[2]
|
|
980
|
+
if (Array.isArray(v)) {
|
|
981
|
+
if (v[0] === 'i64.reinterpret_f64' && Array.isArray(v[1]) && v[1][0] === 'local.get' && typeof v[1][1] === 'string') ptrAlias.set(name, v[1][1])
|
|
982
|
+
else if (v[0] === 'local.get' && typeof v[1] === 'string' && ptrAlias.has(v[1])) ptrAlias.set(name, ptrAlias.get(v[1]))
|
|
983
|
+
else { const tx = tagSrc(v); if (tx != null) tagAlias.set(name, tx) }
|
|
984
|
+
}
|
|
985
|
+
return
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (op === 'if') {
|
|
989
|
+
const { condIdx } = parseIf(node)
|
|
990
|
+
if (Array.isArray(node[condIdx])) walkSeq(node[condIdx], node, condIdx)
|
|
991
|
+
const cond = node[condIdx] // re-read: the walk may have folded it
|
|
992
|
+
const { thenBranch, elseBranch } = parseIf(node)
|
|
993
|
+
const writes = writesOf(node)
|
|
994
|
+
const pre = snap()
|
|
995
|
+
addFacts(condFacts(cond, true, []))
|
|
996
|
+
if (thenBranch) for (let i = 1; i < thenBranch.length; i++) walkSeq(thenBranch[i], thenBranch, i)
|
|
997
|
+
restore(pre)
|
|
998
|
+
addFacts(condFacts(cond, false, []))
|
|
999
|
+
if (elseBranch) for (let i = 1; i < elseBranch.length; i++) walkSeq(elseBranch[i], elseBranch, i)
|
|
1000
|
+
restore(pre)
|
|
1001
|
+
for (const w of writes) killLocal(w)
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (op === 'loop') {
|
|
1006
|
+
const writes = writesOf(node)
|
|
1007
|
+
for (const w of writes) killLocal(w)
|
|
1008
|
+
for (let i = 1; i < node.length; i++) walkSeq(node[i], node, i)
|
|
1009
|
+
for (const w of writes) killLocal(w)
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (op === 'block') {
|
|
1014
|
+
for (let i = 1; i < node.length; i++) walkSeq(node[i], node, i)
|
|
1015
|
+
for (const w of writesOf(node)) killLocal(w)
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Whole-node tag read under an equality fact → constant.
|
|
1020
|
+
const tx = tagSrc(node)
|
|
1021
|
+
if (tx != null && eqFact.has(tx) && parent) { parent[idx] = ['i32.const', eqFact.get(tx)]; return }
|
|
1022
|
+
|
|
1023
|
+
// eq/ne against a constant under a ne-fact (eq-facts are covered by the
|
|
1024
|
+
// tag-read fold above plus the regular `fold` pass).
|
|
1025
|
+
if ((op === 'i32.eq' || op === 'i32.ne') && node.length === 3) {
|
|
1026
|
+
let x = tagSrc(node[1]), k = i32Val(node[2])
|
|
1027
|
+
if (x == null || k == null) { x = tagSrc(node[2]); k = i32Val(node[1]) }
|
|
1028
|
+
if (x != null && k != null && neFact.get(x)?.has(k) && parent) {
|
|
1029
|
+
parent[idx] = ['i32.const', op === 'i32.eq' ? 0 : 1]
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
for (let i = 1; i < node.length; i++) walkSeq(node[i], node, i)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
for (let i = 1; i < fn.length; i++) walkSeq(fn[i], fn, i)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
647
1040
|
// ==================== DEAD CODE ELIMINATION ====================
|
|
648
1041
|
|
|
649
1042
|
/** Control flow terminators */
|
|
@@ -814,6 +1207,38 @@ const isPure = (node) => {
|
|
|
814
1207
|
return true
|
|
815
1208
|
}
|
|
816
1209
|
|
|
1210
|
+
// Structured / control-flow forms: they do NOT evaluate all children eagerly
|
|
1211
|
+
// (an `if` runs one arm; a `block`/`loop` scopes branches), so their side effects
|
|
1212
|
+
// can't be flattened to the children's — they stay whole under a drop. (`br*`,
|
|
1213
|
+
// `try_table` are already in IMPURE_OPS.)
|
|
1214
|
+
const STRUCTURED_OPS = new Set(['if', 'then', 'else', 'block', 'loop', 'try'])
|
|
1215
|
+
|
|
1216
|
+
// `op` is an EAGER value operation: it evaluates every operand unconditionally
|
|
1217
|
+
// and only computes a result (arithmetic, compare, convert, select, load) — so
|
|
1218
|
+
// discarding its value leaves just the operands' side effects. Excludes impure
|
|
1219
|
+
// ops and the structured forms above.
|
|
1220
|
+
const isEagerValueOp = (op) => typeof op === 'string' && !IMPURE_OPS.has(op) &&
|
|
1221
|
+
!STRUCTURED_OPS.has(op) && !IMPURE_SUBSTRINGS.some(s => op.includes(s))
|
|
1222
|
+
|
|
1223
|
+
// Statements that preserve `node`'s side effects when its VALUE is discarded.
|
|
1224
|
+
// A fully-pure value contributes nothing; an eager value op contributes only its
|
|
1225
|
+
// operands' effects (the op result is dead); a `local.tee` keeps the store as a
|
|
1226
|
+
// `local.set`; anything else (call, store-expr, structured form) stays under a drop.
|
|
1227
|
+
// This turns `drop(i32.sub(tee X V, 1))` — the post-increment's dropped old value
|
|
1228
|
+
// — into the bare `local.set X V`, eliminating dead arithmetic the plain
|
|
1229
|
+
// `drop(PURE)→nop` rule can't (the tee makes the whole subtree impure).
|
|
1230
|
+
const dropEffects = (node) => {
|
|
1231
|
+
if (!Array.isArray(node) || isPure(node)) return []
|
|
1232
|
+
const op = node[0]
|
|
1233
|
+
if (op === 'local.tee' && node.length === 3) return [['local.set', node[1], node[2]]]
|
|
1234
|
+
if (isEagerValueOp(op)) {
|
|
1235
|
+
const eff = []
|
|
1236
|
+
for (let i = 1; i < node.length; i++) eff.push(...dropEffects(node[i]))
|
|
1237
|
+
return eff
|
|
1238
|
+
}
|
|
1239
|
+
return [['drop', node]]
|
|
1240
|
+
}
|
|
1241
|
+
|
|
817
1242
|
/** Count all local.get/set/tee occurrences in one walk */
|
|
818
1243
|
const countLocalUses = (node) => {
|
|
819
1244
|
const counts = new Map()
|
|
@@ -835,16 +1260,30 @@ const isTinyConst = (node) => {
|
|
|
835
1260
|
const c = getConst(node)
|
|
836
1261
|
if (!c) return false
|
|
837
1262
|
if (c.type === 'i32') { const v = c.value | 0; return v >= -64 && v <= 63 }
|
|
838
|
-
if (c.type === 'i64') { const v =
|
|
1263
|
+
if (c.type === 'i64') { const v = BigInt(c.value); return v <= 63n || v >= 0xffffffffffffffc0n } // unsigned bits: [0,63] or two's-comp [−64,−1]
|
|
839
1264
|
return false
|
|
840
1265
|
}
|
|
841
1266
|
|
|
1267
|
+
/** A pure local→local copy value `(local.get $src)`, with $src ≠ the local being set.
|
|
1268
|
+
* Substituting it for a `(local.get $dst)` is byte-neutral (local.get for local.get),
|
|
1269
|
+
* so — unlike a reused wide constant — it can never grow an instruction, and it turns
|
|
1270
|
+
* the copy `$dst = $src` into a dead store the next pass drops. Self-copies are
|
|
1271
|
+
* excluded: they're no-ops that would re-trigger `changed` every round. (Propagating a
|
|
1272
|
+
* copy lengthens $src's live range, which can rarely cost coalesceLocals a slot — a
|
|
1273
|
+
* few bytes — but net-shrinks across the corpus, e.g. −1.7 KB on the watr self-host.) */
|
|
1274
|
+
const isLocalCopy = (val, dest) =>
|
|
1275
|
+
Array.isArray(val) && val[0] === 'local.get' && val.length === 2 &&
|
|
1276
|
+
typeof val[1] === 'string' && val[1] !== dest
|
|
1277
|
+
|
|
842
1278
|
/** Can this tracked value be substituted for a local.get?
|
|
843
1279
|
* - single use of a pure value: always shrinks (drops the set, the lone get, the decl);
|
|
844
|
-
* - any use of a tiny constant: byte-neutral at worst, still drops the set + decl
|
|
1280
|
+
* - any use of a tiny constant: byte-neutral at worst, still drops the set + decl;
|
|
1281
|
+
* - any use of a pure local copy: byte-neutral, frees the copy as a dead store.
|
|
845
1282
|
* Anything else (a wide constant reused many times, an impure expr) could inflate
|
|
846
|
-
* or reorder side effects, so it's left alone.
|
|
847
|
-
|
|
1283
|
+
* or reorder side effects, so it's left alone. Copy validity (the source not being
|
|
1284
|
+
* reassigned between copy and use) is enforced by the same purgeRefs/branch-clear
|
|
1285
|
+
* machinery that guards every tracked value. */
|
|
1286
|
+
const canSubst = (k) => (k.pure && k.singleUse) || isTinyConst(k.val) || k.copy
|
|
848
1287
|
|
|
849
1288
|
/** Drop tracked values that read `$name`: rewriting `$name` makes them stale. */
|
|
850
1289
|
const purgeRefs = (known, name) => {
|
|
@@ -855,6 +1294,19 @@ const purgeRefs = (known, name) => {
|
|
|
855
1294
|
}
|
|
856
1295
|
}
|
|
857
1296
|
|
|
1297
|
+
/** Drop tracked values that read global `$name`: a `global.set $name` makes them stale.
|
|
1298
|
+
* The local-only {@link purgeRefs} misses this — so a value captured from a global
|
|
1299
|
+
* (`let s = f`, where `f` is a reassignable module-level binding) would survive an
|
|
1300
|
+
* intervening `f = …` and substitute the NEW global. That silently breaks the canonical
|
|
1301
|
+
* pointer swap `let s = f; f = g; g = s` (g would read post-swap f, i.e. itself). */
|
|
1302
|
+
const purgeGlobalRefs = (known, name) => {
|
|
1303
|
+
for (const [key, tracked] of known) {
|
|
1304
|
+
let refs = false
|
|
1305
|
+
walk(tracked.val, n => { if (Array.isArray(n) && n[0] === 'global.get' && n[1] === name) refs = true })
|
|
1306
|
+
if (refs) known.delete(key)
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
858
1310
|
/** True if `node` recursively contains an op that may read linear memory.
|
|
859
1311
|
* Tracked values whose RHS reads memory go stale after any intervening
|
|
860
1312
|
* memory-mutating op (`*.store`, `memory.copy/fill/init`, atomic stores/rmw). */
|
|
@@ -915,7 +1367,7 @@ const substGets = (node, known) => {
|
|
|
915
1367
|
return node
|
|
916
1368
|
}
|
|
917
1369
|
let inner = known
|
|
918
|
-
if (op
|
|
1370
|
+
if (isBranchScope(op)) {
|
|
919
1371
|
let cloned = null
|
|
920
1372
|
walk(node, n => {
|
|
921
1373
|
if (!Array.isArray(n)) return
|
|
@@ -942,6 +1394,9 @@ const substGets = (node, known) => {
|
|
|
942
1394
|
if (inner === known) inner = new Map(known)
|
|
943
1395
|
inner.delete(n[1])
|
|
944
1396
|
purgeRefs(inner, n[1])
|
|
1397
|
+
} else if (n[0] === 'global.set' && typeof n[1] === 'string') { // same staleness as a local write — a sibling operand's
|
|
1398
|
+
if (inner === known) inner = new Map(known) // global write invalidates a later operand's global-sourced copy
|
|
1399
|
+
purgeGlobalRefs(inner, n[1])
|
|
945
1400
|
}
|
|
946
1401
|
})
|
|
947
1402
|
}
|
|
@@ -984,6 +1439,7 @@ const forwardPropagate = (funcNode, params, useCounts) => {
|
|
|
984
1439
|
if (!Array.isArray(n)) return
|
|
985
1440
|
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
|
|
986
1441
|
{ known.delete(n[1]); purgeRefs(known, n[1]) }
|
|
1442
|
+
else if (n[0] === 'global.set' && typeof n[1] === 'string') purgeGlobalRefs(known, n[1])
|
|
987
1443
|
})
|
|
988
1444
|
const uses = getUseCount(instr[1])
|
|
989
1445
|
purgeRefs(known, instr[1]) // entries that read this local just went stale
|
|
@@ -996,13 +1452,14 @@ const forwardPropagate = (funcNode, params, useCounts) => {
|
|
|
996
1452
|
known.set(instr[1], {
|
|
997
1453
|
val: instr[2], pure: isPure(instr[2]),
|
|
998
1454
|
readsMem: readsMemory(instr[2]),
|
|
999
|
-
singleUse: uses.gets <= 1 && uses.sets <= 1 && uses.tees === 0
|
|
1455
|
+
singleUse: uses.gets <= 1 && uses.sets <= 1 && uses.tees === 0,
|
|
1456
|
+
copy: isLocalCopy(instr[2], instr[1])
|
|
1000
1457
|
})
|
|
1001
1458
|
continue
|
|
1002
1459
|
}
|
|
1003
1460
|
|
|
1004
1461
|
// Invalidate at control-flow boundaries
|
|
1005
|
-
if (op
|
|
1462
|
+
if (isBranchScope(op)) known.clear()
|
|
1006
1463
|
// Calls invalidate tracked values that read state a callee can mutate
|
|
1007
1464
|
// (memory, globals, tables, nested calls). Pure expressions over locals
|
|
1008
1465
|
// and constants survive — callees can't reach caller locals.
|
|
@@ -1029,8 +1486,10 @@ const forwardPropagate = (funcNode, params, useCounts) => {
|
|
|
1029
1486
|
// pre-write tracked value (correct), but later reads must see the new
|
|
1030
1487
|
// (untracked) value, not the stale constant.
|
|
1031
1488
|
walk(instr, n => {
|
|
1032
|
-
if (Array.isArray(n)
|
|
1489
|
+
if (!Array.isArray(n)) return
|
|
1490
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
|
|
1033
1491
|
{ known.delete(n[1]); purgeRefs(known, n[1]) }
|
|
1492
|
+
else if (n[0] === 'global.set' && typeof n[1] === 'string') purgeGlobalRefs(known, n[1])
|
|
1034
1493
|
})
|
|
1035
1494
|
// Memory write in this statement (any nested store / memory.copy / etc.)
|
|
1036
1495
|
// invalidates every tracked value whose RHS reads memory: inlining one
|
|
@@ -1145,6 +1604,35 @@ const eliminateDeadStores = (funcNode, params, useCounts) => {
|
|
|
1145
1604
|
return changed
|
|
1146
1605
|
}
|
|
1147
1606
|
|
|
1607
|
+
/**
|
|
1608
|
+
* Drop `(local.set $x A)` when the very next statement re-sets $x without reading it
|
|
1609
|
+
* first (A pure). The two writes are adjacent, so A's value is overwritten before any
|
|
1610
|
+
* observation — it's dead. The whole-function {@link eliminateDeadStores} misses this:
|
|
1611
|
+
* it only fires when $x is read NOWHERE, whereas here $x is live later, just not
|
|
1612
|
+
* between these two writes. Pairs with copy-propagation, which rewrites
|
|
1613
|
+
* `$x=$y; $x=f($x)` to `$x=$y; $x=f($y)` — an adjacent dead store this removes,
|
|
1614
|
+
* collapsing the round-trip jz's value-model lowering leaves behind.
|
|
1615
|
+
* @param {Array} funcNode a straight-line scope (body / block / loop / then / else)
|
|
1616
|
+
* @param {Set<string>} params
|
|
1617
|
+
*/
|
|
1618
|
+
const eliminateAdjacentDeadStores = (funcNode, params) => {
|
|
1619
|
+
let changed = false
|
|
1620
|
+
for (let i = 1; i < funcNode.length - 1; i++) {
|
|
1621
|
+
const a = funcNode[i], b = funcNode[i + 1]
|
|
1622
|
+
// `a` must be a plain set (a tee leaves its value on the stack — not removable);
|
|
1623
|
+
// `b` may be a set OR a tee (both overwrite the local before `a`'s value is read).
|
|
1624
|
+
if (!Array.isArray(a) || a[0] !== 'local.set' || a.length !== 3) continue
|
|
1625
|
+
if (!Array.isArray(b) || (b[0] !== 'local.set' && b[0] !== 'local.tee') || b.length !== 3 || b[1] !== a[1]) continue
|
|
1626
|
+
if (params.has(a[1]) || !isPure(a[2])) continue
|
|
1627
|
+
// Dead only if b's value doesn't read $x before overwriting it.
|
|
1628
|
+
let reads = false
|
|
1629
|
+
walk(b[2], n => { if (Array.isArray(n) && (n[0] === 'local.get' || n[0] === 'local.tee') && n[1] === a[1]) reads = true })
|
|
1630
|
+
if (reads) continue
|
|
1631
|
+
funcNode.splice(i, 1); changed = true; i--
|
|
1632
|
+
}
|
|
1633
|
+
return changed
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1148
1636
|
/**
|
|
1149
1637
|
* Propagate values through locals and eliminate single-use/dead locals.
|
|
1150
1638
|
* Constants propagate to all uses; pure single-use exprs inline into get site.
|
|
@@ -1154,6 +1642,9 @@ const eliminateDeadStores = (funcNode, params, useCounts) => {
|
|
|
1154
1642
|
const isScopeNode = (n) => Array.isArray(n) &&
|
|
1155
1643
|
(n[0] === 'func' || n[0] === 'block' || n[0] === 'loop' || n[0] === 'then' || n[0] === 'else')
|
|
1156
1644
|
|
|
1645
|
+
/** Branch-target scopes: ops that carry an optional label/result header and can be jumped to via br/br_if. */
|
|
1646
|
+
const isBranchScope = (op) => op === 'block' || op === 'loop' || op === 'if'
|
|
1647
|
+
|
|
1157
1648
|
const propagate = (ast) => {
|
|
1158
1649
|
walk(ast, (funcNode) => {
|
|
1159
1650
|
if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
|
|
@@ -1176,7 +1667,7 @@ const propagate = (ast) => {
|
|
|
1176
1667
|
// (skip a not-yet-provably-dead store, decline a not-yet-provably-single use) —
|
|
1177
1668
|
// never wrongly. The next round re-counts and mops up. (Recounting per sub-pass
|
|
1178
1669
|
// per scope is O(scopes·funcSize) and crippling on big modules.)
|
|
1179
|
-
for (let round = 0; round <
|
|
1670
|
+
for (let round = 0; round < MAX_PROP_ROUNDS; round++) {
|
|
1180
1671
|
const useCounts = countLocalUses(funcNode)
|
|
1181
1672
|
let progressed = false
|
|
1182
1673
|
for (const scope of scopes) {
|
|
@@ -1184,6 +1675,7 @@ const propagate = (ast) => {
|
|
|
1184
1675
|
if (eliminateSetGetPairs(scope, params, useCounts)) progressed = true
|
|
1185
1676
|
if (createLocalTees(scope, params, useCounts)) progressed = true
|
|
1186
1677
|
if (eliminateDeadStores(scope, params, useCounts)) progressed = true
|
|
1678
|
+
if (eliminateAdjacentDeadStores(scope, params)) progressed = true
|
|
1187
1679
|
}
|
|
1188
1680
|
if (!progressed) break
|
|
1189
1681
|
}
|
|
@@ -1194,113 +1686,545 @@ const propagate = (ast) => {
|
|
|
1194
1686
|
|
|
1195
1687
|
// ==================== FUNCTION INLINING ====================
|
|
1196
1688
|
|
|
1689
|
+
// Shared inliner primitives, used by BOTH passes below: `inline` (duplicates tiny
|
|
1690
|
+
// multi-caller bodies — size-for-speed) and `inlineOnce` (splices single-caller
|
|
1691
|
+
// bodies, never duplicates). The lift technique is identical — rename the callee's
|
|
1692
|
+
// params/locals/labels to fresh `$__inlN_*` names, evaluate args once into the
|
|
1693
|
+
// renamed param locals, turn `return X` into `br $__inlN X`, wrap the body in a
|
|
1694
|
+
// `(block $__inlN (result T)? …)`. Only the SELECTION policy differs (one caller vs
|
|
1695
|
+
// every caller of a small body), so the lift lives here once.
|
|
1696
|
+
|
|
1697
|
+
let inlineUid = 0
|
|
1698
|
+
const INL_HEAD = new Set(['export', 'type', 'param', 'result', 'local'])
|
|
1699
|
+
const inlBodyStart = (fn) => {
|
|
1700
|
+
let i = 2
|
|
1701
|
+
while (i < fn.length && (typeof fn[i] === 'string' || (Array.isArray(fn[i]) && INL_HEAD.has(fn[i][0])))) i++
|
|
1702
|
+
return i
|
|
1703
|
+
}
|
|
1704
|
+
const inlIsBranch = op => op === 'br' || op === 'br_if' || op === 'br_table'
|
|
1705
|
+
// A subtree we can't lift into a (block …): depth-relative branch labels (which would
|
|
1706
|
+
// shift under the added nesting) or tail calls (which would escape the wrapping block).
|
|
1707
|
+
const inlUnsafe = (n) => {
|
|
1708
|
+
if (!Array.isArray(n)) return false
|
|
1709
|
+
const op = n[0]
|
|
1710
|
+
if (op === 'return_call' || op === 'return_call_indirect' || op === 'return_call_ref') return true
|
|
1711
|
+
if (op === 'try' || op === 'try_table' || op === 'delegate' || op === 'rethrow') return true // exception labels — not handled by the relabeler below
|
|
1712
|
+
if (inlIsBranch(op)) for (let i = 1; i < n.length; i++) if (typeof n[i] === 'number' || (typeof n[i] === 'string' && /^\d+$/.test(n[i]))) return true
|
|
1713
|
+
for (let i = 1; i < n.length; i++) if (inlUnsafe(n[i])) return true
|
|
1714
|
+
return false
|
|
1715
|
+
}
|
|
1716
|
+
const inlCallsSelf = (n, name) => {
|
|
1717
|
+
if (!Array.isArray(n)) return false
|
|
1718
|
+
if ((n[0] === 'call' || n[0] === 'return_call') && n[1] === name) return true
|
|
1719
|
+
for (let i = 1; i < n.length; i++) if (inlCallsSelf(n[i], name)) return true
|
|
1720
|
+
return false
|
|
1721
|
+
}
|
|
1722
|
+
// Per-call zero-init constant for a callee local re-entered from a caller loop.
|
|
1723
|
+
// null ⇒ a type we can't safely zero here (skip inlining such a callee).
|
|
1724
|
+
const inlZeroFor = (t) => {
|
|
1725
|
+
if (t === 'i32') return ['i32.const', 0]
|
|
1726
|
+
if (t === 'i64') return ['i64.const', 0]
|
|
1727
|
+
if (t === 'f32') return ['f32.const', 0]
|
|
1728
|
+
if (t === 'f64') return ['f64.const', 0]
|
|
1729
|
+
if (t === 'v128') return ['v128.const', 'i64x2', '0', '0'] // STRING lanes — watr's v128 encoder calls .replaceAll
|
|
1730
|
+
return null
|
|
1731
|
+
}
|
|
1732
|
+
// A callee local needs a per-entry reset only if some path reads it before any
|
|
1733
|
+
// unconditional write (so it relied on the callee's fresh zero-init). Mirrors
|
|
1734
|
+
// coalesceLocals' readsZero heuristic; unconditionally-written scratch needs none.
|
|
1735
|
+
const inlNeedsReset = (body, name) => {
|
|
1736
|
+
let seen = false, conditional = false, depth = 0
|
|
1737
|
+
const visit = (n) => {
|
|
1738
|
+
if (seen || !Array.isArray(n)) return
|
|
1739
|
+
const op = n[0]
|
|
1740
|
+
const isSet = op === 'local.set' || op === 'local.tee'
|
|
1741
|
+
if ((isSet || op === 'local.get') && n[1] === name) {
|
|
1742
|
+
if (isSet) for (let i = 2; i < n.length && !seen; i++) visit(n[i])
|
|
1743
|
+
if (seen) return
|
|
1744
|
+
seen = true
|
|
1745
|
+
if (op === 'local.get' || depth > 0) conditional = true
|
|
1746
|
+
return
|
|
1747
|
+
}
|
|
1748
|
+
const isIf = op === 'if'
|
|
1749
|
+
for (let i = 1; i < n.length && !seen; i++) {
|
|
1750
|
+
const c = n[i]
|
|
1751
|
+
const cond = isIf && Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')
|
|
1752
|
+
if (cond) depth++
|
|
1753
|
+
visit(c)
|
|
1754
|
+
if (cond) depth--
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
for (const n of body) { if (seen) break; visit(n) }
|
|
1758
|
+
if (!seen) return false
|
|
1759
|
+
return conditional
|
|
1760
|
+
}
|
|
1761
|
+
// Module-level references that pin a function (can't be inlined-away/removed).
|
|
1762
|
+
const inlCollectPinned = (n, pinned) => {
|
|
1763
|
+
if (!Array.isArray(n)) return
|
|
1764
|
+
const op = n[0]
|
|
1765
|
+
if (op === 'export' && Array.isArray(n[2]) && n[2][0] === 'func' && typeof n[2][1] === 'string') pinned.add(n[2][1])
|
|
1766
|
+
else if (op === 'start' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1767
|
+
else if (op === 'ref.func' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1768
|
+
else if (op === 'elem') for (const c of n) if (typeof c === 'string' && c[0] === '$') pinned.add(c)
|
|
1769
|
+
for (const c of n) inlCollectPinned(c, pinned)
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/** Pinned function names (export/start/ref.func/elem targets) across the module's
|
|
1773
|
+
* non-func nodes — a Set the inliner must never dissolve.
|
|
1774
|
+
*
|
|
1775
|
+
* KEEP THIS EXTRACTED — do not inline back into inlineOnce. The self-host kernel
|
|
1776
|
+
* (jz compiling jz) mis-compiles this `new Set()` + scan when it lives inside the
|
|
1777
|
+
* oversized inlineOnce scope: the `pinned` value's pointer is zeroed, so the first
|
|
1778
|
+
* `pinned.add` traps in `__set_add` ("memory access out of bounds") on every L2
|
|
1779
|
+
* compile of a program with an inlinable helper. Building it in this small scope
|
|
1780
|
+
* keeps the local count under the threshold that triggers the miscompile. Pinned by
|
|
1781
|
+
* test/selfhost.js "level-2 inliner is sound". (Underlying large-function self-host
|
|
1782
|
+
* codegen bug is tracked separately; this is the surgical dodge.) */
|
|
1783
|
+
const inlBuildPinned = (ast) => {
|
|
1784
|
+
const pinned = new Set()
|
|
1785
|
+
for (const n of ast) if (!Array.isArray(n) || n[0] !== 'func') inlCollectPinned(n, pinned)
|
|
1786
|
+
return pinned
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Parse a func node into { params, locals, inlResult } once, enforcing the
|
|
1790
|
+
// liftability contract (named params/locals, zero-init-able local types, ≤1
|
|
1791
|
+
// result, no inline export). Returns null if the func can't be lifted.
|
|
1792
|
+
const inlParse = (fn) => {
|
|
1793
|
+
const params = [], locals = []
|
|
1794
|
+
let inlResult = null, ok = true, nResult = 0
|
|
1795
|
+
for (let i = 2; i < fn.length; i++) {
|
|
1796
|
+
const c = fn[i]
|
|
1797
|
+
if (typeof c === 'string') continue
|
|
1798
|
+
if (!Array.isArray(c)) { ok = false; break }
|
|
1799
|
+
if (c[0] === 'param') { if (typeof c[1] !== 'string' || c[1][0] !== '$') { ok = false; break } params.push({ name: c[1], type: c[2] }) }
|
|
1800
|
+
else if (c[0] === 'local') { if (typeof c[1] !== 'string' || c[1][0] !== '$' || !inlZeroFor(c[2])) { ok = false; break } locals.push({ name: c[1], type: c[2] }) }
|
|
1801
|
+
else if (c[0] === 'result') { nResult += c.length - 1; if (c.length > 1) inlResult = c[1] }
|
|
1802
|
+
else if (c[0] === 'export') { ok = false; break }
|
|
1803
|
+
else if (c[0] === 'type') continue
|
|
1804
|
+
else break
|
|
1805
|
+
}
|
|
1806
|
+
if (nResult > 1) ok = false
|
|
1807
|
+
return ok ? { params, locals, inlResult } : null
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// IR-node count of a callee body — the cheap size proxy gating multi-caller inline.
|
|
1811
|
+
const inlBodySize = (fn) => {
|
|
1812
|
+
let n = 0
|
|
1813
|
+
const count = (x) => { if (!Array.isArray(x)) return; n++; for (let i = 1; i < x.length; i++) count(x[i]) }
|
|
1814
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) count(fn[i])
|
|
1815
|
+
return n
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Lift one callee into ONE `(call …)` node. Returns `{ block, decls }` — `block`
|
|
1820
|
+
* replaces the call; `decls` are the renamed param+local declarations to splice into
|
|
1821
|
+
* the caller's local list. A fresh uid per invocation keeps every inlined copy's
|
|
1822
|
+
* locals/labels unique, so the same body can be lifted into many sites.
|
|
1823
|
+
*
|
|
1824
|
+
* (call $f a0 a1 …) → (block $__inlN (result T)?
|
|
1825
|
+
* (local.set $__inlN_p0 a0) … ;; args evaluated once, in order
|
|
1826
|
+
* (local.set $__inlN_l reset) … ;; only locals that rely on zero-init
|
|
1827
|
+
* …body, renamed, `return X` → `br $__inlN X`…)
|
|
1828
|
+
*/
|
|
1829
|
+
const buildInline = (params, locals, inlResult, cBody, args) => {
|
|
1830
|
+
const uid = ++inlineUid
|
|
1831
|
+
const exit = `$__inl${uid}`
|
|
1832
|
+
const rename = new Map()
|
|
1833
|
+
for (const p of params) rename.set(p.name, `$__inl${uid}_${p.name.slice(1)}`)
|
|
1834
|
+
for (const l of locals) rename.set(l.name, `$__inl${uid}_${l.name.slice(1)}`)
|
|
1835
|
+
// The callee's own block/loop/if labels would shadow same-named caller labels (and
|
|
1836
|
+
// break depth resolution) under the added nesting — give them fresh names too.
|
|
1837
|
+
const labelRename = new Map()
|
|
1838
|
+
const collectLabels = (n) => {
|
|
1839
|
+
if (!Array.isArray(n)) return
|
|
1840
|
+
if (isBranchScope(n[0]) && typeof n[1] === 'string' && n[1][0] === '$' && !labelRename.has(n[1]))
|
|
1841
|
+
labelRename.set(n[1], `$__inl${uid}L_${n[1].slice(1)}`)
|
|
1842
|
+
for (let i = 1; i < n.length; i++) collectLabels(n[i])
|
|
1843
|
+
}
|
|
1844
|
+
for (const n of cBody) collectLabels(n)
|
|
1845
|
+
const sub = (n) => {
|
|
1846
|
+
if (!Array.isArray(n)) return n
|
|
1847
|
+
const op = n[0]
|
|
1848
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string' && rename.has(n[1]))
|
|
1849
|
+
return [op, rename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1850
|
+
if (op === 'return') return ['br', exit, ...n.slice(1).map(sub)]
|
|
1851
|
+
if (isBranchScope(op) && typeof n[1] === 'string' && labelRename.has(n[1]))
|
|
1852
|
+
return [op, labelRename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1853
|
+
if (inlIsBranch(op)) return [op, ...n.slice(1).map(c => (typeof c === 'string' && labelRename.has(c)) ? labelRename.get(c) : sub(c))]
|
|
1854
|
+
return n.map((c, i) => i === 0 ? c : sub(c))
|
|
1855
|
+
}
|
|
1856
|
+
const setup = params.map((p, k) => ['local.set', rename.get(p.name), args[k]])
|
|
1857
|
+
const resets = locals.filter(l => inlNeedsReset(cBody, l.name)).map(l => ['local.set', rename.get(l.name), inlZeroFor(l.type)])
|
|
1858
|
+
const inner = cBody.map(sub)
|
|
1859
|
+
const block = inlResult
|
|
1860
|
+
? ['block', exit, ['result', inlResult], ...setup, ...resets, ...inner]
|
|
1861
|
+
: ['block', exit, ...setup, ...resets, ...inner]
|
|
1862
|
+
const decls = [...params, ...locals].map(p => ['local', rename.get(p.name), p.type])
|
|
1863
|
+
return { block, decls }
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1197
1866
|
/**
|
|
1198
|
-
* Inline
|
|
1867
|
+
* Inline SMALL functions into every caller, then delete them — the multi-caller
|
|
1868
|
+
* complement to {@link inlineOnce}. inlineOnce only fires for a lone caller (so it
|
|
1869
|
+
* never duplicates); this duplicates a tiny body across ALL its sites, trading a
|
|
1870
|
+
* bounded amount of size to remove call overhead from hot inner loops (e.g. a
|
|
1871
|
+
* raymarcher's per-step SDF, evaluated 4-wide but still paying a wasm call each
|
|
1872
|
+
* march step). Size-for-speed — opt-in, on at the 'speed' level only.
|
|
1873
|
+
*
|
|
1874
|
+
* A callee qualifies when it is small (≤ INLINE_MAX_NODES IR nodes), named with
|
|
1875
|
+
* named params/locals, single-result-or-void, non-recursive, not pinned
|
|
1876
|
+
* (export/start/elem/ref.func), not in the caller's `pin` set, and free of
|
|
1877
|
+
* depth-relative branches / tail calls (the inlParse + inlUnsafe liftability
|
|
1878
|
+
* contract). Runs to a fixpoint so small-helper chains (sdf → sdRep) collapse.
|
|
1879
|
+
*
|
|
1880
|
+
* `simdOnly` (the speed-level default) restricts inlining to pure SIMD helpers —
|
|
1881
|
+
* every param and the result are `v128`. That targets the case this exists for —
|
|
1882
|
+
* a hand-vectorized hot loop's per-step helper (a raymarcher's SDF), where the call
|
|
1883
|
+
* overhead is paid every iteration and V8's wasm JIT won't inline it — while leaving
|
|
1884
|
+
* SCALAR helpers untouched: those are where jz's codegen-shape/size tuning and the
|
|
1885
|
+
* auto-vectorizer's call-lifting (plasma's fbm → sin2) live, and duplicating them
|
|
1886
|
+
* both bloats and perturbs that machinery for no gain (V8's JIT inlines scalar
|
|
1887
|
+
* helpers itself). The unrestricted form stays available as `watr: { inline: true }`.
|
|
1888
|
+
*
|
|
1199
1889
|
* @param {Array} ast
|
|
1890
|
+
* @param {{simdOnly?: boolean}} [opts]
|
|
1200
1891
|
* @returns {Array}
|
|
1201
1892
|
*/
|
|
1202
|
-
const
|
|
1893
|
+
const INLINE_MAX_NODES = 90
|
|
1894
|
+
const isV128SimdHelper = (params, inlResult) =>
|
|
1895
|
+
inlResult === 'v128' && params.length > 0 && params.every(p => p.type === 'v128')
|
|
1896
|
+
const inline = (ast, { simdOnly = false, pin = EMPTY_SET } = {}) => {
|
|
1203
1897
|
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1204
1898
|
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1899
|
+
const skip = new Set() // callees with a non-inlinable site (arity mismatch) — don't re-pick
|
|
1900
|
+
for (let round = 0; round < MAX_INLINE_ROUNDS; round++) {
|
|
1901
|
+
const funcs = ast.filter(n => Array.isArray(n) && n[0] === 'func')
|
|
1902
|
+
const funcByName = new Map()
|
|
1903
|
+
for (const n of funcs) if (typeof n[1] === 'string') funcByName.set(n[1], n)
|
|
1207
1904
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1905
|
+
const callRefs = new Map(), otherRef = new Set()
|
|
1906
|
+
const countRefs = (n) => {
|
|
1907
|
+
if (!Array.isArray(n)) return
|
|
1908
|
+
const op = n[0]
|
|
1909
|
+
if (op === 'call' && typeof n[1] === 'string') callRefs.set(n[1], (callRefs.get(n[1]) || 0) + 1)
|
|
1910
|
+
else if (op === 'return_call' && typeof n[1] === 'string') otherRef.add(n[1])
|
|
1911
|
+
for (let i = 1; i < n.length; i++) countRefs(n[i])
|
|
1912
|
+
}
|
|
1913
|
+
countRefs(ast)
|
|
1914
|
+
const pinned = new Set()
|
|
1915
|
+
for (const n of ast) if (!Array.isArray(n) || n[0] !== 'func') inlCollectPinned(n, pinned)
|
|
1210
1916
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1917
|
+
// Pick a small, liftable, non-recursive callee with ≥1 plain-call site.
|
|
1918
|
+
let calleeName = null, parsed = null
|
|
1919
|
+
for (const [name, fn] of funcByName) {
|
|
1920
|
+
if (skip.has(name) || pinned.has(name) || otherRef.has(name) || pin.has(name)) continue
|
|
1921
|
+
if (!(callRefs.get(name) >= 1)) continue
|
|
1922
|
+
if (inlBodySize(fn) > INLINE_MAX_NODES) continue
|
|
1923
|
+
if (inlCallsSelf(fn, name)) continue
|
|
1924
|
+
const p = inlParse(fn)
|
|
1925
|
+
if (!p) continue
|
|
1926
|
+
if (simdOnly && !isV128SimdHelper(p.params, p.inlResult)) continue
|
|
1927
|
+
let bad = false
|
|
1928
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) if (inlUnsafe(fn[i])) { bad = true; break }
|
|
1929
|
+
if (bad) continue
|
|
1930
|
+
calleeName = name; parsed = p; break
|
|
1931
|
+
}
|
|
1932
|
+
if (!calleeName) break
|
|
1213
1933
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
let
|
|
1934
|
+
const callee = funcByName.get(calleeName)
|
|
1935
|
+
const { params, locals, inlResult } = parsed
|
|
1936
|
+
const cBody = callee.slice(inlBodyStart(callee))
|
|
1937
|
+
const expected = callRefs.get(calleeName) || 0 // callee is non-recursive ⇒ all sites are in other funcs
|
|
1938
|
+
let replaced = 0
|
|
1219
1939
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
params
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
hasExport = true
|
|
1236
|
-
} else if (sub[0] !== 'result' && sub[0] !== 'type') {
|
|
1237
|
-
body.push(sub)
|
|
1940
|
+
// Splice into EVERY caller. A body that itself still calls an as-yet-uninlined
|
|
1941
|
+
// helper is fine — later rounds collapse it (or it stays a call).
|
|
1942
|
+
for (const fn of funcs) {
|
|
1943
|
+
if (fn === callee) continue
|
|
1944
|
+
const addDecls = []
|
|
1945
|
+
for (let i = inlBodyStart(fn); i < fn.length; i++) {
|
|
1946
|
+
fn[i] = walkPost(fn[i], (n) => {
|
|
1947
|
+
if (!Array.isArray(n) || n[0] !== 'call' || n[1] !== calleeName) return
|
|
1948
|
+
const args = n.slice(2)
|
|
1949
|
+
if (args.length !== params.length) return // arity mismatch — leave the call
|
|
1950
|
+
const { block, decls } = buildInline(params, locals, inlResult, cBody, args)
|
|
1951
|
+
addDecls.push(...decls)
|
|
1952
|
+
replaced++
|
|
1953
|
+
return block
|
|
1954
|
+
})
|
|
1238
1955
|
}
|
|
1956
|
+
if (addDecls.length) fn.splice(inlBodyStart(fn), 0, ...addDecls)
|
|
1239
1957
|
}
|
|
1240
1958
|
|
|
1241
|
-
//
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
// or contains a control-transfer op (`return`, `return_call`,
|
|
1245
|
-
// `return_call_indirect`). Inlining such bodies into a different-typed
|
|
1246
|
-
// caller would propagate the transfer to the caller, returning from the
|
|
1247
|
-
// wrong function with the wrong type. Lifting the body into a
|
|
1248
|
-
// `(block $exit ...)` and rewriting returns to `(br $exit X)` would
|
|
1249
|
-
// unlock these — left for a future pass.
|
|
1250
|
-
const paramNames = new Set(params.map(p => p.name))
|
|
1251
|
-
let mutatesParam = false
|
|
1252
|
-
let hasReturn = false
|
|
1253
|
-
walk(body[0], (n) => {
|
|
1254
|
-
if (!Array.isArray(n)) return
|
|
1255
|
-
if ((n[0] === 'local.set' || n[0] === 'local.tee') && paramNames.has(n[1])) {
|
|
1256
|
-
mutatesParam = true
|
|
1257
|
-
}
|
|
1258
|
-
if (n[0] === 'return' || n[0] === 'return_call' || n[0] === 'return_call_indirect') {
|
|
1259
|
-
hasReturn = true
|
|
1260
|
-
}
|
|
1261
|
-
})
|
|
1262
|
-
if (!mutatesParam && !hasReturn) {
|
|
1263
|
-
inlinable.set(name, { body: body[0], params })
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1959
|
+
// Drop the callee only if every site inlined; else keep it and stop re-picking it.
|
|
1960
|
+
if (replaced === expected) { const idx = ast.indexOf(callee); if (idx >= 0) ast.splice(idx, 1) }
|
|
1961
|
+
else skip.add(calleeName)
|
|
1266
1962
|
}
|
|
1267
1963
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
walkPost(ast, (node) => {
|
|
1272
|
-
if (!Array.isArray(node) || node[0] !== 'call') return
|
|
1273
|
-
const fname = node[1]
|
|
1274
|
-
if (!inlinable.has(fname)) return
|
|
1964
|
+
return ast
|
|
1965
|
+
}
|
|
1275
1966
|
|
|
1276
|
-
|
|
1277
|
-
const args = node.slice(2)
|
|
1967
|
+
// ==================== INLINE-ONCE ====================
|
|
1278
1968
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1969
|
+
/**
|
|
1970
|
+
* Devirtualize `call_indirect` through NaN-boxed closure values with a statically
|
|
1971
|
+
* known candidate set. `let f = c ? a : b; … f(x)` emits a select of two i64
|
|
1972
|
+
* closure constants into an f64 local; every call site then derives the table
|
|
1973
|
+
* slot from that local's bits:
|
|
1974
|
+
* (i32.wrap_i64 (i64.and (i64.shr_u (i64.reinterpret_f64 (local.get $f))
|
|
1975
|
+
* (i64.const 32)) (i64.const 32767)))
|
|
1976
|
+
* When EVERY write to $f in the function is such a constant set (≤2 candidates),
|
|
1977
|
+
* each call site becomes a guarded direct call —
|
|
1978
|
+
* (if (result …) (i64.eq (i64.reinterpret_f64 (local.get $f)) (i64.const C1))
|
|
1979
|
+
* (then (call $tramp1 …args)) (else <next guard | original call_indirect>))
|
|
1980
|
+
* — with the ORIGINAL call_indirect kept as the final arm, so unknown flows
|
|
1981
|
+
* (zero-init paths the analysis can't see) behave exactly as before: the rewrite
|
|
1982
|
+
* is a pure branch-predicted fast path, ~25% on callback loops, and the direct
|
|
1983
|
+
* calls participate in inlining. A trivially-constant slot ((i32.const N) after
|
|
1984
|
+
* fold) becomes a bare direct call with no guard.
|
|
1985
|
+
*
|
|
1986
|
+
* Soundness: the guard compares the SAME bits the slot extraction reads, so
|
|
1987
|
+
* whichever constant flows to the call dispatches identically in both forms;
|
|
1988
|
+
* candidates that don't resolve to an elem entry (or whose target's signature
|
|
1989
|
+
* differs from the call's type — would-be runtime trap) disable the site. Any
|
|
1990
|
+
* table mutation op in the module disables the pass entirely. The function
|
|
1991
|
+
* table is exported for host-side closure invocation (reads); host mutation of
|
|
1992
|
+
* it is outside the ABI contract, same as the closure-constant model itself.
|
|
1993
|
+
*/
|
|
1994
|
+
const devirt = (ast) => {
|
|
1995
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1996
|
+
// Module facts: elem slot → func name (constant offsets only), type defs,
|
|
1997
|
+
// named funcs. Bail on dynamic elem offsets or table mutation anywhere.
|
|
1998
|
+
const slots = new Map(), typeDefs = new Map(), funcsByName = new Map(), allFuncs = []
|
|
1999
|
+
let tableMutated = false
|
|
2000
|
+
walk(ast, n => {
|
|
2001
|
+
if (Array.isArray(n) && typeof n[0] === 'string' &&
|
|
2002
|
+
(n[0] === 'table.set' || n[0] === 'table.grow' || n[0] === 'table.init' ||
|
|
2003
|
+
n[0] === 'table.copy' || n[0] === 'table.fill')) tableMutated = true
|
|
2004
|
+
})
|
|
2005
|
+
if (tableMutated) return ast
|
|
2006
|
+
for (const node of ast.slice(1)) {
|
|
2007
|
+
if (!Array.isArray(node)) continue
|
|
2008
|
+
if (node[0] === 'elem') {
|
|
2009
|
+
const off = node[1]
|
|
2010
|
+
if (!Array.isArray(off) || off[0] !== 'i32.const') return ast
|
|
2011
|
+
let base = Number(off[1])
|
|
2012
|
+
for (let i = 2; i < node.length; i++)
|
|
2013
|
+
if (typeof node[i] === 'string' && node[i][0] === '$') slots.set(base++, node[i])
|
|
2014
|
+
}
|
|
2015
|
+
else if (node[0] === 'type' && typeof node[1] === 'string') typeDefs.set(node[1], node[2])
|
|
2016
|
+
else if (node[0] === 'func') { allFuncs.push(node); if (typeof node[1] === 'string') funcsByName.set(node[1], node) }
|
|
2017
|
+
}
|
|
2018
|
+
if (!slots.size) return ast
|
|
2019
|
+
|
|
2020
|
+
// Closure-valued GLOBALS: multiProp function-property slots dissolve into
|
|
2021
|
+
// f64 module globals (plan/scope.js flattenFuncNamespaces) — the subscript
|
|
2022
|
+
// hook pattern (`parse.space = fn`, overridden per feature at module init).
|
|
2023
|
+
// Every observed `global.set $G <closure-const>` contributes a candidate;
|
|
2024
|
+
// a non-const store poisons the global. The guard ladder stays SOUND even
|
|
2025
|
+
// with an incomplete set — unknown values take the original call_indirect
|
|
2026
|
+
// fallback arm — so candidates only need to cover the hot value.
|
|
2027
|
+
const globalCands = new Map()
|
|
2028
|
+
|
|
2029
|
+
// All i64 const handling is canonical-hex STRING math (see the i64 VALUE
|
|
2030
|
+
// CONTRACT above): a helper RETURNING a BigInt is kind-erased in-kernel and
|
|
2031
|
+
// every op on it misdispatches — devirt silently no-ops.
|
|
2032
|
+
const isC64 = (n, hex) => Array.isArray(n) && n[0] === 'i64.const' && _i64Canon(n[1]) === hex
|
|
2033
|
+
const MASK15 = '0x0000000000007fff', SHIFT32 = '0x0000000000000020'
|
|
2034
|
+
// Collect the i64 constants reachable through reinterpret/select arms.
|
|
2035
|
+
const boxConsts = (v, out) => {
|
|
2036
|
+
if (!Array.isArray(v)) return false
|
|
2037
|
+
if (v[0] === 'i64.const') { out.push(v); return true }
|
|
2038
|
+
// f64-carrier closure const (`f64.const nan:0xHEX`) — the form module-init
|
|
2039
|
+
// global.set stores for hook slots; normalize to its i64 bits.
|
|
2040
|
+
if (v[0] === 'f64.const' && typeof v[1] === 'string' && v[1].startsWith('nan:')) {
|
|
2041
|
+
out.push(['i64.const', _i64Canon(v[1].slice(4))])
|
|
2042
|
+
return true
|
|
2043
|
+
}
|
|
2044
|
+
if (v[0] === 'f64.reinterpret_i64' && v.length === 2) return boxConsts(v[1], out)
|
|
2045
|
+
if (v[0] === 'select' && v.length === 4) return boxConsts(v[1], out) && boxConsts(v[2], out)
|
|
2046
|
+
return false
|
|
2047
|
+
}
|
|
2048
|
+
// Per-global write VALUES collected first; candidates resolved by fixpoint so
|
|
2049
|
+
// the hook-alias pattern works: `baseSpace = parse.space ?? default` stores a
|
|
2050
|
+
// select/if whose arms are a GLOBAL READ of another const slot plus a const —
|
|
2051
|
+
// candidates = union through the alias edge. Soundness is unchanged (the
|
|
2052
|
+
// guard ladder keeps the original indirect fallback for unknown values); the
|
|
2053
|
+
// fixpoint only widens the candidate set. `if (result f64)` arms and
|
|
2054
|
+
// `__is_nullish`-style guard CONDITIONS are skipped — only VALUE positions
|
|
2055
|
+
// contribute. A write that contains anything else poisons the global.
|
|
2056
|
+
const globalWrites = new Map()
|
|
2057
|
+
walk(ast, n => {
|
|
2058
|
+
if (!Array.isArray(n) || n[0] !== 'global.set' || typeof n[1] !== 'string') return
|
|
2059
|
+
if (!globalWrites.has(n[1])) globalWrites.set(n[1], [])
|
|
2060
|
+
globalWrites.get(n[1]).push(n[2])
|
|
2061
|
+
})
|
|
2062
|
+
// Value-position scan: consts and global.get leaves, through reinterprets,
|
|
2063
|
+
// select arms and if/result arms. Returns false (poison) on anything else.
|
|
2064
|
+
const candLeaves = (v, consts, reads) => {
|
|
2065
|
+
if (!Array.isArray(v)) return false
|
|
2066
|
+
if (v[0] === 'i64.const') { consts.push(v); return true }
|
|
2067
|
+
if (v[0] === 'f64.const' && typeof v[1] === 'string' && v[1].startsWith('nan:')) {
|
|
2068
|
+
consts.push(['i64.const', _i64Canon(v[1].slice(4))]); return true
|
|
2069
|
+
}
|
|
2070
|
+
if ((v[0] === 'f64.reinterpret_i64' || v[0] === 'i64.reinterpret_f64') && v.length === 2)
|
|
2071
|
+
return candLeaves(v[1], consts, reads)
|
|
2072
|
+
if (v[0] === 'global.get' && typeof v[1] === 'string') { reads.push(v[1]); return true }
|
|
2073
|
+
if (v[0] === 'local.get' || v[0] === 'local.tee') {
|
|
2074
|
+
// a tee'd copy of one of the above — the tee VALUE was already scanned
|
|
2075
|
+
// where it was written; the bare read alone proves nothing → poison
|
|
2076
|
+
return v[0] === 'local.tee' && v.length === 3 ? candLeaves(v[2], consts, reads) : false
|
|
2077
|
+
}
|
|
2078
|
+
if (v[0] === 'select' && v.length === 4)
|
|
2079
|
+
return candLeaves(v[1], consts, reads) && candLeaves(v[2], consts, reads)
|
|
2080
|
+
if (v[0] === 'if') {
|
|
2081
|
+
// (if (result T) COND (then A) (else B)) — arms are value positions
|
|
2082
|
+
let ok = true, seenArm = false
|
|
2083
|
+
for (let i = 1; i < v.length; i++) {
|
|
2084
|
+
const p = v[i]
|
|
2085
|
+
if (!Array.isArray(p)) continue
|
|
2086
|
+
if (p[0] === 'then' || p[0] === 'else') {
|
|
2087
|
+
seenArm = true
|
|
2088
|
+
if (p.length !== 2 || !candLeaves(p[1], consts, reads)) ok = false
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return ok && seenArm
|
|
1282
2092
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
2093
|
+
return false
|
|
2094
|
+
}
|
|
2095
|
+
const writeFacts = new Map() // global → { consts: [...], reads: [...] } | null
|
|
2096
|
+
for (const [g, ws] of globalWrites) {
|
|
2097
|
+
let consts = [], reads = [], ok = true
|
|
2098
|
+
for (const w of ws) if (!candLeaves(w, consts, reads)) { ok = false; break }
|
|
2099
|
+
writeFacts.set(g, ok ? { consts, reads } : null)
|
|
2100
|
+
}
|
|
2101
|
+
// Fixpoint: a global's candidates = its const writes ∪ candidates of every
|
|
2102
|
+
// global it reads in value position. A poisoned alias poisons the reader.
|
|
2103
|
+
let changed = true
|
|
2104
|
+
const resolved = new Map()
|
|
2105
|
+
while (changed) {
|
|
2106
|
+
changed = false
|
|
2107
|
+
for (const [g, f] of writeFacts) {
|
|
2108
|
+
if (resolved.get(g) === null) continue
|
|
2109
|
+
if (f === null) { if (resolved.get(g) !== null) { resolved.set(g, null); changed = true } continue }
|
|
2110
|
+
const m = resolved.get(g) || new Map()
|
|
2111
|
+
const before = m.size
|
|
2112
|
+
let poisoned = false
|
|
2113
|
+
for (const c of f.consts) m.set(_i64Canon(c[1]), c)
|
|
2114
|
+
for (const r of f.reads) {
|
|
2115
|
+
if (writeFacts.get(r) === null || resolved.get(r) === null) { poisoned = true; break }
|
|
2116
|
+
const rm = resolved.get(r)
|
|
2117
|
+
if (rm) for (const [hex, c] of rm) m.set(hex, c)
|
|
1291
2118
|
}
|
|
1292
|
-
|
|
2119
|
+
if (poisoned) { resolved.set(g, null); changed = true; continue }
|
|
2120
|
+
if (!resolved.has(g) || m.size !== before) { resolved.set(g, m); changed = true }
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
for (const [g, m] of resolved) globalCands.set(g, m)
|
|
2124
|
+
|
|
2125
|
+
// The slot-extraction idiom — returns the source local name or null.
|
|
2126
|
+
const matchSlotOfLocal = (e) => {
|
|
2127
|
+
if (!Array.isArray(e) || e[0] !== 'i32.wrap_i64') return null
|
|
2128
|
+
const a = e[1]
|
|
2129
|
+
if (!Array.isArray(a) || a[0] !== 'i64.and') return null
|
|
2130
|
+
let sh = a[1], mk = a[2]
|
|
2131
|
+
if (!isC64(mk, MASK15)) { sh = a[2]; mk = a[1] }
|
|
2132
|
+
if (!isC64(mk, MASK15) || !Array.isArray(sh) || sh[0] !== 'i64.shr_u' || !isC64(sh[2], SHIFT32)) return null
|
|
2133
|
+
const ri = sh[1]
|
|
2134
|
+
if (!Array.isArray(ri) || ri[0] !== 'i64.reinterpret_f64') return null
|
|
2135
|
+
const leaf = ri[1]
|
|
2136
|
+
if (Array.isArray(leaf) && leaf[0] === 'local.get' && typeof leaf[1] === 'string') return { local: leaf[1] }
|
|
2137
|
+
if (Array.isArray(leaf) && leaf[0] === 'global.get' && typeof leaf[1] === 'string') return { global: leaf[1] }
|
|
2138
|
+
return null
|
|
2139
|
+
}
|
|
2140
|
+
// Canonical "params -> results" token string for signature comparison.
|
|
2141
|
+
const tokSig = (parts) => {
|
|
2142
|
+
const ps = [], rs = []
|
|
2143
|
+
for (const p of parts) {
|
|
2144
|
+
if (!Array.isArray(p)) continue
|
|
2145
|
+
if (p[0] === 'param') { for (const t of p.slice(1)) if (typeof t === 'string' && t[0] !== '$') ps.push(t) }
|
|
2146
|
+
else if (p[0] === 'result') rs.push(...p.slice(1))
|
|
2147
|
+
}
|
|
2148
|
+
return ps.join(',') + '->' + rs.join(',')
|
|
2149
|
+
}
|
|
1293
2150
|
|
|
1294
|
-
|
|
1295
|
-
|
|
2151
|
+
for (const fn of allFuncs) { // rewrite call_indirect in EVERY func, named or not
|
|
2152
|
+
// Candidate sets: local → Map<bits, constNode>, or null once poisoned.
|
|
2153
|
+
// Params are poisoned (incoming value unknown).
|
|
2154
|
+
const cands = new Map()
|
|
2155
|
+
for (const part of fn)
|
|
2156
|
+
if (Array.isArray(part) && part[0] === 'param' && typeof part[1] === 'string') cands.set(part[1], null)
|
|
2157
|
+
walk(fn, n => {
|
|
2158
|
+
if (!Array.isArray(n) || (n[0] !== 'local.set' && n[0] !== 'local.tee') || typeof n[1] !== 'string') return
|
|
2159
|
+
if (cands.get(n[1]) === null) return
|
|
2160
|
+
const out = []
|
|
2161
|
+
if (boxConsts(n[2], out)) {
|
|
2162
|
+
const m = cands.get(n[1]) || new Map()
|
|
2163
|
+
for (const c of out) m.set(_i64Canon(c[1]), c)
|
|
2164
|
+
cands.set(n[1], m)
|
|
2165
|
+
} else if (Array.isArray(n[2]) && n[2][0] === 'global.get' && typeof n[2][1] === 'string'
|
|
2166
|
+
&& globalCands.get(n[2][1])) {
|
|
2167
|
+
// promoteGlobals snapshot (`$_pg = global.get $G`) — inherit G's set
|
|
2168
|
+
const g = globalCands.get(n[2][1])
|
|
2169
|
+
const m = cands.get(n[1]) || new Map()
|
|
2170
|
+
for (const [hex, c] of g) m.set(hex, c)
|
|
2171
|
+
cands.set(n[1], m)
|
|
2172
|
+
} else cands.set(n[1], null)
|
|
2173
|
+
})
|
|
1296
2174
|
|
|
2175
|
+
walkPost(fn, (n, parent) => {
|
|
2176
|
+
if (!Array.isArray(n) || n[0] !== 'call_indirect') return
|
|
2177
|
+
// A call_indirect sitting directly under an `else` is (or looks exactly
|
|
2178
|
+
// like) the fallback arm of an existing guard — never re-wrap it, so the
|
|
2179
|
+
// pass is idempotent across repeated optimize() runs.
|
|
2180
|
+
if (parent && parent[0] === 'else') return
|
|
2181
|
+
const typeUse = Array.isArray(n[1]) && n[1][0] === 'type' ? n[1] : null
|
|
2182
|
+
if (!typeUse) return
|
|
2183
|
+
const sig = typeDefs.get(typeUse[1])
|
|
2184
|
+
const callSig = Array.isArray(sig) ? tokSig(sig.slice(1)) : null
|
|
2185
|
+
if (callSig == null) return
|
|
2186
|
+
const results = []
|
|
2187
|
+
for (const s of sig.slice(1)) if (Array.isArray(s) && s[0] === 'result') results.push(...s.slice(1))
|
|
2188
|
+
const args = n.slice(2, -1)
|
|
2189
|
+
const idx = n[n.length - 1]
|
|
2190
|
+
const sigOk = (name) => {
|
|
2191
|
+
const target = funcsByName.get(name)
|
|
2192
|
+
if (!target) return false
|
|
2193
|
+
const tu = target.find(p => Array.isArray(p) && p[0] === 'type')
|
|
2194
|
+
if (tu) return tu[1] === typeUse[1] ||
|
|
2195
|
+
(typeDefs.get(tu[1]) && tokSig(typeDefs.get(tu[1]).slice(1)) === callSig)
|
|
2196
|
+
return tokSig(target.slice(2)) === callSig
|
|
2197
|
+
}
|
|
2198
|
+
// Constant slot → bare direct call.
|
|
2199
|
+
if (Array.isArray(idx) && idx[0] === 'i32.const') {
|
|
2200
|
+
const name = slots.get(Number(idx[1]))
|
|
2201
|
+
return name && sigOk(name) ? ['call', name, ...args] : undefined
|
|
2202
|
+
}
|
|
2203
|
+
const f = matchSlotOfLocal(idx)
|
|
2204
|
+
if (!f) return
|
|
2205
|
+
const m = f.local != null ? cands.get(f.local) : globalCands.get(f.global)
|
|
2206
|
+
if (!m || m.size === 0 || m.size > 4) return
|
|
2207
|
+
const arms = []
|
|
2208
|
+
for (const cNode of m.values()) {
|
|
2209
|
+
const name = slots.get(_i64HiU(_i64Canon(cNode[1])) & 32767)
|
|
2210
|
+
if (!name || !sigOk(name)) return
|
|
2211
|
+
arms.push([cNode, name])
|
|
2212
|
+
}
|
|
2213
|
+
const readBack = f.local != null ? ['local.get', f.local] : ['global.get', f.global]
|
|
2214
|
+
let out = n
|
|
2215
|
+
for (let i = arms.length - 1; i >= 0; i--) {
|
|
2216
|
+
const [cNode, name] = arms[i]
|
|
2217
|
+
out = ['if', ...(results.length ? [['result', ...results]] : []),
|
|
2218
|
+
['i64.eq', ['i64.reinterpret_f64', clone(readBack)], clone(cNode)],
|
|
2219
|
+
['then', ['call', name, ...args.map(clone)]],
|
|
2220
|
+
['else', out]]
|
|
2221
|
+
}
|
|
2222
|
+
return out
|
|
2223
|
+
})
|
|
2224
|
+
}
|
|
1297
2225
|
return ast
|
|
1298
2226
|
}
|
|
1299
2227
|
|
|
1300
|
-
// ==================== INLINE-ONCE ====================
|
|
1301
|
-
|
|
1302
|
-
let inlineUid = 0
|
|
1303
|
-
|
|
1304
2228
|
/**
|
|
1305
2229
|
* Inline functions that are called from exactly one place into their lone caller,
|
|
1306
2230
|
* then delete them. Unlike {@link inline} (which duplicates tiny stateless bodies),
|
|
@@ -1330,101 +2254,15 @@ let inlineUid = 0
|
|
|
1330
2254
|
* @param {Array} ast
|
|
1331
2255
|
* @returns {Array}
|
|
1332
2256
|
*/
|
|
1333
|
-
const inlineOnce = (ast) => {
|
|
2257
|
+
const inlineOnce = (ast, { pin = EMPTY_SET } = {}) => {
|
|
1334
2258
|
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
1335
2259
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
return i
|
|
1341
|
-
}
|
|
1342
|
-
const isBranch = op => op === 'br' || op === 'br_if' || op === 'br_table'
|
|
1343
|
-
// A subtree we can't lift into a (block …): depth-relative branch labels (shift
|
|
1344
|
-
// under added nesting) or tail calls (would escape the wrapping block).
|
|
1345
|
-
const unsafe = (n) => {
|
|
1346
|
-
if (!Array.isArray(n)) return false
|
|
1347
|
-
const op = n[0]
|
|
1348
|
-
if (op === 'return_call' || op === 'return_call_indirect' || op === 'return_call_ref') return true
|
|
1349
|
-
if (op === 'try' || op === 'try_table' || op === 'delegate' || op === 'rethrow') return true // exception labels — not handled by the relabeler below
|
|
1350
|
-
if (isBranch(op)) for (let i = 1; i < n.length; i++) if (typeof n[i] === 'number' || (typeof n[i] === 'string' && /^\d+$/.test(n[i]))) return true
|
|
1351
|
-
for (let i = 1; i < n.length; i++) if (unsafe(n[i])) return true
|
|
1352
|
-
return false
|
|
1353
|
-
}
|
|
1354
|
-
const callsSelf = (n, name) => {
|
|
1355
|
-
if (!Array.isArray(n)) return false
|
|
1356
|
-
if ((n[0] === 'call' || n[0] === 'return_call') && n[1] === name) return true
|
|
1357
|
-
for (let i = 1; i < n.length; i++) if (callsSelf(n[i], name)) return true
|
|
1358
|
-
return false
|
|
1359
|
-
}
|
|
1360
|
-
// Locals must be re-zeroed each time the inlined block is entered IF the
|
|
1361
|
-
// callee body actually relies on zero-init — i.e. some path reads the local
|
|
1362
|
-
// before any unconditional write. In the original callee they got fresh
|
|
1363
|
-
// zero-init per call; after inlining they're outer-func locals, zeroed only
|
|
1364
|
-
// at outer entry, so a caller-loop that re-enters the inlined block reads
|
|
1365
|
-
// stale values otherwise. Returns null for any type we can't safely
|
|
1366
|
-
// zero-init here (skip inlining such callees).
|
|
1367
|
-
const zeroFor = (t) => {
|
|
1368
|
-
if (t === 'i32') return ['i32.const', 0]
|
|
1369
|
-
if (t === 'i64') return ['i64.const', 0n]
|
|
1370
|
-
if (t === 'f32') return ['f32.const', 0]
|
|
1371
|
-
if (t === 'f64') return ['f64.const', 0]
|
|
1372
|
-
if (t === 'v128') return ['v128.const', 'i64x2', 0n, 0n]
|
|
1373
|
-
// Nullable ref types (`(ref null …)`, `funcref`, `externref`, `anyref`, etc.)
|
|
1374
|
-
// zero-init to `ref.null …` per call; emitting that here would need the exact
|
|
1375
|
-
// heap-type. Non-nullable refs aren't zero-init at all (codegen must seed
|
|
1376
|
-
// them). Either way, skip — let the call survive.
|
|
1377
|
-
return null
|
|
1378
|
-
}
|
|
2260
|
+
// Lift primitives are shared with `inline` (defined once above buildInline). inlineOnce
|
|
2261
|
+
// splices into a SINGLE caller (never duplicating); `inline` duplicates into every caller.
|
|
2262
|
+
const bodyStart = inlBodyStart, callsSelf = inlCallsSelf, unsafe = inlUnsafe, isBranch = inlIsBranch
|
|
2263
|
+
const zeroFor = inlZeroFor, needsReset = inlNeedsReset
|
|
1379
2264
|
|
|
1380
|
-
|
|
1381
|
-
// a conditional branch, where the alternate path bypasses it — depend on
|
|
1382
|
-
// zero-init and need a reset when inlined into a caller-loop. Locals that
|
|
1383
|
-
// are unconditionally written before any read (the common scratch pattern,
|
|
1384
|
-
// e.g. `(local.set $bits (local.get $ptr))` opening a helper) don't, and
|
|
1385
|
-
// emitting a spurious reset would only inflate that local's set-count and
|
|
1386
|
-
// block downstream propagation/coalescing. Mirrors `coalesceLocals`'
|
|
1387
|
-
// `readsZero` heuristic.
|
|
1388
|
-
const needsReset = (body, name) => {
|
|
1389
|
-
let seen = false, conditional = false, depth = 0
|
|
1390
|
-
const visit = (n) => {
|
|
1391
|
-
if (seen || !Array.isArray(n)) return
|
|
1392
|
-
const op = n[0]
|
|
1393
|
-
const isSet = op === 'local.set' || op === 'local.tee'
|
|
1394
|
-
if ((isSet || op === 'local.get') && n[1] === name) {
|
|
1395
|
-
if (isSet) for (let i = 2; i < n.length && !seen; i++) visit(n[i])
|
|
1396
|
-
if (seen) return
|
|
1397
|
-
seen = true
|
|
1398
|
-
if (op === 'local.get' || depth > 0) conditional = true
|
|
1399
|
-
return
|
|
1400
|
-
}
|
|
1401
|
-
const isIf = op === 'if'
|
|
1402
|
-
for (let i = 1; i < n.length && !seen; i++) {
|
|
1403
|
-
const c = n[i]
|
|
1404
|
-
const cond = isIf && Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')
|
|
1405
|
-
if (cond) depth++
|
|
1406
|
-
visit(c)
|
|
1407
|
-
if (cond) depth--
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
for (const n of body) { if (seen) break; visit(n) }
|
|
1411
|
-
// If the local is never used (dead), no reset; the dead decl will be pruned.
|
|
1412
|
-
if (!seen) return false
|
|
1413
|
-
return conditional
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// Module-level references that pin a function (can't be removed/inlined-away).
|
|
1417
|
-
const collectPinned = (n, pinned) => {
|
|
1418
|
-
if (!Array.isArray(n)) return
|
|
1419
|
-
const op = n[0]
|
|
1420
|
-
if (op === 'export' && Array.isArray(n[2]) && n[2][0] === 'func' && typeof n[2][1] === 'string') pinned.add(n[2][1])
|
|
1421
|
-
else if (op === 'start' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1422
|
-
else if (op === 'ref.func' && typeof n[1] === 'string') pinned.add(n[1])
|
|
1423
|
-
else if (op === 'elem') for (const c of n) if (typeof c === 'string' && c[0] === '$') pinned.add(c)
|
|
1424
|
-
for (const c of n) collectPinned(c, pinned)
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
for (let round = 0; round < 16; round++) {
|
|
2265
|
+
for (let round = 0; round < MAX_INLINE_ROUNDS; round++) {
|
|
1428
2266
|
const funcs = ast.filter(n => Array.isArray(n) && n[0] === 'func')
|
|
1429
2267
|
const funcByName = new Map()
|
|
1430
2268
|
for (const n of funcs) if (typeof n[1] === 'string') funcByName.set(n[1], n)
|
|
@@ -1440,8 +2278,7 @@ const inlineOnce = (ast) => {
|
|
|
1440
2278
|
for (let i = 1; i < n.length; i++) countRefs(n[i])
|
|
1441
2279
|
}
|
|
1442
2280
|
countRefs(ast)
|
|
1443
|
-
const pinned =
|
|
1444
|
-
for (const n of ast) if (!Array.isArray(n) || n[0] !== 'func') collectPinned(n, pinned)
|
|
2281
|
+
const pinned = inlBuildPinned(ast)
|
|
1445
2282
|
// a func may carry its own (export "name") — the signature scan below rejects those too
|
|
1446
2283
|
|
|
1447
2284
|
// Pick a callee.
|
|
@@ -1449,6 +2286,10 @@ const inlineOnce = (ast) => {
|
|
|
1449
2286
|
for (const [name, fn] of funcByName) {
|
|
1450
2287
|
if (pinned.has(name) || otherRef.has(name)) continue
|
|
1451
2288
|
if (callRefs.get(name) !== 1) continue
|
|
2289
|
+
// Caller-pinned functions stay intact: inlining a single-caller helper would dissolve the
|
|
2290
|
+
// call node, and a consumer may rely on it surviving (e.g. jz pins the scalar transcendentals
|
|
2291
|
+
// its auto-vectorizer later rewrites to f64x2 mirrors). The policy lives with the caller.
|
|
2292
|
+
if (pin.has(name)) continue
|
|
1452
2293
|
if (callsSelf(fn, name)) continue
|
|
1453
2294
|
// named params/locals only (we'll rename them); reject locals with types
|
|
1454
2295
|
// we can't zero-init on block re-entry.
|
|
@@ -1495,11 +2336,10 @@ const inlineOnce = (ast) => {
|
|
|
1495
2336
|
for (const l of locals) rename.set(l.name, `$__inl${uid}_${l.name.slice(1)}`)
|
|
1496
2337
|
// The callee's own block/loop/if labels would shadow same-named labels in the
|
|
1497
2338
|
// caller after nesting (and break depth resolution) — give them fresh names too.
|
|
1498
|
-
const isBlockLabel = op => op === 'block' || op === 'loop' || op === 'if'
|
|
1499
2339
|
const labelRename = new Map()
|
|
1500
2340
|
const collectLabels = (n) => {
|
|
1501
2341
|
if (!Array.isArray(n)) return
|
|
1502
|
-
if (
|
|
2342
|
+
if (isBranchScope(n[0]) && typeof n[1] === 'string' && n[1][0] === '$' && !labelRename.has(n[1]))
|
|
1503
2343
|
labelRename.set(n[1], `$__inl${uid}L_${n[1].slice(1)}`)
|
|
1504
2344
|
for (let i = 1; i < n.length; i++) collectLabels(n[i])
|
|
1505
2345
|
}
|
|
@@ -1510,7 +2350,7 @@ const inlineOnce = (ast) => {
|
|
|
1510
2350
|
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string' && rename.has(n[1]))
|
|
1511
2351
|
return [op, rename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1512
2352
|
if (op === 'return') return ['br', exit, ...n.slice(1).map(sub)]
|
|
1513
|
-
if (
|
|
2353
|
+
if (isBranchScope(op) && typeof n[1] === 'string' && labelRename.has(n[1]))
|
|
1514
2354
|
return [op, labelRename.get(n[1]), ...n.slice(2).map(sub)]
|
|
1515
2355
|
if (isBranch(op)) return [op, ...n.slice(1).map(c => (typeof c === 'string' && labelRename.has(c)) ? labelRename.get(c) : sub(c))]
|
|
1516
2356
|
return n.map((c, i) => i === 0 ? c : sub(c))
|
|
@@ -1832,13 +2672,20 @@ const vacuum = (ast) => {
|
|
|
1832
2672
|
// Remove nop entirely (return array marker; parent or post-pass cleans it)
|
|
1833
2673
|
if (op === 'nop') return ['nop']
|
|
1834
2674
|
|
|
1835
|
-
// (drop
|
|
1836
|
-
|
|
1837
|
-
|
|
2675
|
+
// (drop V) → just V's side effects. Pure V vanishes (→ nop); a pure op over
|
|
2676
|
+
// a `local.tee` collapses to the bare store (kills the post-increment's dead
|
|
2677
|
+
// old-value arithmetic); impure V is kept under a drop.
|
|
2678
|
+
if (op === 'drop' && node.length === 2) {
|
|
2679
|
+
const eff = dropEffects(node[1])
|
|
2680
|
+
if (eff.length === 0) return ['nop']
|
|
2681
|
+
if (eff.length === 1) return eff[0]
|
|
2682
|
+
return ['block', ...eff]
|
|
1838
2683
|
}
|
|
1839
2684
|
|
|
1840
|
-
// (select x x cond) → x
|
|
1841
|
-
|
|
2685
|
+
// (select x x cond) → x — only when cond is PURE. An impure cond may set a
|
|
2686
|
+
// local that a later op reads (e.g. an address `local.tee` the matching store
|
|
2687
|
+
// reuses); dropping it would leave that local stale. Keep the select otherwise.
|
|
2688
|
+
if (op === 'select' && node.length >= 4 && equal(node[1], node[2]) && isPure(node[3])) return node[1]
|
|
1842
2689
|
|
|
1843
2690
|
if (op === 'if') {
|
|
1844
2691
|
const { cond, thenBranch, elseBranch } = parseIf(node)
|
|
@@ -1855,18 +2702,25 @@ const vacuum = (ast) => {
|
|
|
1855
2702
|
}
|
|
1856
2703
|
|
|
1857
2704
|
// Clean out nops, drop-of-pure sequences, and empty annotations from blocks
|
|
1858
|
-
if (
|
|
2705
|
+
if (isScopeNode(node)) {
|
|
1859
2706
|
const cleaned = [op]
|
|
1860
2707
|
for (let i = 1; i < node.length; i++) {
|
|
1861
2708
|
const child = node[i]
|
|
1862
2709
|
if (child === 'nop' || (Array.isArray(child) && child[0] === 'nop')) continue
|
|
1863
|
-
//
|
|
2710
|
+
// Stack-form `EXPR drop`: a pure EXPR drops out entirely; a bare
|
|
2711
|
+
// `tee X V drop` keeps just the store (`set X V`) — the dropped value
|
|
2712
|
+
// was the only reason it was a tee.
|
|
1864
2713
|
const next = node[i + 1]
|
|
1865
2714
|
const isDrop = next === 'drop' || (Array.isArray(next) && next[0] === 'drop' && next.length === 1)
|
|
1866
|
-
if (Array.isArray(child) && isPure(child)
|
|
2715
|
+
if (Array.isArray(child) && isDrop && isPure(child)) {
|
|
1867
2716
|
i++ // skip the drop too
|
|
1868
2717
|
continue
|
|
1869
2718
|
}
|
|
2719
|
+
if (Array.isArray(child) && isDrop && child[0] === 'local.tee' && child.length === 3) {
|
|
2720
|
+
cleaned.push(['local.set', child[1], child[2]])
|
|
2721
|
+
i++ // skip the drop
|
|
2722
|
+
continue
|
|
2723
|
+
}
|
|
1870
2724
|
cleaned.push(child)
|
|
1871
2725
|
}
|
|
1872
2726
|
if (cleaned.length !== node.length) return cleaned
|
|
@@ -1876,67 +2730,74 @@ const vacuum = (ast) => {
|
|
|
1876
2730
|
|
|
1877
2731
|
// ==================== PEEPHOLE ====================
|
|
1878
2732
|
|
|
1879
|
-
/** Peephole optimizations: simple algebraic identities
|
|
2733
|
+
/** Peephole optimizations: simple algebraic identities.
|
|
2734
|
+
* Every rule that DROPS an operand guards on isPure: an impure operand must still
|
|
2735
|
+
* be evaluated for its side effects. The load-bearing case is a typed-array element
|
|
2736
|
+
* store, whose address is a `local.tee` inside the value expression (the element's
|
|
2737
|
+
* own read); dropping that operand (e.g. `(a[i] op a[i]) & 0`) would strand the
|
|
2738
|
+
* store with a stale address — a silent miscompile. When impure, keep the op (it
|
|
2739
|
+
* still yields the same value AND runs the operand). */
|
|
2740
|
+
const selfFold = (val) => (a, b) => equal(a, b) && isPure(a) ? val : null
|
|
1880
2741
|
const PEEPHOLE = {
|
|
1881
|
-
// Self-cancelling / tautological binary ops
|
|
1882
|
-
'i32.sub': (
|
|
1883
|
-
'i64.sub': (
|
|
1884
|
-
'i32.xor': (
|
|
1885
|
-
'i64.xor': (
|
|
1886
|
-
'i32.eq': (
|
|
1887
|
-
'i64.eq': (
|
|
1888
|
-
'i32.ne': (
|
|
1889
|
-
'i64.ne': (
|
|
1890
|
-
'i32.lt_s': (
|
|
1891
|
-
'i32.lt_u': (
|
|
1892
|
-
'i32.gt_s': (
|
|
1893
|
-
'i32.gt_u': (
|
|
1894
|
-
'i32.le_s': (
|
|
1895
|
-
'i32.le_u': (
|
|
1896
|
-
'i32.ge_s': (
|
|
1897
|
-
'i32.ge_u': (
|
|
1898
|
-
'i64.lt_s': (
|
|
1899
|
-
'i64.lt_u': (
|
|
1900
|
-
'i64.gt_s': (
|
|
1901
|
-
'i64.gt_u': (
|
|
1902
|
-
'i64.le_s': (
|
|
1903
|
-
'i64.le_u': (
|
|
1904
|
-
'i64.ge_s': (
|
|
1905
|
-
'i64.ge_u': (
|
|
1906
|
-
|
|
1907
|
-
// Zero/all-bits absorption
|
|
2742
|
+
// Self-cancelling / tautological binary ops — drop both (equal) operands.
|
|
2743
|
+
'i32.sub': selfFold(['i32.const', 0]),
|
|
2744
|
+
'i64.sub': selfFold(['i64.const', 0]),
|
|
2745
|
+
'i32.xor': selfFold(['i32.const', 0]),
|
|
2746
|
+
'i64.xor': selfFold(['i64.const', 0]),
|
|
2747
|
+
'i32.eq': selfFold(['i32.const', 1]),
|
|
2748
|
+
'i64.eq': selfFold(['i32.const', 1]),
|
|
2749
|
+
'i32.ne': selfFold(['i32.const', 0]),
|
|
2750
|
+
'i64.ne': selfFold(['i32.const', 0]),
|
|
2751
|
+
'i32.lt_s': selfFold(['i32.const', 0]),
|
|
2752
|
+
'i32.lt_u': selfFold(['i32.const', 0]),
|
|
2753
|
+
'i32.gt_s': selfFold(['i32.const', 0]),
|
|
2754
|
+
'i32.gt_u': selfFold(['i32.const', 0]),
|
|
2755
|
+
'i32.le_s': selfFold(['i32.const', 1]),
|
|
2756
|
+
'i32.le_u': selfFold(['i32.const', 1]),
|
|
2757
|
+
'i32.ge_s': selfFold(['i32.const', 1]),
|
|
2758
|
+
'i32.ge_u': selfFold(['i32.const', 1]),
|
|
2759
|
+
'i64.lt_s': selfFold(['i32.const', 0]),
|
|
2760
|
+
'i64.lt_u': selfFold(['i32.const', 0]),
|
|
2761
|
+
'i64.gt_s': selfFold(['i32.const', 0]),
|
|
2762
|
+
'i64.gt_u': selfFold(['i32.const', 0]),
|
|
2763
|
+
'i64.le_s': selfFold(['i32.const', 1]),
|
|
2764
|
+
'i64.le_u': selfFold(['i32.const', 1]),
|
|
2765
|
+
'i64.ge_s': selfFold(['i32.const', 1]),
|
|
2766
|
+
'i64.ge_u': selfFold(['i32.const', 1]),
|
|
2767
|
+
|
|
2768
|
+
// Zero/all-bits absorption — drops the NON-const operand, so guard its purity.
|
|
1908
2769
|
'i32.mul': (a, b) => {
|
|
1909
|
-
|
|
1910
|
-
if (
|
|
2770
|
+
if (getConst(b)?.value === 0 && isPure(a)) return ['i32.const', 0]
|
|
2771
|
+
if (getConst(a)?.value === 0 && isPure(b)) return ['i32.const', 0]
|
|
1911
2772
|
return null
|
|
1912
2773
|
},
|
|
1913
2774
|
'i64.mul': (a, b) => {
|
|
1914
|
-
|
|
1915
|
-
if (
|
|
2775
|
+
if (getConst(b)?.value === ZERO64 && isPure(a)) return ['i64.const', 0]
|
|
2776
|
+
if (getConst(a)?.value === ZERO64 && isPure(b)) return ['i64.const', 0]
|
|
1916
2777
|
return null
|
|
1917
2778
|
},
|
|
1918
2779
|
'i32.and': (a, b) => {
|
|
1919
|
-
if (equal(a, b)) return a
|
|
1920
|
-
|
|
1921
|
-
if (
|
|
2780
|
+
if (equal(a, b) && isPure(b)) return a
|
|
2781
|
+
if (getConst(b)?.value === 0 && isPure(a)) return ['i32.const', 0]
|
|
2782
|
+
if (getConst(a)?.value === 0 && isPure(b)) return ['i32.const', 0]
|
|
1922
2783
|
return null
|
|
1923
2784
|
},
|
|
1924
2785
|
'i64.and': (a, b) => {
|
|
1925
|
-
if (equal(a, b)) return a
|
|
1926
|
-
|
|
1927
|
-
if (
|
|
2786
|
+
if (equal(a, b) && isPure(b)) return a
|
|
2787
|
+
if (getConst(b)?.value === ZERO64 && isPure(a)) return ['i64.const', 0]
|
|
2788
|
+
if (getConst(a)?.value === ZERO64 && isPure(b)) return ['i64.const', 0]
|
|
1928
2789
|
return null
|
|
1929
2790
|
},
|
|
1930
2791
|
'i32.or': (a, b) => {
|
|
1931
|
-
if (equal(a, b)) return a
|
|
1932
|
-
|
|
1933
|
-
if (
|
|
2792
|
+
if (equal(a, b) && isPure(b)) return a
|
|
2793
|
+
if (getConst(b)?.value === -1 && isPure(a)) return ['i32.const', -1]
|
|
2794
|
+
if (getConst(a)?.value === -1 && isPure(b)) return ['i32.const', -1]
|
|
1934
2795
|
return null
|
|
1935
2796
|
},
|
|
1936
2797
|
'i64.or': (a, b) => {
|
|
1937
|
-
if (equal(a, b)) return a
|
|
1938
|
-
|
|
1939
|
-
if (
|
|
2798
|
+
if (equal(a, b) && isPure(b)) return a
|
|
2799
|
+
if (getConst(b)?.value === NEG164 && isPure(a)) return ['i64.const', -1]
|
|
2800
|
+
if (getConst(a)?.value === NEG164 && isPure(b)) return ['i64.const', -1]
|
|
1940
2801
|
return null
|
|
1941
2802
|
},
|
|
1942
2803
|
|
|
@@ -1963,7 +2824,12 @@ const peephole = (ast) => {
|
|
|
1963
2824
|
|
|
1964
2825
|
/** Bytes a signed-LEB128 integer encodes to. */
|
|
1965
2826
|
const slebSize = (v) => {
|
|
1966
|
-
let x = typeof v === 'bigint' ? v
|
|
2827
|
+
let x = typeof v === 'bigint' ? v
|
|
2828
|
+
: typeof v === 'string' ? BigInt(v.replaceAll('_', ''))
|
|
2829
|
+
: BigInt(Math.trunc(Number(v) || 0))
|
|
2830
|
+
// Signed view of raw bits — exact natively; in-kernel the arm is dead
|
|
2831
|
+
// (BigInt('0x…') already arrives as the signed i64 carrier there).
|
|
2832
|
+
if (x > 0x7fffffffffffffffn) x = x - 0x8000000000000000n - 0x8000000000000000n
|
|
1967
2833
|
let n = 1
|
|
1968
2834
|
while (true) {
|
|
1969
2835
|
const b = x & 0x7fn
|
|
@@ -2433,7 +3299,7 @@ const dedupe = (ast) => {
|
|
|
2433
3299
|
walk(node, (n) => {
|
|
2434
3300
|
if (!Array.isArray(n) || typeof n[1] !== 'string' || n[1][0] !== '$') return
|
|
2435
3301
|
const op = n[0]
|
|
2436
|
-
if (op === 'param' || op === 'local' || op
|
|
3302
|
+
if (op === 'param' || op === 'local' || isBranchScope(op)) {
|
|
2437
3303
|
localNames.add(n[1])
|
|
2438
3304
|
}
|
|
2439
3305
|
})
|
|
@@ -2561,53 +3427,62 @@ const dedupTypes = (ast) => {
|
|
|
2561
3427
|
|
|
2562
3428
|
// ==================== DATA SEGMENT PACKING ====================
|
|
2563
3429
|
|
|
2564
|
-
/** Parse a WAT data string literal into
|
|
3430
|
+
/** Parse a WAT data string literal into a plain byte array. Plain arrays —
|
|
3431
|
+
* not Uint8Array — throughout the data codecs: typed-array views/methods have
|
|
3432
|
+
* spotty native lowerings (subarray dispatches to the HOST, in-situ variable-
|
|
3433
|
+
* index reads misread in the kernel), while plain number arrays are the
|
|
3434
|
+
* optimizer's lingua franca and proven kernel-faithful. */
|
|
2565
3435
|
const parseDataString = (str) => {
|
|
2566
|
-
if (typeof str !== 'string' || str.length < 2 || str[0] !== '"') return
|
|
2567
|
-
const inner = str.slice(1, -1)
|
|
3436
|
+
if (typeof str !== 'string' || str.length < 2 || str[0] !== '"') return []
|
|
2568
3437
|
const bytes = []
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
else if (next === 'r') bytes.push(13)
|
|
2581
|
-
else if (next === '\\') bytes.push(92)
|
|
2582
|
-
else if (next === '"') bytes.push(34)
|
|
2583
|
-
else bytes.push(next.charCodeAt(0))
|
|
3438
|
+
// Hex digit value by char code, −1 for non-hex — pure number math (no
|
|
3439
|
+
// regex/slice/parseInt on string views; see contract note above).
|
|
3440
|
+
const hexv = (c) => c >= 48 && c <= 57 ? c - 48 : c >= 97 && c <= 102 ? c - 87 : c >= 65 && c <= 70 ? c - 55 : -1
|
|
3441
|
+
const end = str.length - 1 // skip surrounding quotes
|
|
3442
|
+
for (let i = 1; i < end; i++) {
|
|
3443
|
+
const c = str.charCodeAt(i)
|
|
3444
|
+
if (c !== 92) { bytes.push(c); continue }
|
|
3445
|
+
const n = str.charCodeAt(++i)
|
|
3446
|
+
if (n === 120 || n === 88) { // \xHH
|
|
3447
|
+
bytes.push((hexv(str.charCodeAt(i + 1)) << 4) | hexv(str.charCodeAt(i + 2)))
|
|
3448
|
+
i += 2
|
|
2584
3449
|
} else {
|
|
2585
|
-
|
|
3450
|
+
const h1 = hexv(n), h2 = i + 1 < end ? hexv(str.charCodeAt(i + 1)) : -1
|
|
3451
|
+
if (h1 >= 0 && h2 >= 0) { bytes.push((h1 << 4) | h2); i++ }
|
|
3452
|
+
else if (n === 110) bytes.push(10) // \n
|
|
3453
|
+
else if (n === 116) bytes.push(9) // \t
|
|
3454
|
+
else if (n === 114) bytes.push(13) // \r
|
|
3455
|
+
else bytes.push(n) // \\ \" and any other escaped char
|
|
2586
3456
|
}
|
|
2587
3457
|
}
|
|
2588
|
-
return
|
|
3458
|
+
return bytes
|
|
2589
3459
|
}
|
|
2590
3460
|
|
|
2591
|
-
/** Encode
|
|
2592
|
-
|
|
3461
|
+
/** Encode a plain byte array as a WAT data string literal; `end` bounds the
|
|
3462
|
+
* bytes (always passed explicitly — see parseDataString's contract note).
|
|
3463
|
+
* (`b` comes from a plain-array element read, i.e. an untyped receiver — the
|
|
3464
|
+
* `.toString(16)` here is exactly the dispatch the tryRuntimeNumberMethod /
|
|
3465
|
+
* runtime-string-fork number arm exists for; it used to yield `undefined`
|
|
3466
|
+
* in-kernel and zeroed every escaped byte of the emitted data segment.) */
|
|
3467
|
+
const encodeDataString = (bytes, end) => {
|
|
2593
3468
|
let str = '"'
|
|
2594
|
-
for (let i = 0; i <
|
|
3469
|
+
for (let i = 0; i < end; i++) {
|
|
2595
3470
|
const b = bytes[i]
|
|
2596
|
-
if (b >= 32 && b < 127 && b !== 34 && b !== 92)
|
|
2597
|
-
|
|
2598
|
-
} else {
|
|
2599
|
-
str += '\\' + b.toString(16).padStart(2, '0')
|
|
2600
|
-
}
|
|
3471
|
+
if (b >= 32 && b < 127 && b !== 34 && b !== 92) str += String.fromCharCode(b)
|
|
3472
|
+
else str += '\\' + b.toString(16).padStart(2, '0')
|
|
2601
3473
|
}
|
|
2602
3474
|
return str + '"'
|
|
2603
3475
|
}
|
|
2604
3476
|
|
|
2605
|
-
/** Trim trailing zeros from data content items
|
|
3477
|
+
/** Trim trailing zeros from data content items. Per-byte pushes (never
|
|
3478
|
+
* push(...spread) — a segment can be hundreds of KB and spreading overflows
|
|
3479
|
+
* V8's argument stack; never Uint8Array — see parseDataString's note). */
|
|
2606
3480
|
const trimTrailingZeros = (items) => {
|
|
2607
3481
|
const bytes = []
|
|
2608
3482
|
for (const item of items) {
|
|
2609
3483
|
if (typeof item === 'string') {
|
|
2610
|
-
|
|
3484
|
+
const chunk = parseDataString(item)
|
|
3485
|
+
for (let i = 0; i < chunk.length; i++) bytes.push(chunk[i])
|
|
2611
3486
|
} else if (Array.isArray(item) && item[0] === 'i8') {
|
|
2612
3487
|
for (let i = 1; i < item.length; i++) bytes.push(Number(item[i]) & 0xff)
|
|
2613
3488
|
} else {
|
|
@@ -2618,7 +3493,7 @@ const trimTrailingZeros = (items) => {
|
|
|
2618
3493
|
while (end > 0 && bytes[end - 1] === 0) end--
|
|
2619
3494
|
if (end === bytes.length) return items
|
|
2620
3495
|
if (end === 0) return []
|
|
2621
|
-
return [encodeDataString(
|
|
3496
|
+
return [encodeDataString(bytes, end)]
|
|
2622
3497
|
}
|
|
2623
3498
|
|
|
2624
3499
|
/** Extract { memidx, offset } from an active data segment with constant offset */
|
|
@@ -2676,11 +3551,9 @@ const mergeDataSegments = (a, b) => {
|
|
|
2676
3551
|
typeof aContent[0] === 'string' && typeof bContent[0] === 'string') {
|
|
2677
3552
|
const aBytes = parseDataString(aContent[0])
|
|
2678
3553
|
const bBytes = parseDataString(bContent[0])
|
|
2679
|
-
|
|
2680
|
-
merged.set(aBytes)
|
|
2681
|
-
merged.set(bBytes, aBytes.length)
|
|
3554
|
+
for (let i = 0; i < bBytes.length; i++) aBytes.push(bBytes[i])
|
|
2682
3555
|
a.length = aIdx
|
|
2683
|
-
a.push(encodeDataString(
|
|
3556
|
+
a.push(encodeDataString(aBytes, aBytes.length))
|
|
2684
3557
|
return true
|
|
2685
3558
|
}
|
|
2686
3559
|
|
|
@@ -2865,12 +3738,14 @@ const reorder = (ast) => {
|
|
|
2865
3738
|
const PASSES = [
|
|
2866
3739
|
['stripmut', stripmut, true, 'strip mut from never-written globals'],
|
|
2867
3740
|
['globals', globals, true, 'propagate immutable global constants'],
|
|
3741
|
+
['guardRefine', guardRefine, false, 'fold NaN-box tag reads under dominating tag guards (jz NaN-box-specific; opt-in)'],
|
|
2868
3742
|
['fold', fold, true, 'constant folding'],
|
|
2869
3743
|
['identity', identity, true, 'remove identity ops (x + 0 → x)'],
|
|
2870
3744
|
['peephole', peephole, true, 'x-x→0, x&0→0, etc.'],
|
|
2871
3745
|
['strength', strength, true, 'strength reduction (x * 2 → x << 1)'],
|
|
2872
3746
|
['branch', branch, true, 'simplify constant branches'],
|
|
2873
3747
|
['propagate', propagate, true, 'forward-propagate single-use locals & tiny consts (never inflates)'],
|
|
3748
|
+
['devirt', devirt, false, 'call_indirect with a constant or known closure-const index → direct/guarded calls — grows bytes for speed'],
|
|
2874
3749
|
['inlineOnce', inlineOnce, true, 'inline single-call functions into their lone caller (never duplicates)'],
|
|
2875
3750
|
['inline', inline, false, 'inline tiny functions — can duplicate bodies'],
|
|
2876
3751
|
['offset', offset, true, 'fold add+const into load/store offset'],
|
|
@@ -2928,67 +3803,155 @@ const normalize = (opts) => {
|
|
|
2928
3803
|
* optimize(ast, 'treeshake') // only treeshake
|
|
2929
3804
|
* optimize(ast, { fold: true }) // explicit
|
|
2930
3805
|
*/
|
|
3806
|
+
/**
|
|
3807
|
+
* Could `inlineOnce`/`inline` grow the binary on this module? They are the only
|
|
3808
|
+
* size-*increasing* passes: splicing a callee body plus its `block`/param-setup
|
|
3809
|
+
* wrapper can exceed the `call` it removes. Every other pass strictly shrinks or
|
|
3810
|
+
* holds. So if no function is even a candidate (called exactly once, not pinned
|
|
3811
|
+
* by export/start/elem/ref.func, not its own exporter), nothing can inflate and
|
|
3812
|
+
* the size guard is dead weight. Cheap over-approximation of inlineOnce's own
|
|
3813
|
+
* gating — a false positive only costs the guarded path (correct, just slower).
|
|
3814
|
+
*/
|
|
3815
|
+
const mayInline = (ast) => {
|
|
3816
|
+
if (!Array.isArray(ast)) return false
|
|
3817
|
+
const callRefs = new Map(), pinned = new Set(), other = new Set()
|
|
3818
|
+
const scan = (n) => {
|
|
3819
|
+
if (!Array.isArray(n)) return
|
|
3820
|
+
const op = n[0]
|
|
3821
|
+
if (op === 'call' && typeof n[1] === 'string') callRefs.set(n[1], (callRefs.get(n[1]) || 0) + 1)
|
|
3822
|
+
else if (op === 'return_call' && typeof n[1] === 'string') other.add(n[1])
|
|
3823
|
+
else if (op === 'ref.func' && typeof n[1] === 'string') pinned.add(n[1])
|
|
3824
|
+
else if (op === 'export' && Array.isArray(n[2]) && n[2][0] === 'func' && typeof n[2][1] === 'string') pinned.add(n[2][1])
|
|
3825
|
+
else if (op === 'start' && typeof n[1] === 'string') pinned.add(n[1])
|
|
3826
|
+
else if (op === 'elem') for (const c of n) if (typeof c === 'string' && c[0] === '$') pinned.add(c)
|
|
3827
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
3828
|
+
}
|
|
3829
|
+
scan(ast)
|
|
3830
|
+
for (const n of ast) {
|
|
3831
|
+
if (!Array.isArray(n) || n[0] !== 'func' || typeof n[1] !== 'string') continue
|
|
3832
|
+
const name = n[1]
|
|
3833
|
+
if (callRefs.get(name) !== 1 || pinned.has(name) || other.has(name)) continue
|
|
3834
|
+
if (n.some(c => Array.isArray(c) && c[0] === 'export')) continue // self-exporting func
|
|
3835
|
+
return true
|
|
3836
|
+
}
|
|
3837
|
+
return false
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// A `__start` whose body DCE emptied down to nothing — plus its `(start)`
|
|
3841
|
+
// directive — is pure noise: the directive invokes a no-op. jz's buildStartFn
|
|
3842
|
+
// only emits `__start` when it has content, so an empty one is always a post-DCE
|
|
3843
|
+
// artifact (e.g. a top-level `1 + 2;` whose dropped value the dead-code pass
|
|
3844
|
+
// removed). Drop both. Header nodes (param/result/local/export/type) don't count
|
|
3845
|
+
// as a body — a function carrying only locals still does nothing.
|
|
3846
|
+
const START_HEAD = new Set(['export', 'type', 'param', 'result', 'local'])
|
|
3847
|
+
function pruneEmptyStart(ast) {
|
|
3848
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
3849
|
+
const fn = ast.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
3850
|
+
if (!fn) return ast
|
|
3851
|
+
// Skip the name (index 1) and header nodes; a bare-string node past them is a
|
|
3852
|
+
// real instruction (`drop`/`nop`/`unreachable`), so it stops the scan.
|
|
3853
|
+
let b = 2
|
|
3854
|
+
while (b < fn.length && Array.isArray(fn[b]) && START_HEAD.has(fn[b][0])) b++
|
|
3855
|
+
if (b < fn.length) return ast // real instructions remain
|
|
3856
|
+
return ast.filter(n => !(Array.isArray(n) &&
|
|
3857
|
+
((n[0] === 'func' && n[1] === '$__start') || (n[0] === 'start' && n[1] === '$__start'))))
|
|
3858
|
+
}
|
|
3859
|
+
|
|
2931
3860
|
export default function optimize(ast, opts = true) {
|
|
2932
|
-
if (typeof ast === 'string') ast = parse(ast)
|
|
3861
|
+
if (typeof ast === 'string') ast = parse(ast) // accept WAT source directly
|
|
2933
3862
|
const strictGuard = opts === true // default: zero tolerance for bloat
|
|
2934
3863
|
opts = normalize(opts)
|
|
3864
|
+
// `pin`: caller-supplied function names that inlineOnce/inline must NOT dissolve. Keeps
|
|
3865
|
+
// optimizer policy with the CALLER — e.g. jz pins the scalar transcendentals its own
|
|
3866
|
+
// auto-vectorizer later rewrites to f64x2 mirrors, so no consumer-specific names live here.
|
|
3867
|
+
opts.pin = opts.pin instanceof Set ? opts.pin : new Set(opts.pin || [])
|
|
2935
3868
|
|
|
2936
3869
|
const log = opts.log ? (msg, delta) => opts.log(msg, delta) : () => {}
|
|
2937
3870
|
const verbose = opts.verbose || opts.log
|
|
2938
3871
|
|
|
2939
3872
|
ast = clone(ast)
|
|
2940
|
-
let beforeRound = null
|
|
2941
3873
|
|
|
2942
|
-
//
|
|
2943
|
-
//
|
|
2944
|
-
//
|
|
2945
|
-
//
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
//
|
|
2949
|
-
//
|
|
2950
|
-
//
|
|
2951
|
-
//
|
|
2952
|
-
//
|
|
3874
|
+
// devirt trades bytes for speed by design (guards + duplicated args), so it
|
|
3875
|
+
// runs ONCE after the rounds — its candidate shape (select of two i64 closure
|
|
3876
|
+
// constants) only emerges from fold/propagate, and its intended growth must
|
|
3877
|
+
// not trip the size-guard into reverting a whole round. A single sweep is
|
|
3878
|
+
// complete: every call_indirect is visited; rewritten sites keep the original
|
|
3879
|
+
// as the guarded fallback arm.
|
|
3880
|
+
// `inline` (multi-caller, size-for-speed) is like `devirt`: it INTENTIONALLY grows
|
|
3881
|
+
// the binary, so it must run OUTSIDE the per-round size-revert guard below (which
|
|
3882
|
+
// would otherwise undo it). Run it once after the rounds converge, then tidy the
|
|
3883
|
+
// (block (local.set $p arg) … body) wrappers it leaves with the same cleanup passes
|
|
3884
|
+
// a normal round would. opt-in (speed level); a no-op when no small callee qualifies.
|
|
3885
|
+
const runInline = (a) => {
|
|
3886
|
+
if (!opts.inline) return a
|
|
3887
|
+
// `inline: 'simd'` → SIMD-helper-only (jz's speed tier, avoids general bloat);
|
|
3888
|
+
// `inline: true` / `'all'` → general inlining of tiny functions.
|
|
3889
|
+
a = inline(a, { simdOnly: opts.inline === 'simd', pin: opts.pin })
|
|
3890
|
+
if (opts.propagate) a = propagate(a)
|
|
3891
|
+
if (opts.mergeBlocks) a = mergeBlocks(a)
|
|
3892
|
+
if (opts.vacuum) a = vacuum(a)
|
|
3893
|
+
if (opts.coalesceLocals) a = coalesceLocals(a)
|
|
3894
|
+
return a
|
|
3895
|
+
}
|
|
3896
|
+
const finish = (a) => { a = runInline(a); return pruneEmptyStart(opts.devirt ? devirt(a) : a) }
|
|
3897
|
+
|
|
3898
|
+
// Fast path: jz owns this optimizer and feeds it a controlled, type-aware IR.
|
|
3899
|
+
// The only passes that can *grow* the binary are inlineOnce/inline; when no
|
|
3900
|
+
// function is an inline candidate (the common case for scalar REPL kernels)
|
|
3901
|
+
// nothing can inflate, so we skip watr's per-round `binarySize` re-compile
|
|
3902
|
+
// guard — up to four full encodes per call — and iterate to a fixpoint with
|
|
3903
|
+
// zero compiles. A round that changes nothing is the natural exit.
|
|
3904
|
+
if (!((opts.inlineOnce || opts.inline) && mayInline(ast))) {
|
|
3905
|
+
// inlineOnce/inline can't fire here, so skip them — their candidate scan
|
|
3906
|
+
// (a 16-round whole-module walk) is the second-costliest thing after
|
|
3907
|
+
// propagate, and it would only confirm what `mayInline` already proved.
|
|
3908
|
+
for (let round = 0; round < 3; round++) {
|
|
3909
|
+
const beforeRound = clone(ast)
|
|
3910
|
+
for (const [key, fn] of PASSES) if (opts[key] && key !== 'inlineOnce' && key !== 'inline' && key !== 'devirt') ast = fn(ast, opts)
|
|
3911
|
+
if (equal(beforeRound, ast)) break // fixpoint
|
|
3912
|
+
if (verbose) log(` round ${round + 1} applied`)
|
|
3913
|
+
}
|
|
3914
|
+
return finish(ast)
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// Guarded path: inlining can inflate (a body bigger than the call it replaces),
|
|
3918
|
+
// so score each round on encoded bytes and revert any that grows the binary.
|
|
3919
|
+
// `binarySize` returns Infinity for invalid wat, so a broken round reverts too.
|
|
3920
|
+
// A round's starting size equals the prior round's ending size, so carry it
|
|
3921
|
+
// forward and compile once per round.
|
|
3922
|
+
let beforeRound = null
|
|
2953
3923
|
let sizeBefore = binarySize(ast)
|
|
2954
3924
|
for (let round = 0; round < 3; round++) {
|
|
2955
3925
|
beforeRound = clone(ast)
|
|
2956
3926
|
|
|
2957
|
-
for (const [key, fn] of PASSES) if (opts[key]) ast = fn(ast)
|
|
3927
|
+
for (const [key, fn] of PASSES) if (opts[key] && key !== 'devirt' && key !== 'inline') ast = fn(ast, opts)
|
|
2958
3928
|
// Second propagate sweep: `inlineOnce`/`inline` (above) leave fresh
|
|
2959
3929
|
// `(local.set $p arg) … (local.get $p)` wrappers around each inlined call;
|
|
2960
3930
|
// re-running propagation collapses them within this same round, so the size
|
|
2961
|
-
// guard scores the cleaned result
|
|
2962
|
-
// never get if `equal()` declares a fixpoint first).
|
|
3931
|
+
// guard scores the cleaned result.
|
|
2963
3932
|
if (opts.propagate && (opts.inlineOnce || opts.inline)) ast = propagate(ast)
|
|
2964
3933
|
|
|
2965
|
-
// A round that changed nothing can't have inflated
|
|
2966
|
-
//
|
|
2967
|
-
// (the common exit) then costs zero compiles instead of one.
|
|
3934
|
+
// A round that changed nothing can't have inflated — check convergence
|
|
3935
|
+
// before compiling so the fixpoint-confirming round costs zero compiles.
|
|
2968
3936
|
if (equal(beforeRound, ast)) break
|
|
2969
3937
|
|
|
2970
3938
|
const sizeAfter = binarySize(ast)
|
|
2971
3939
|
const delta = sizeAfter - sizeBefore
|
|
3940
|
+
if (verbose || delta !== 0) log(` round ${round + 1}: ${delta > 0 ? '+' : ''}${delta} bytes`, delta)
|
|
2972
3941
|
|
|
2973
|
-
|
|
2974
|
-
log(` round ${round + 1}: ${delta > 0 ? '+' : ''}${delta} bytes`, delta)
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
// Size guard: default optimize must never inflate. Explicit passes get a
|
|
2978
|
-
// little leniency (a round may grow a few bytes setting up a bigger win).
|
|
3942
|
+
// Default optimize must never inflate; explicit passes get slight leniency.
|
|
2979
3943
|
const tolerance = strictGuard ? 0 : 16
|
|
2980
3944
|
if (delta > tolerance) {
|
|
2981
3945
|
if (verbose) log(` ⚠ round ${round + 1} inflated by ${delta} bytes, reverting`, delta)
|
|
2982
3946
|
ast = beforeRound
|
|
2983
3947
|
break
|
|
2984
3948
|
}
|
|
2985
|
-
|
|
2986
|
-
sizeBefore = sizeAfter // this round's result is next round's baseline
|
|
3949
|
+
sizeBefore = sizeAfter
|
|
2987
3950
|
}
|
|
2988
3951
|
|
|
2989
|
-
return ast
|
|
3952
|
+
return finish(ast)
|
|
2990
3953
|
}
|
|
2991
3954
|
|
|
2992
3955
|
/** Count AST nodes (fast size heuristic). */
|
|
2993
3956
|
export { count as size, count, binarySize }
|
|
2994
|
-
export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, inlineOnce, normalize, OPTS, vacuum, peephole, globals, offset, unbranch, loopify, stripmut, brif, foldarms, dedupe, reorder, dedupTypes, packData, minifyImports, mergeBlocks, coalesceLocals }
|
|
3957
|
+
export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, inlineOnce, devirt, normalize, OPTS, vacuum, peephole, globals, offset, unbranch, loopify, stripmut, brif, foldarms, dedupe, reorder, dedupTypes, packData, minifyImports, mergeBlocks, coalesceLocals }
|