jz 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
package/src/optimize.js
CHANGED
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
* @module optimize
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { LAYOUT } from './ctx.js'
|
|
31
|
+
import { vectorizeLaneLocal } from './vectorize.js'
|
|
32
|
+
|
|
30
33
|
const MEMOP = /^[fi](32|64)\.(load|store)(\d+(_[su])?)?$/
|
|
31
|
-
const
|
|
34
|
+
const NAN_BITS = '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
|
|
35
|
+
const NULL_BITS = '0x' + (LAYOUT.NAN_PREFIX_BITS | (1n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
36
|
+
const UNDEF_BITS = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* Optimization passes, partitioned by phase. The `level` presets pick which
|
|
@@ -39,23 +44,30 @@ const NAN_PREFIX_BITS = 0x7FF8n
|
|
|
39
44
|
* 0 — nothing. Fastest compile, largest output. Useful for live coding.
|
|
40
45
|
* 1 — encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
|
|
41
46
|
* Cheap, no IR rewrites that perturb V8's tier-up shape.
|
|
42
|
-
* 2 — default.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
47
|
+
* 2 — default. Stable passes (watr CSE/DCE/inline + every jz pass that has
|
|
48
|
+
* benchmark proof).
|
|
49
|
+
* 3 — aggressive experimental passes in addition to level 2.
|
|
45
50
|
*/
|
|
46
51
|
export const PASS_NAMES = [
|
|
47
52
|
'watr', // third-party WAT-level CSE/DCE/inlining (heaviest)
|
|
48
53
|
'hoistPtrType',
|
|
49
54
|
'hoistInvariantPtrOffset',
|
|
55
|
+
'hoistInvariantPtrOffsetLoop',
|
|
50
56
|
'fusedRewrite', // peephole + ptr-helper inline + memarg fold
|
|
51
57
|
'hoistAddrBase',
|
|
52
58
|
'hoistInvariantCellLoads',
|
|
59
|
+
'cseScalarLoad',
|
|
60
|
+
'csePureExpr',
|
|
53
61
|
'sortLocalsByUse',
|
|
54
62
|
'specializeMkptr',
|
|
55
63
|
'specializePtrBase',
|
|
56
64
|
'sortStrPoolByFreq',
|
|
57
65
|
'hoistConstantPool',
|
|
66
|
+
'sourceInline',
|
|
58
67
|
'smallConstForUnroll',
|
|
68
|
+
'nestedSmallConstForUnroll',
|
|
69
|
+
'vectorizeLaneLocal', // SIMD-128 lift for lane-pure typed-array loops
|
|
70
|
+
'arenaRewind', // per-call heap rewind for no-arg scalar allocator kernels
|
|
59
71
|
'treeshake',
|
|
60
72
|
]
|
|
61
73
|
|
|
@@ -64,14 +76,14 @@ const ALL_OFF = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, false])
|
|
|
64
76
|
const LEVEL_PRESETS = Object.freeze({
|
|
65
77
|
0: ALL_OFF,
|
|
66
78
|
1: Object.freeze({ ...ALL_OFF, treeshake: true, sortLocalsByUse: true, fusedRewrite: true }),
|
|
67
|
-
2: ALL_ON,
|
|
79
|
+
2: Object.freeze({ ...ALL_ON, nestedSmallConstForUnroll: 'auto' }),
|
|
68
80
|
3: ALL_ON,
|
|
69
81
|
})
|
|
70
82
|
|
|
71
83
|
/**
|
|
72
84
|
* Normalize the user's `opts.optimize` value into a flat config object.
|
|
73
85
|
*
|
|
74
|
-
* resolveOptimize(undefined | true) →
|
|
86
|
+
* resolveOptimize(undefined | true) → level 2 stable defaults
|
|
75
87
|
* resolveOptimize(false | 0) → all off
|
|
76
88
|
* resolveOptimize(1 | 2 | 3) → preset for that level
|
|
77
89
|
* resolveOptimize({ level: 1, watr: true }) → level 1 base, with watr forced on
|
|
@@ -79,13 +91,13 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
79
91
|
*/
|
|
80
92
|
export function resolveOptimize(opt) {
|
|
81
93
|
if (opt === false || opt === 0) return { ...ALL_OFF }
|
|
82
|
-
if (opt === true || opt == null) return { ...
|
|
94
|
+
if (opt === true || opt == null) return { ...LEVEL_PRESETS[2] }
|
|
83
95
|
if (typeof opt === 'number') return { ...(LEVEL_PRESETS[opt] || ALL_ON) }
|
|
84
96
|
if (typeof opt === 'object') {
|
|
85
97
|
const baseLevel = typeof opt.level === 'number' ? opt.level : 2
|
|
86
98
|
const base = LEVEL_PRESETS[baseLevel] || ALL_ON
|
|
87
99
|
const out = { ...base }
|
|
88
|
-
for (const n of PASS_NAMES) if (n in opt) out[n] = !!opt[n]
|
|
100
|
+
for (const n of PASS_NAMES) if (n in opt) out[n] = n === 'nestedSmallConstForUnroll' && opt[n] === 'auto' ? 'auto' : !!opt[n]
|
|
89
101
|
return out
|
|
90
102
|
}
|
|
91
103
|
return { ...ALL_ON }
|
|
@@ -137,8 +149,10 @@ export function hoistPtrType(fn) {
|
|
|
137
149
|
|
|
138
150
|
if (op === 'call' && node[1] === '$__ptr_type' && node.length === 3) {
|
|
139
151
|
const arg = node[2]
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
// Post-i64 migration: arg is (i64.reinterpret_f64 (local.get X)). Peel both wrappers.
|
|
153
|
+
const inner = (Array.isArray(arg) && arg[0] === 'i64.reinterpret_f64' && arg.length === 2) ? arg[1] : arg
|
|
154
|
+
if (Array.isArray(inner) && inner[0] === 'local.get' && typeof inner[1] === 'string') {
|
|
155
|
+
const x = inner[1]
|
|
142
156
|
let region = open.get(x)
|
|
143
157
|
if (!region) {
|
|
144
158
|
region = []
|
|
@@ -449,9 +463,11 @@ export function hoistInvariantPtrOffset(fn) {
|
|
|
449
463
|
const callee = node[1]
|
|
450
464
|
if (callee === '$__ptr_offset' && node.length === 3) {
|
|
451
465
|
const a = node[2]
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
466
|
+
// Post-i64 migration: arg may be (i64.reinterpret_f64 (local.get X)).
|
|
467
|
+
const inner = (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) ? a[1] : a
|
|
468
|
+
if (Array.isArray(inner) && inner[0] === 'local.get' && typeof inner[1] === 'string' && params.has(inner[1])) {
|
|
469
|
+
let arr = sites.get(inner[1])
|
|
470
|
+
if (!arr) { arr = []; sites.set(inner[1], arr) }
|
|
455
471
|
arr.push({ parent, idx: pi })
|
|
456
472
|
return
|
|
457
473
|
}
|
|
@@ -459,8 +475,9 @@ export function hoistInvariantPtrOffset(fn) {
|
|
|
459
475
|
const isSafe = SAFE_OFFSET_CALLS.has(callee)
|
|
460
476
|
for (let i = 2; i < node.length; i++) {
|
|
461
477
|
const arg = node[i]
|
|
462
|
-
|
|
463
|
-
|
|
478
|
+
const inner = (Array.isArray(arg) && arg[0] === 'i64.reinterpret_f64' && arg.length === 2) ? arg[1] : arg
|
|
479
|
+
if (Array.isArray(inner) && inner[0] === 'local.get' && typeof inner[1] === 'string' && params.has(inner[1])) {
|
|
480
|
+
if (!isSafe) unsafe.add(inner[1])
|
|
464
481
|
continue
|
|
465
482
|
}
|
|
466
483
|
walk(arg, node, i)
|
|
@@ -497,7 +514,7 @@ export function hoistInvariantPtrOffset(fn) {
|
|
|
497
514
|
if (arr.length < 2) continue
|
|
498
515
|
const tLocal = `$__po${hoistId++}`
|
|
499
516
|
newLocals.push(['local', tLocal, 'i32'])
|
|
500
|
-
snaps.push(['local.set', tLocal, ['call', '$__ptr_offset', ['local.get', X]]])
|
|
517
|
+
snaps.push(['local.set', tLocal, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', X]]]])
|
|
501
518
|
for (const { parent, idx } of arr) {
|
|
502
519
|
parent[idx] = ['local.get', tLocal]
|
|
503
520
|
}
|
|
@@ -506,6 +523,132 @@ export function hoistInvariantPtrOffset(fn) {
|
|
|
506
523
|
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals, ...snaps)
|
|
507
524
|
}
|
|
508
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Per-loop hoist of `(call $__ptr_offset (local.get X))` for locals X that are
|
|
528
|
+
* loop-invariant (no `local.set/local.tee` to X anywhere in the loop body, no
|
|
529
|
+
* non-safe call inside the loop). Mirrors `hoistInvariantCellLoads`.
|
|
530
|
+
*
|
|
531
|
+
* The function-level pass above only handles params (single hoist at func
|
|
532
|
+
* entry). This pass handles assigned locals that go invariant within a loop
|
|
533
|
+
* scope — the aos pattern: `let rows = []; for (...) rows.push(...)` — outside
|
|
534
|
+
* the build loop, `rows` is no longer reassigned, so `__ptr_offset(rows)` is
|
|
535
|
+
* loop-invariant in the consumer loop.
|
|
536
|
+
*
|
|
537
|
+
* Inside-out: inner loops first. After an inner-loop hoist, the outer loop now
|
|
538
|
+
* contains a `(local.set $__pol (call $__ptr_offset ...))` whose call gets
|
|
539
|
+
* hoisted again at the outer level, climbing the snap up to the outermost loop
|
|
540
|
+
* where X is invariant. Cleanup of the chained `local.set` movs is handled by
|
|
541
|
+
* watr CSE/DCE.
|
|
542
|
+
*/
|
|
543
|
+
export function hoistInvariantPtrOffsetLoop(fn) {
|
|
544
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
545
|
+
const bodyStart = findBodyStart(fn)
|
|
546
|
+
if (bodyStart < 0) return
|
|
547
|
+
|
|
548
|
+
let snapId = 0
|
|
549
|
+
while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__pol${snapId}`)) snapId++
|
|
550
|
+
const newLocals = []
|
|
551
|
+
|
|
552
|
+
// Refcount across the whole fn to skip shared subtrees (watr CSE may leave them).
|
|
553
|
+
const refcount = new Map()
|
|
554
|
+
const countRefs = (node) => {
|
|
555
|
+
if (!Array.isArray(node)) return
|
|
556
|
+
const n = (refcount.get(node) || 0) + 1
|
|
557
|
+
refcount.set(node, n)
|
|
558
|
+
if (n > 1) return
|
|
559
|
+
for (let i = 0; i < node.length; i++) countRefs(node[i])
|
|
560
|
+
}
|
|
561
|
+
countRefs(fn)
|
|
562
|
+
|
|
563
|
+
const processLoop = (loopNode) => {
|
|
564
|
+
// Recurse into inner loops first (bottom-up).
|
|
565
|
+
for (let i = 1; i < loopNode.length; i++) {
|
|
566
|
+
const child = loopNode[i]
|
|
567
|
+
if (!Array.isArray(child)) continue
|
|
568
|
+
processNode(child, loopNode, i)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Scan loop body (including nested loops) for writes to any local and for
|
|
572
|
+
// any non-safe call. Non-safe calls could realloc/move the underlying
|
|
573
|
+
// array storage and invalidate cached offsets.
|
|
574
|
+
const writes = new Set()
|
|
575
|
+
let unsafe = false
|
|
576
|
+
const scan = (node) => {
|
|
577
|
+
if (!Array.isArray(node)) return
|
|
578
|
+
const op = node[0]
|
|
579
|
+
if (op === 'local.set' || op === 'local.tee') {
|
|
580
|
+
if (typeof node[1] === 'string') writes.add(node[1])
|
|
581
|
+
for (let i = 2; i < node.length; i++) scan(node[i])
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
if (op === 'call') {
|
|
585
|
+
if (!SAFE_OFFSET_CALLS.has(node[1])) unsafe = true
|
|
586
|
+
for (let i = 2; i < node.length; i++) scan(node[i])
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
if (op === 'call_ref' || op === 'call_indirect') {
|
|
590
|
+
unsafe = true
|
|
591
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
595
|
+
}
|
|
596
|
+
for (let i = 1; i < loopNode.length; i++) scan(loopNode[i])
|
|
597
|
+
if (unsafe) return []
|
|
598
|
+
|
|
599
|
+
// Collect call sites for invariant locals. Skip nested loops (already
|
|
600
|
+
// processed) but recurse through everything else.
|
|
601
|
+
const sites = new Map() // localName → [{ parent, idx }]
|
|
602
|
+
const collect = (node, parent, idx) => {
|
|
603
|
+
if (!Array.isArray(node)) return
|
|
604
|
+
const op = node[0]
|
|
605
|
+
if (op === 'loop') return
|
|
606
|
+
if (op === 'call' && node[1] === '$__ptr_offset' && node.length === 3) {
|
|
607
|
+
const a = node[2]
|
|
608
|
+
const inner = (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) ? a[1] : a
|
|
609
|
+
if (Array.isArray(inner) && inner[0] === 'local.get' && typeof inner[1] === 'string'
|
|
610
|
+
&& !writes.has(inner[1])
|
|
611
|
+
&& (refcount.get(node) || 0) <= 1
|
|
612
|
+
&& (refcount.get(parent) || 0) <= 1) {
|
|
613
|
+
let arr = sites.get(inner[1])
|
|
614
|
+
if (!arr) { arr = []; sites.set(inner[1], arr) }
|
|
615
|
+
arr.push({ parent, idx })
|
|
616
|
+
}
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
for (let i = 0; i < node.length; i++) collect(node[i], node, i)
|
|
620
|
+
}
|
|
621
|
+
for (let i = 1; i < loopNode.length; i++) collect(loopNode[i], loopNode, i)
|
|
622
|
+
|
|
623
|
+
const snaps = []
|
|
624
|
+
for (const [X, arr] of sites) {
|
|
625
|
+
if (arr.length < 1) continue
|
|
626
|
+
const snapName = `$__pol${snapId++}`
|
|
627
|
+
newLocals.push(['local', snapName, 'i32'])
|
|
628
|
+
snaps.push(['local.set', snapName, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', X]]]])
|
|
629
|
+
for (const { parent, idx } of arr) {
|
|
630
|
+
parent[idx] = ['local.get', snapName]
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return snaps
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const processNode = (node, parent, idx) => {
|
|
637
|
+
if (!Array.isArray(node)) return
|
|
638
|
+
const op = node[0]
|
|
639
|
+
if (op === 'loop') {
|
|
640
|
+
const snaps = processLoop(node)
|
|
641
|
+
if (snaps.length) parent.splice(idx, 0, ...snaps)
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
for (let i = 0; i < node.length; i++) processNode(node[i], node, i)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (let i = bodyStart; i < fn.length; i++) processNode(fn[i], fn, i)
|
|
648
|
+
|
|
649
|
+
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
650
|
+
}
|
|
651
|
+
|
|
509
652
|
/**
|
|
510
653
|
* Hoist loop-invariant boxed-cell reads out of loops.
|
|
511
654
|
*
|
|
@@ -665,6 +808,314 @@ export function hoistInvariantCellLoads(fn) {
|
|
|
665
808
|
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
666
809
|
}
|
|
667
810
|
|
|
811
|
+
/**
|
|
812
|
+
* CSE for `(f64.load offset=K (local.get $X))` over straight-line regions
|
|
813
|
+
* where $X is an i32-typed local (an unboxed pointer in jz's value model).
|
|
814
|
+
*
|
|
815
|
+
* Aos hot path: `let p = rows[i]; xs[i] = p.x + p.y*0.25 + r;
|
|
816
|
+
* ys[i] = p.y - p.z*0.5;
|
|
817
|
+
* zs[i] = p.z + p.x*0.125`
|
|
818
|
+
* — emits 6 f64.load on $p (each of x/y/z twice); collapses to 3 unique loads
|
|
819
|
+
* shared via tee'd snap locals.
|
|
820
|
+
*
|
|
821
|
+
* Safety: jz's invariant — distinct unboxed-pointer locals come from distinct
|
|
822
|
+
* fresh allocations (analyzePtrUnboxable refuses to unbox aliased locals).
|
|
823
|
+
* So `(f64.store ADDR ...)` with base `(local.get $Y)` for $Y ≠ $X cannot
|
|
824
|
+
* touch addresses reachable via `$X + K`. Stores to typed-array slots in the
|
|
825
|
+
* loop body don't invalidate row-pointer reads.
|
|
826
|
+
*
|
|
827
|
+
* Region boundaries that flush the table:
|
|
828
|
+
* - branch (br/br_if/br_table/return/unreachable)
|
|
829
|
+
* - non-pure call
|
|
830
|
+
* - loop / if (control flow)
|
|
831
|
+
* - local.set/local.tee on a tracked $X (invalidates that X's entries)
|
|
832
|
+
* - store whose address tree references a tracked $X
|
|
833
|
+
* Blocks are treated as transparent — recurse into children.
|
|
834
|
+
*/
|
|
835
|
+
export function cseScalarLoad(fn) {
|
|
836
|
+
// DISABLED: the safety claim above relies on `analyzePtrUnboxable` having vetted
|
|
837
|
+
// every i32 local as a non-aliased fresh-allocation pointer. But this pass scans
|
|
838
|
+
// *all* i32 locals from `(local … i32)` decls — wasm-native i32 scalars (lengths,
|
|
839
|
+
// indices), narrow-ABI helper returns, and analyze.js's new arrayElemSchema-driven
|
|
840
|
+
// unboxes share the same declaration form. The metacircular path (jz-compiled
|
|
841
|
+
// watr.wasm) trips this: CSE'd loads survive across stores that legitimately
|
|
842
|
+
// mutate the same memory through a different i32 local, returning stale bytes
|
|
843
|
+
// (manifests as "memory access out of bounds" once a corrupted offset is
|
|
844
|
+
// dereferenced). Re-enable once candidacy is restricted to vetted pointers
|
|
845
|
+
// (e.g. emit-side annotation or rep-derived whitelist).
|
|
846
|
+
return
|
|
847
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
848
|
+
const bodyStart = findBodyStart(fn)
|
|
849
|
+
if (bodyStart < 0) return
|
|
850
|
+
|
|
851
|
+
const i32Locals = new Set()
|
|
852
|
+
for (let i = 2; i < fn.length; i++) {
|
|
853
|
+
const c = fn[i]
|
|
854
|
+
if (Array.isArray(c) && (c[0] === 'local' || c[0] === 'param') && typeof c[1] === 'string' && c[2] === 'i32') {
|
|
855
|
+
i32Locals.add(c[1])
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (!i32Locals.size) return
|
|
859
|
+
|
|
860
|
+
let snapId = 0
|
|
861
|
+
while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__cs${snapId}`)) snapId++
|
|
862
|
+
const newLocals = []
|
|
863
|
+
|
|
864
|
+
// CSE table: key `${X}|${K}` → { snapName | null, anchorParent, anchorIdx }
|
|
865
|
+
const table = new Map()
|
|
866
|
+
|
|
867
|
+
const invalidateLocal = (X) => {
|
|
868
|
+
for (const key of table.keys()) {
|
|
869
|
+
if (key.startsWith(`${X}|`)) table.delete(key)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Scan a node's subtree and return the set of i32 locals referenced via local.get.
|
|
874
|
+
const collectGets = (node, out) => {
|
|
875
|
+
if (!Array.isArray(node)) return
|
|
876
|
+
if (node[0] === 'local.get' && typeof node[1] === 'string' && i32Locals.has(node[1])) {
|
|
877
|
+
out.add(node[1])
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
for (let i = 1; i < node.length; i++) collectGets(node[i], out)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Parse f64.load shape; returns { K, addrIdx } or null.
|
|
884
|
+
const parseLoad = (node) => {
|
|
885
|
+
if (!Array.isArray(node) || node[0] !== 'f64.load') return null
|
|
886
|
+
let K = 0, addrIdx = 1
|
|
887
|
+
if (typeof node[1] === 'string' && node[1].startsWith('offset=')) {
|
|
888
|
+
K = parseInt(node[1].slice(7), 10) | 0
|
|
889
|
+
addrIdx = 2
|
|
890
|
+
}
|
|
891
|
+
if (node.length <= addrIdx) return null
|
|
892
|
+
return { K, addrIdx }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const walk = (node, parent, idx) => {
|
|
896
|
+
if (!Array.isArray(node)) return
|
|
897
|
+
const op = node[0]
|
|
898
|
+
|
|
899
|
+
// Control-flow boundaries: clear table.
|
|
900
|
+
if (op === 'br' || op === 'br_if' || op === 'br_table' || op === 'return' || op === 'unreachable') {
|
|
901
|
+
// Process args first (a br_if value, br arg, etc. could still benefit from current table)
|
|
902
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
903
|
+
table.clear()
|
|
904
|
+
return
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (op === 'loop' || op === 'if') {
|
|
908
|
+
// Save table state isn't useful; recurse with cleared table, then clear after.
|
|
909
|
+
const saved = new Map(table)
|
|
910
|
+
table.clear()
|
|
911
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
912
|
+
// After leaving compound, conservatively assume invalidation.
|
|
913
|
+
table.clear()
|
|
914
|
+
// Restore? No — restoring would be unsafe since the compound may have written.
|
|
915
|
+
saved.clear()
|
|
916
|
+
return
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (op === 'call') {
|
|
920
|
+
const callee = node[1]
|
|
921
|
+
// Process args first.
|
|
922
|
+
for (let i = 2; i < node.length; i++) walk(node[i], node, i)
|
|
923
|
+
if (!SAFE_OFFSET_CALLS.has(callee)) table.clear()
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (op === 'call_ref' || op === 'call_indirect') {
|
|
928
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
929
|
+
table.clear()
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (op === 'local.set' || op === 'local.tee') {
|
|
934
|
+
// Process value first.
|
|
935
|
+
for (let i = 2; i < node.length; i++) walk(node[i], node, i)
|
|
936
|
+
const X = node[1]
|
|
937
|
+
if (typeof X === 'string') invalidateLocal(X)
|
|
938
|
+
return
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Stores: process operands first; if address tree references any tracked X,
|
|
942
|
+
// invalidate that X's entries.
|
|
943
|
+
if (op === 'f64.store' || op === 'i32.store' || op === 'i64.store'
|
|
944
|
+
|| op === 'i32.store8' || op === 'i32.store16'
|
|
945
|
+
|| op === 'i64.store8' || op === 'i64.store16' || op === 'i64.store32'
|
|
946
|
+
|| op === 'f32.store') {
|
|
947
|
+
// Address may be node[1] (raw) or node[2] (when node[1] is offset=/align= attr).
|
|
948
|
+
let addrIdx = 1
|
|
949
|
+
if (typeof node[1] === 'string' && (node[1].startsWith('offset=') || node[1].startsWith('align='))) {
|
|
950
|
+
addrIdx = 2
|
|
951
|
+
}
|
|
952
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
953
|
+
const dirty = new Set()
|
|
954
|
+
collectGets(node[addrIdx], dirty)
|
|
955
|
+
for (const X of dirty) invalidateLocal(X)
|
|
956
|
+
return
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// f64.load: try CSE.
|
|
960
|
+
const lp = parseLoad(node)
|
|
961
|
+
if (lp) {
|
|
962
|
+
const addr = node[lp.addrIdx]
|
|
963
|
+
if (Array.isArray(addr) && addr[0] === 'local.get' && typeof addr[1] === 'string' && i32Locals.has(addr[1])) {
|
|
964
|
+
const X = addr[1]
|
|
965
|
+
const key = `${X}|${lp.K}`
|
|
966
|
+
const entry = table.get(key)
|
|
967
|
+
if (entry) {
|
|
968
|
+
if (!entry.snapName) {
|
|
969
|
+
const snapName = `$__cs${snapId++}`
|
|
970
|
+
entry.snapName = snapName
|
|
971
|
+
newLocals.push(['local', snapName, 'f64'])
|
|
972
|
+
// Wrap anchor with (local.tee $snap originalLoad).
|
|
973
|
+
const orig = entry.anchorParent[entry.anchorIdx]
|
|
974
|
+
entry.anchorParent[entry.anchorIdx] = ['local.tee', snapName, orig]
|
|
975
|
+
}
|
|
976
|
+
parent[idx] = ['local.get', entry.snapName]
|
|
977
|
+
return
|
|
978
|
+
} else {
|
|
979
|
+
table.set(key, { snapName: null, anchorParent: parent, anchorIdx: idx })
|
|
980
|
+
// Don't recurse; (local.get $X) has no children of interest.
|
|
981
|
+
return
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Non-CSE'able address; recurse to find inner loads.
|
|
985
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
986
|
+
return
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Default: recurse.
|
|
990
|
+
for (let i = 0; i < node.length; i++) walk(node[i], node, i)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (let i = bodyStart; i < fn.length; i++) walk(fn[i], fn, i)
|
|
994
|
+
|
|
995
|
+
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* CSE for pure f64 binary ops on local-only operands.
|
|
1000
|
+
*
|
|
1001
|
+
* Mandelbrot loop: condition computes `(f64.mul $zx $zx)` and `(f64.mul $zy $zy)`;
|
|
1002
|
+
* body recomputes both inside `tx = zx*zx - zy*zy + cx`. Pure ops on locals can't
|
|
1003
|
+
* alias memory — only `local.set/tee X` invalidates entries referencing X. Unlike
|
|
1004
|
+
* `cseScalarLoad`, br_if doesn't need to clear (no memory aliasing concern).
|
|
1005
|
+
*
|
|
1006
|
+
* Targets nodes of shape `(OP A B)` where OP ∈ {f64.mul, f64.add, f64.sub} and
|
|
1007
|
+
* A,B ∈ `(local.get X)` | `(f64.const N)`. Commutative ops (mul, add) sort
|
|
1008
|
+
* operand keys for canonical form.
|
|
1009
|
+
*
|
|
1010
|
+
* Region boundaries:
|
|
1011
|
+
* - `local.set/tee X` → invalidates entries referencing X
|
|
1012
|
+
* - `loop`, `if` → recurse with cleared table; clear after (compound may have written)
|
|
1013
|
+
* - `call`, `call_ref`, `call_indirect` → no clear (calls don't write locals directly;
|
|
1014
|
+
* the surrounding `local.set/tee` handles that)
|
|
1015
|
+
* - `br/br_if/br_table/return/unreachable` → NO clear (pure values still valid)
|
|
1016
|
+
*/
|
|
1017
|
+
export function csePureExpr(fn) {
|
|
1018
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
1019
|
+
const bodyStart = findBodyStart(fn)
|
|
1020
|
+
if (bodyStart < 0) return
|
|
1021
|
+
|
|
1022
|
+
let snapId = 0
|
|
1023
|
+
while (fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === `$__pe${snapId}`)) snapId++
|
|
1024
|
+
const newLocals = []
|
|
1025
|
+
|
|
1026
|
+
const COMMUTATIVE = new Set(['f64.mul', 'f64.add'])
|
|
1027
|
+
const TARGET_OPS = new Set(['f64.mul', 'f64.add', 'f64.sub'])
|
|
1028
|
+
|
|
1029
|
+
// Encode a leaf operand to a stable string key. Returns null if not pure-leaf.
|
|
1030
|
+
const leafKey = (n) => {
|
|
1031
|
+
if (!Array.isArray(n)) return null
|
|
1032
|
+
if (n[0] === 'local.get' && typeof n[1] === 'string') return `L:${n[1]}`
|
|
1033
|
+
if (n[0] === 'f64.const') return `C:${n[1]}`
|
|
1034
|
+
return null
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// table: key → { snapName | null, anchorParent, anchorIdx, locals: Set<string> }
|
|
1038
|
+
const table = new Map()
|
|
1039
|
+
|
|
1040
|
+
const invalidateLocal = (X) => {
|
|
1041
|
+
for (const [key, entry] of table) {
|
|
1042
|
+
if (entry.locals.has(X)) table.delete(key)
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const walk = (node, parent, idx) => {
|
|
1047
|
+
if (!Array.isArray(node)) return
|
|
1048
|
+
const op = node[0]
|
|
1049
|
+
|
|
1050
|
+
if (op === 'loop' || op === 'if') {
|
|
1051
|
+
const saved = new Map(table)
|
|
1052
|
+
table.clear()
|
|
1053
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
1054
|
+
table.clear()
|
|
1055
|
+
saved.clear()
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// `then`/`else` branches of an `if` are mutually exclusive at runtime —
|
|
1060
|
+
// a snap tee cached in the `then` branch is unset when the `else` runs.
|
|
1061
|
+
// Isolate per-branch tables so a sibling branch can't reach into another's
|
|
1062
|
+
// CSE entries.
|
|
1063
|
+
if (op === 'then' || op === 'else') {
|
|
1064
|
+
table.clear()
|
|
1065
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
1066
|
+
table.clear()
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (op === 'call' || op === 'call_ref' || op === 'call_indirect') {
|
|
1071
|
+
// Calls don't write locals; recurse, no clear.
|
|
1072
|
+
for (let i = 1; i < node.length; i++) walk(node[i], node, i)
|
|
1073
|
+
return
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (op === 'local.set' || op === 'local.tee') {
|
|
1077
|
+
for (let i = 2; i < node.length; i++) walk(node[i], node, i)
|
|
1078
|
+
const X = node[1]
|
|
1079
|
+
if (typeof X === 'string') invalidateLocal(X)
|
|
1080
|
+
return
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Try CSE on (OP A B) where A,B are pure leaves.
|
|
1084
|
+
if (TARGET_OPS.has(op) && node.length === 3) {
|
|
1085
|
+
const ka = leafKey(node[1])
|
|
1086
|
+
const kb = leafKey(node[2])
|
|
1087
|
+
if (ka && kb) {
|
|
1088
|
+
const key = COMMUTATIVE.has(op) && ka > kb ? `${op}|${kb}|${ka}` : `${op}|${ka}|${kb}`
|
|
1089
|
+
const entry = table.get(key)
|
|
1090
|
+
if (entry) {
|
|
1091
|
+
if (!entry.snapName) {
|
|
1092
|
+
const snapName = `$__pe${snapId++}`
|
|
1093
|
+
entry.snapName = snapName
|
|
1094
|
+
newLocals.push(['local', snapName, 'f64'])
|
|
1095
|
+
const orig = entry.anchorParent[entry.anchorIdx]
|
|
1096
|
+
entry.anchorParent[entry.anchorIdx] = ['local.tee', snapName, orig]
|
|
1097
|
+
}
|
|
1098
|
+
parent[idx] = ['local.get', entry.snapName]
|
|
1099
|
+
return
|
|
1100
|
+
} else {
|
|
1101
|
+
const locals = new Set()
|
|
1102
|
+
if (ka.startsWith('L:')) locals.add(ka.slice(2))
|
|
1103
|
+
if (kb.startsWith('L:')) locals.add(kb.slice(2))
|
|
1104
|
+
table.set(key, { snapName: null, anchorParent: parent, anchorIdx: idx, locals })
|
|
1105
|
+
return
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// Fall through to recurse.
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
for (let i = 0; i < node.length; i++) walk(node[i], node, i)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
for (let i = bodyStart; i < fn.length; i++) walk(fn[i], fn, i)
|
|
1115
|
+
|
|
1116
|
+
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
668
1119
|
/**
|
|
669
1120
|
* Find the index of the first body-content child in a func node.
|
|
670
1121
|
* Skips `$name`, (export …), (import …), (type …), (param …), (result …), (local …).
|
|
@@ -698,7 +1149,10 @@ export function hoistConstantPool(funcs, addGlobal) {
|
|
|
698
1149
|
for (let i = 0; i < node.length; i++) {
|
|
699
1150
|
const c = node[i]
|
|
700
1151
|
if (Array.isArray(c) && c[0] === 'f64.const' && (typeof c[1] === 'number' || typeof c[1] === 'string')) {
|
|
701
|
-
|
|
1152
|
+
// Distinguish -0 from +0 by sign: template literal collapses both to "0".
|
|
1153
|
+
const k = typeof c[1] === 'number'
|
|
1154
|
+
? (Object.is(c[1], -0) ? 'n:-0' : `n:${c[1]}`)
|
|
1155
|
+
: `s:${c[1]}`
|
|
702
1156
|
counts.set(k, (counts.get(k) || 0) + 1)
|
|
703
1157
|
sites.push({ parent: node, idx: i, key: k })
|
|
704
1158
|
}
|
|
@@ -758,8 +1212,8 @@ export function specializeMkptr(funcs, addFunc, parseWat) {
|
|
|
758
1212
|
const SPECS = {
|
|
759
1213
|
'$__mkptr': { params: ['i32', 'i32', 'i32'], result: 'f64', inline: true },
|
|
760
1214
|
'$__alloc_hdr': { params: ['i32', 'i32', 'i32'], result: 'i32' },
|
|
761
|
-
'$__typed_idx': { params: ['
|
|
762
|
-
'$__str_idx': { params: ['
|
|
1215
|
+
'$__typed_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
1216
|
+
'$__str_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
763
1217
|
}
|
|
764
1218
|
const MIN_USES = 5
|
|
765
1219
|
|
|
@@ -815,9 +1269,9 @@ export function specializeMkptr(funcs, addFunc, parseWat) {
|
|
|
815
1269
|
// $__mkptr inline fast path: bake (type, aux) literals into i64.const template.
|
|
816
1270
|
if (target === '$__mkptr' && spec.inline && parts[0].startsWith('L:') && parts[1].startsWith('L:')) {
|
|
817
1271
|
const type = +parts[0].slice(2), aux = +parts[1].slice(2)
|
|
818
|
-
const tmpl =
|
|
819
|
-
| ((BigInt(type) &
|
|
820
|
-
| ((BigInt(aux) &
|
|
1272
|
+
const tmpl = LAYOUT.NAN_PREFIX_BITS
|
|
1273
|
+
| ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
|
|
1274
|
+
| ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
|
|
821
1275
|
// Third arg (offset) may also be literal — emit (f64.const nan:…) then.
|
|
822
1276
|
if (parts[2].startsWith('L:')) {
|
|
823
1277
|
// Fully literal: all sites can be f64.const — no helper needed, handled in rewrite below.
|
|
@@ -859,10 +1313,10 @@ export function specializeMkptr(funcs, addFunc, parseWat) {
|
|
|
859
1313
|
// $__mkptr fully literal (rare — mkPtrIR usually folds these ahead of us, but defensive):
|
|
860
1314
|
if (target === '$__mkptr' && parts[0].startsWith('L:') && parts[1].startsWith('L:') && parts[2].startsWith('L:')) {
|
|
861
1315
|
const type = +parts[0].slice(2), aux = +parts[1].slice(2), off = +parts[2].slice(2)
|
|
862
|
-
const bits =
|
|
863
|
-
| ((BigInt(type) &
|
|
864
|
-
| ((BigInt(aux) &
|
|
865
|
-
| (BigInt(off >>> 0) &
|
|
1316
|
+
const bits = LAYOUT.NAN_PREFIX_BITS
|
|
1317
|
+
| ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
|
|
1318
|
+
| ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
|
|
1319
|
+
| (BigInt(off >>> 0) & BigInt(LAYOUT.OFFSET_MASK))
|
|
866
1320
|
const n = ['f64.const', 'nan:0x' + bits.toString(16).toUpperCase().padStart(16, '0')]
|
|
867
1321
|
n.type = 'f64'
|
|
868
1322
|
parent[idx] = n
|
|
@@ -1053,16 +1507,28 @@ export function sortStrPoolByFreq(funcs, strPoolRef, strDedupMap) {
|
|
|
1053
1507
|
export function optimizeFunc(fn, cfg) {
|
|
1054
1508
|
if (cfg && cfg.hoistPtrType === false &&
|
|
1055
1509
|
cfg.hoistInvariantPtrOffset === false &&
|
|
1510
|
+
cfg.hoistInvariantPtrOffsetLoop === false &&
|
|
1056
1511
|
cfg.fusedRewrite === false &&
|
|
1057
1512
|
cfg.hoistAddrBase === false &&
|
|
1058
1513
|
cfg.hoistInvariantCellLoads === false &&
|
|
1059
|
-
cfg.
|
|
1514
|
+
cfg.cseScalarLoad === false &&
|
|
1515
|
+
cfg.csePureExpr === false &&
|
|
1516
|
+
cfg.sortLocalsByUse === false &&
|
|
1517
|
+
cfg.vectorizeLaneLocal === false) return
|
|
1060
1518
|
if (!cfg || cfg.hoistPtrType !== false) hoistPtrType(fn)
|
|
1061
1519
|
if (!cfg || cfg.hoistInvariantPtrOffset !== false) hoistInvariantPtrOffset(fn)
|
|
1520
|
+
if (!cfg || cfg.hoistInvariantPtrOffsetLoop !== false) hoistInvariantPtrOffsetLoop(fn)
|
|
1062
1521
|
const counts = new Map()
|
|
1063
1522
|
if (!cfg || cfg.fusedRewrite !== false) fusedRewrite(fn, counts)
|
|
1064
1523
|
if (!cfg || cfg.hoistAddrBase !== false) hoistAddrBase(fn)
|
|
1065
1524
|
if (!cfg || cfg.hoistInvariantCellLoads !== false) hoistInvariantCellLoads(fn)
|
|
1525
|
+
if (!cfg || cfg.cseScalarLoad !== false) cseScalarLoad(fn)
|
|
1526
|
+
if (!cfg || cfg.csePureExpr !== false) csePureExpr(fn)
|
|
1527
|
+
// Vectorizer runs only in the POST-watr phase (`__phase === 'post'`), where
|
|
1528
|
+
// narrow.js + watr have produced the canonical lane-local shape. Running it
|
|
1529
|
+
// pre-watr matches a noisier IR and would re-fire post-watr on the residual
|
|
1530
|
+
// tail — producing nested SIMD blocks. Single-pass via phase gating.
|
|
1531
|
+
if (cfg && cfg.vectorizeLaneLocal === true && cfg.__phase === 'post') vectorizeLaneLocal(fn)
|
|
1066
1532
|
if (!cfg || cfg.sortLocalsByUse !== false) sortLocalsByUse(fn, cfg && cfg.fusedRewrite !== false ? counts : null)
|
|
1067
1533
|
}
|
|
1068
1534
|
|
|
@@ -1109,32 +1575,59 @@ function walkRewrite(node, doInline, counts) {
|
|
|
1109
1575
|
if (doInline && op === 'call' && node.length === 3 && typeof node[1] === 'string') {
|
|
1110
1576
|
const fname = node[1]
|
|
1111
1577
|
if (fname === '$__ptr_type') return ['i32.and',
|
|
1112
|
-
['i32.wrap_i64', ['i64.shr_u',
|
|
1113
|
-
['i32.const',
|
|
1578
|
+
['i32.wrap_i64', ['i64.shr_u', node[2], ['i64.const', LAYOUT.TAG_SHIFT]]],
|
|
1579
|
+
['i32.const', LAYOUT.TAG_MASK]]
|
|
1114
1580
|
if (fname === '$__ptr_aux') return ['i32.and',
|
|
1115
|
-
['i32.wrap_i64', ['i64.shr_u',
|
|
1116
|
-
['i32.const',
|
|
1117
|
-
if (fname === '$__is_null') return ['i64.eq',
|
|
1118
|
-
if (fname === '$__is_nullish' && Array.isArray(node[2]) && node[2][0] === '
|
|
1119
|
-
|
|
1120
|
-
['i64.eq',
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1581
|
+
['i32.wrap_i64', ['i64.shr_u', node[2], ['i64.const', LAYOUT.AUX_SHIFT]]],
|
|
1582
|
+
['i32.const', LAYOUT.AUX_MASK]]
|
|
1583
|
+
if (fname === '$__is_null') return ['i64.eq', node[2], ['i64.const', NULL_BITS]]
|
|
1584
|
+
if (fname === '$__is_nullish' && Array.isArray(node[2]) && node[2][0] === 'i64.reinterpret_f64'
|
|
1585
|
+
&& Array.isArray(node[2][1]) && node[2][1][0] === 'local.get') return ['i32.or',
|
|
1586
|
+
['i64.eq', node[2], ['i64.const', NULL_BITS]],
|
|
1587
|
+
['i64.eq', node[2], ['i64.const', UNDEF_BITS]]]
|
|
1588
|
+
if (fname === '$__is_truthy' && Array.isArray(node[2]) && node[2][0] === 'i64.reinterpret_f64'
|
|
1589
|
+
&& Array.isArray(node[2][1]) && node[2][1][0] === 'local.get') {
|
|
1590
|
+
const lget = node[2][1]
|
|
1591
|
+
const bits = node[2]
|
|
1124
1592
|
return ['if', ['result', 'i32'],
|
|
1125
1593
|
['f64.eq', lget, lget],
|
|
1126
1594
|
['then', ['f64.ne', lget, ['f64.const', 0]]],
|
|
1127
1595
|
['else', ['i32.and',
|
|
1128
1596
|
['i32.and',
|
|
1129
|
-
['i64.ne', bits, ['i64.const',
|
|
1130
|
-
['i64.ne', bits, ['i64.const',
|
|
1597
|
+
['i64.ne', bits, ['i64.const', NAN_BITS]],
|
|
1598
|
+
['i64.ne', bits, ['i64.const', NULL_BITS]]],
|
|
1131
1599
|
['i32.and',
|
|
1132
|
-
['i64.ne', bits, ['i64.const',
|
|
1133
|
-
['i64.ne', bits, ['i64.const', '
|
|
1600
|
+
['i64.ne', bits, ['i64.const', UNDEF_BITS]],
|
|
1601
|
+
['i64.ne', bits, ['i64.const', '0x7FFA400000000000']]]]]]
|
|
1134
1602
|
}
|
|
1135
1603
|
}
|
|
1136
1604
|
|
|
1137
1605
|
// Peephole: rebox/unbox round-trips
|
|
1606
|
+
if ((op === 'f64.convert_i32_s' || op === 'f64.convert_i32_u') && node.length === 2) {
|
|
1607
|
+
const a = node[1]
|
|
1608
|
+
if (Array.isArray(a) && a[0] === 'i32.const') {
|
|
1609
|
+
const n = typeof a[1] === 'number' ? a[1] : typeof a[1] === 'string' ? Number(a[1]) : NaN
|
|
1610
|
+
if (Number.isFinite(n)) return ['f64.const', op === 'f64.convert_i32_u' ? n >>> 0 : n]
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (op === 'f64.mul' && node.length === 3) {
|
|
1614
|
+
const a = node[1], b = node[2]
|
|
1615
|
+
const isTwo = x => Array.isArray(x) && x[0] === 'f64.const' && x[1] === 2
|
|
1616
|
+
const isCheapF64 = x => Array.isArray(x) &&
|
|
1617
|
+
((x[0] === 'local.get' && typeof x[1] === 'string') ||
|
|
1618
|
+
(x[0] === 'f64.const' && typeof x[1] === 'number'))
|
|
1619
|
+
if (isTwo(a) && isCheapF64(b)) return ['f64.add', b, b]
|
|
1620
|
+
if (isTwo(b) && isCheapF64(a)) return ['f64.add', a, a]
|
|
1621
|
+
}
|
|
1622
|
+
if (op === 'i32.trunc_sat_f64_s' && node.length === 2) {
|
|
1623
|
+
const a = node[1]
|
|
1624
|
+
if (Array.isArray(a) && a[0] === 'f64.convert_i32_s' && a.length === 2) return a[1]
|
|
1625
|
+
}
|
|
1626
|
+
if (op === 'i64.trunc_sat_f64_s' && node.length === 2) {
|
|
1627
|
+
const a = node[1]
|
|
1628
|
+
if (Array.isArray(a) && a[0] === 'f64.convert_i32_s' && a.length === 2) return ['i64.extend_i32_s', a[1]]
|
|
1629
|
+
if (Array.isArray(a) && a[0] === 'f64.convert_i32_u' && a.length === 2) return ['i64.extend_i32_u', a[1]]
|
|
1630
|
+
}
|
|
1138
1631
|
if (op === 'i64.reinterpret_f64' && node.length === 2) {
|
|
1139
1632
|
const a = node[1]
|
|
1140
1633
|
if (Array.isArray(a) && a[0] === 'f64.reinterpret_i64' && a.length === 2) return a[1]
|
|
@@ -1350,3 +1843,96 @@ export function sortLocalsByUse(fn, precomputedCounts) {
|
|
|
1350
1843
|
locals.sort((a, b) => (counts.get(b[1]) || 0) - (counts.get(a[1]) || 0))
|
|
1351
1844
|
localIdxs.forEach((i, k) => { fn[i] = locals[k] })
|
|
1352
1845
|
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Module-level arena rewind: transitive escape analysis.
|
|
1849
|
+
*
|
|
1850
|
+
* Per-function `applyArenaRewind` in compile.js is limited to a static whitelist
|
|
1851
|
+
* of internal helpers. This pass generalizes by building a call graph and
|
|
1852
|
+
* propagating "arena-safe callee" status via fixed-point iteration.
|
|
1853
|
+
*
|
|
1854
|
+
* A function is an arena-safe callee if:
|
|
1855
|
+
* - no global.set, call_indirect, call_ref in body
|
|
1856
|
+
* - all user-function calls are to other arena-safe callees
|
|
1857
|
+
*
|
|
1858
|
+
* A function is arena-rewindable (gets heap save/restore injected) if:
|
|
1859
|
+
* - single scalar result (f64 or i32)
|
|
1860
|
+
* - contains allocation ($__alloc / $__alloc_hdr)
|
|
1861
|
+
* - no global.set, return_call, call_indirect, call_ref
|
|
1862
|
+
* - all user-function calls are to arena-safe callees
|
|
1863
|
+
* - does NOT return a pointer (checked via ptrTypes map from compile.js)
|
|
1864
|
+
*
|
|
1865
|
+
* Unlike the per-function pass, this does NOT require 0 params.
|
|
1866
|
+
*
|
|
1867
|
+
* @param {Array[]} fns - Array of func IR nodes (sec.funcs + sec.stdlib + sec.start)
|
|
1868
|
+
* @param {boolean} sharedMemory - Whether memory is shared (affects heap get/set IR)
|
|
1869
|
+
* @param {Map<string, {ptrKind: *}|null>} [ptrTypes] - Map from func name to ptrKind info.
|
|
1870
|
+
* Functions with ptrKind != null return pointers and cannot be rewound.
|
|
1871
|
+
* If omitted, no pointer-return check is done (conservative: fewer functions rewound).
|
|
1872
|
+
*/
|
|
1873
|
+
export function arenaRewindModule(fns) {
|
|
1874
|
+
const BUILTIN_SAFE = new Set([
|
|
1875
|
+
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
1876
|
+
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
1877
|
+
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
1878
|
+
])
|
|
1879
|
+
|
|
1880
|
+
// Phase 1: collect per-function metadata
|
|
1881
|
+
const fnMap = new Map()
|
|
1882
|
+
for (const fn of fns) {
|
|
1883
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') continue
|
|
1884
|
+
const name = fn[1]
|
|
1885
|
+
if (typeof name !== 'string') continue
|
|
1886
|
+
|
|
1887
|
+
let results = [], hasGlobalSet = false, hasReturnCall = false
|
|
1888
|
+
let hasCallIndirect = false, hasCallRef = false, hasAlloc = false
|
|
1889
|
+
const calls = new Set()
|
|
1890
|
+
const bodyStart = findBodyStart(fn)
|
|
1891
|
+
|
|
1892
|
+
for (let i = 2; i < fn.length; i++) {
|
|
1893
|
+
const c = fn[i]
|
|
1894
|
+
if (!Array.isArray(c)) continue
|
|
1895
|
+
if (c[0] === 'result') { results.push(c[1] || c[2]); continue }
|
|
1896
|
+
if (i >= bodyStart) break
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const scan = node => {
|
|
1900
|
+
if (!Array.isArray(node)) return
|
|
1901
|
+
const op = node[0]
|
|
1902
|
+
if (op === 'global.set') hasGlobalSet = true
|
|
1903
|
+
else if (op === 'return_call') hasReturnCall = true
|
|
1904
|
+
else if (op === 'call_indirect') hasCallIndirect = true
|
|
1905
|
+
else if (op === 'call_ref') hasCallRef = true
|
|
1906
|
+
else if (op === 'call') {
|
|
1907
|
+
const callee = node[1]
|
|
1908
|
+
if (callee === '$__alloc' || callee === '$__alloc_hdr') hasAlloc = true
|
|
1909
|
+
if (typeof callee === 'string' && !BUILTIN_SAFE.has(callee)) calls.add(callee)
|
|
1910
|
+
}
|
|
1911
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
1912
|
+
}
|
|
1913
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
1914
|
+
|
|
1915
|
+
fnMap.set(name, {
|
|
1916
|
+
fn, results,
|
|
1917
|
+
hasGlobalSet, hasReturnCall, hasCallIndirect, hasCallRef, hasAlloc,
|
|
1918
|
+
calls: [...calls],
|
|
1919
|
+
})
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Phase 2: fixed-point transitive safety analysis
|
|
1923
|
+
const safeCallees = new Set(BUILTIN_SAFE)
|
|
1924
|
+
let changed = true
|
|
1925
|
+
while (changed) {
|
|
1926
|
+
changed = false
|
|
1927
|
+
for (const [name, info] of fnMap) {
|
|
1928
|
+
if (safeCallees.has(name)) continue
|
|
1929
|
+
if (info.hasGlobalSet || info.hasCallIndirect || info.hasCallRef) continue
|
|
1930
|
+
if (info.calls.every(c => safeCallees.has(c) || !fnMap.has(c))) {
|
|
1931
|
+
safeCallees.add(name)
|
|
1932
|
+
changed = true
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
return safeCallees
|
|
1938
|
+
}
|