|
| 1 | +//@ runDefaultWasm("-m", "--useConcurrentJIT=0", "--thresholdForOMGOptimizeAfterWarmUp=0", "--thresholdForBBQOptimizeAfterWarmUp=0") |
| 2 | + |
| 3 | +// Exercises B3's eliminateWasmGCAllocations (dead non-escaping struct removal) plus the |
| 4 | +// reduceStrength ref.eq folding of fresh allocations. Everything runs in a hot loop so the |
| 5 | +// OMG tier (where the pass runs) compiles and executes. |
| 6 | + |
| 7 | +import { instantiate } from "../gc/wast-wrapper.js" |
| 8 | +import * as assert from "../assert.js" |
| 9 | + |
| 10 | +const wat = ` |
| 11 | +(module |
| 12 | + (type $point (struct (field $x (mut i32)) (field $y (mut i32)))) |
| 13 | + (type $box (struct (field $p (mut (ref null $point))))) |
| 14 | + (type $node (struct (field $v (mut i32)) (field $next (mut (ref null $node))))) |
| 15 | +
|
| 16 | + ;; Allocate a struct, initialize it, never read it: pure dead allocation. |
| 17 | + (func (export "deadStruct") (param $a i32) (param $b i32) (result i32) |
| 18 | + (local $p (ref $point)) |
| 19 | + (local.set $p (struct.new $point (local.get $a) (local.get $b))) |
| 20 | + (i32.add (local.get $a) (local.get $b))) |
| 21 | +
|
| 22 | + ;; Allocate, set fields, read them back: CSE forwards the reads, the allocation is dead, |
| 23 | + ;; and the returned value must still be correct. |
| 24 | + (func (export "readBack") (param $a i32) (param $b i32) (result i32) |
| 25 | + (local $p (ref $point)) |
| 26 | + (local.set $p (struct.new $point (local.get $a) (local.get $b))) |
| 27 | + (i32.sub (struct.get $point $x (local.get $p)) |
| 28 | + (struct.get $point $y (local.get $p)))) |
| 29 | +
|
| 30 | + ;; The struct escapes into a longer-lived struct that is returned, so it must NOT be |
| 31 | + ;; removed and the field read through the escaped reference must be correct. |
| 32 | + (func (export "escapeIntoBox") (param $a i32) (result i32) |
| 33 | + (local $p (ref $point)) |
| 34 | + (local $b (ref $box)) |
| 35 | + (local.set $p (struct.new $point (local.get $a) (i32.const 7))) |
| 36 | + (local.set $b (struct.new $box (local.get $p))) |
| 37 | + (struct.get $point $x (struct.get $box $p (local.get $b)))) |
| 38 | +
|
| 39 | + ;; ref.eq of a fresh struct against itself is always true. |
| 40 | + (func (export "eqSelf") (param $a i32) (result i32) |
| 41 | + (local $p (ref $point)) |
| 42 | + (local.set $p (struct.new $point (local.get $a) (local.get $a))) |
| 43 | + (ref.eq (local.get $p) (local.get $p))) |
| 44 | +
|
| 45 | + ;; ref.eq of two distinct fresh structs is always false. |
| 46 | + (func (export "eqTwo") (param $a i32) (result i32) |
| 47 | + (ref.eq (struct.new $point (local.get $a) (local.get $a)) |
| 48 | + (struct.new $point (local.get $a) (local.get $a)))) |
| 49 | +
|
| 50 | + ;; ref.eq of a fresh struct against null is always false (fresh is non-null). |
| 51 | + (func (export "eqNull") (param $a i32) (result i32) |
| 52 | + (ref.eq (struct.new $point (local.get $a) (local.get $a)) |
| 53 | + (ref.null $point))) |
| 54 | +
|
| 55 | + ;; Self-referential store: the allocation's own pointer is written into its own field |
| 56 | + ;; (child(0) is the base, so it stays a removal candidate), and it is never read. This |
| 57 | + ;; is the a.next = a shape; it must be removed without miscompiling. |
| 58 | + (func (export "selfRefDead") (param $a i32) (result i32) |
| 59 | + (local $n (ref $node)) |
| 60 | + (local.set $n (struct.new $node (local.get $a) (ref.null $node))) |
| 61 | + (struct.set $node $next (local.get $n) (local.get $n)) |
| 62 | + (local.get $a)) |
| 63 | +
|
| 64 | + ;; Same self-referential store, but a field is read back afterwards (CSE forwards the |
| 65 | + ;; read of $v). Still dead; result must be correct. |
| 66 | + (func (export "selfRefRead") (param $a i32) (result i32) |
| 67 | + (local $n (ref $node)) |
| 68 | + (local.set $n (struct.new $node (local.get $a) (ref.null $node))) |
| 69 | + (struct.set $node $next (local.get $n) (local.get $n)) |
| 70 | + (struct.get $node $v (local.get $n))) |
| 71 | +
|
| 72 | + ;; Self-referential store AND the node escapes (returned via $makeNode); it must be kept |
| 73 | + ;; and the cycle followed one hop must read the right value. |
| 74 | + (func $makeNode (param $a i32) (result (ref $node)) |
| 75 | + (local $n (ref $node)) |
| 76 | + (local.set $n (struct.new $node (local.get $a) (ref.null $node))) |
| 77 | + (struct.set $node $next (local.get $n) (local.get $n)) |
| 78 | + (local.get $n)) |
| 79 | + (func (export "selfRefEscape") (param $a i32) (result i32) |
| 80 | + (local $n (ref $node)) |
| 81 | + (local.set $n (call $makeNode (local.get $a))) |
| 82 | + ;; n.next.next.v -- follows the self-cycle twice, must equal a. |
| 83 | + (struct.get $node $v |
| 84 | + (struct.get $node $next |
| 85 | + (struct.get $node $next (local.get $n))))) |
| 86 | +) |
| 87 | +`; |
| 88 | + |
| 89 | +globalThis.testLoopCount ??= 10000; |
| 90 | + |
| 91 | +async function test() { |
| 92 | + const instance = await instantiate(wat, {}); |
| 93 | + const { deadStruct, readBack, escapeIntoBox, eqSelf, eqTwo, eqNull, selfRefDead, selfRefRead, selfRefEscape } = instance.exports; |
| 94 | + |
| 95 | + for (let i = 0; i < testLoopCount; ++i) { |
| 96 | + assert.eq(deadStruct(i, 3), i + 3); |
| 97 | + assert.eq(readBack(i + 10, 4), i + 6); |
| 98 | + assert.eq(escapeIntoBox(i), i); |
| 99 | + assert.eq(eqSelf(i), 1); |
| 100 | + assert.eq(eqTwo(i), 0); |
| 101 | + assert.eq(eqNull(i), 0); |
| 102 | + assert.eq(selfRefDead(i), i); |
| 103 | + assert.eq(selfRefRead(i), i); |
| 104 | + assert.eq(selfRefEscape(i), i); |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +await assert.asyncTest(test()); |
0 commit comments