Skip to content

Commit d3f2df3

Browse files
authored
[Wasm GC] Fix struct.set / ref.as_non_null ordering issue (#5474)
If traps can happen, then we can't always remove a trap on null on the ref input to struct.set, since it has two children, (struct.set (ref.as_non_null X) (call $foo)) Removing the ref.as would not prevent a trap, as the struct.set will trap, but it does move the trap to after the call which is bad.
1 parent 91f54df commit d3f2df3

File tree

2 files changed

+98
-10
lines changed

2 files changed

+98
-10
lines changed

src/passes/OptimizeInstructions.cpp

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,7 +1283,7 @@ struct OptimizeInstructions
12831283
}
12841284

12851285
void visitCallRef(CallRef* curr) {
1286-
skipNonNullCast(curr->target);
1286+
skipNonNullCast(curr->target, curr);
12871287
if (trapOnNull(curr, curr->target)) {
12881288
return;
12891289
}
@@ -1473,10 +1473,51 @@ struct OptimizeInstructions
14731473
// See "notes on removing casts", above. However, in most cases removing a
14741474
// non-null cast is obviously safe to do, since we only remove one if another
14751475
// check will happen later.
1476-
void skipNonNullCast(Expression*& input) {
1476+
//
1477+
// We also pass in the parent, because we need to be careful about ordering:
1478+
// if the parent has other children than |input| then we may not be able to
1479+
// remove the trap. For example,
1480+
//
1481+
// (struct.set
1482+
// (ref.as_non_null X)
1483+
// (call $foo)
1484+
// )
1485+
//
1486+
// If X is null we'd trap before the call to $foo. If we remove the
1487+
// ref.as_non_null then the struct.set will still trap, of course, but that
1488+
// will only happen *after* the call, which is wrong.
1489+
void skipNonNullCast(Expression*& input, Expression* parent) {
1490+
// Check the other children for the ordering problem only if we find a
1491+
// possible optimization, to avoid wasted work.
1492+
bool checkedSiblings = false;
1493+
auto& options = getPassOptions();
14771494
while (1) {
14781495
if (auto* as = input->dynCast<RefAs>()) {
14791496
if (as->op == RefAsNonNull) {
1497+
// The problem with effect ordering that is described above is not an
1498+
// issue if traps are assumed to never happen anyhow.
1499+
if (!checkedSiblings && !options.trapsNeverHappen) {
1500+
// We need to see if a child with side effects exists after |input|.
1501+
// If there is such a child, it is a problem as mentioned above (it
1502+
// is fine for such a child to appear *before* |input|, as then we
1503+
// wouldn't be reordering effects).
1504+
bool seenInput = false;
1505+
for (auto* child : ChildIterator(parent)) {
1506+
if (child == input) {
1507+
seenInput = true;
1508+
} else if (seenInput) {
1509+
// TODO We could ignore trap effects here (since traps are ok to
1510+
// reorder) and also local effects (since a change to a var
1511+
// would not be noticeable, unlike say a global).
1512+
if (EffectAnalyzer(options, *getModule(), child)
1513+
.hasSideEffects()) {
1514+
return;
1515+
}
1516+
}
1517+
}
1518+
// If we got here, we've checked the siblings and found no problem.
1519+
checkedSiblings = true;
1520+
}
14801521
input = as->value;
14811522
continue;
14821523
}
@@ -1717,12 +1758,12 @@ struct OptimizeInstructions
17171758
}
17181759

17191760
void visitStructGet(StructGet* curr) {
1720-
skipNonNullCast(curr->ref);
1761+
skipNonNullCast(curr->ref, curr);
17211762
trapOnNull(curr, curr->ref);
17221763
}
17231764

17241765
void visitStructSet(StructSet* curr) {
1725-
skipNonNullCast(curr->ref);
1766+
skipNonNullCast(curr->ref, curr);
17261767
if (trapOnNull(curr, curr->ref)) {
17271768
return;
17281769
}
@@ -1878,12 +1919,12 @@ struct OptimizeInstructions
18781919
}
18791920

18801921
void visitArrayGet(ArrayGet* curr) {
1881-
skipNonNullCast(curr->ref);
1922+
skipNonNullCast(curr->ref, curr);
18821923
trapOnNull(curr, curr->ref);
18831924
}
18841925

18851926
void visitArraySet(ArraySet* curr) {
1886-
skipNonNullCast(curr->ref);
1927+
skipNonNullCast(curr->ref, curr);
18871928
if (trapOnNull(curr, curr->ref)) {
18881929
return;
18891930
}
@@ -1895,13 +1936,13 @@ struct OptimizeInstructions
18951936
}
18961937

18971938
void visitArrayLen(ArrayLen* curr) {
1898-
skipNonNullCast(curr->ref);
1939+
skipNonNullCast(curr->ref, curr);
18991940
trapOnNull(curr, curr->ref);
19001941
}
19011942

19021943
void visitArrayCopy(ArrayCopy* curr) {
1903-
skipNonNullCast(curr->destRef);
1904-
skipNonNullCast(curr->srcRef);
1944+
skipNonNullCast(curr->destRef, curr);
1945+
skipNonNullCast(curr->srcRef, curr);
19051946
trapOnNull(curr, curr->destRef) || trapOnNull(curr, curr->srcRef);
19061947
}
19071948

@@ -2155,7 +2196,7 @@ struct OptimizeInstructions
21552196
}
21562197

21572198
assert(curr->op == RefAsNonNull);
2158-
skipNonNullCast(curr->value);
2199+
skipNonNullCast(curr->value, curr);
21592200
if (!curr->value->type.isNullable()) {
21602201
replaceCurrent(curr->value);
21612202
return;

test/lit/passes/optimize-instructions-gc-tnh.wast

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
;; NO_TNH: (type $void (func))
1212
(type $void (func))
1313

14+
;; TNH: (import "a" "b" (func $import (result i32)))
15+
;; NO_TNH: (import "a" "b" (func $import (result i32)))
16+
(import "a" "b" (func $import (result i32)))
17+
1418
;; TNH: (func $ref.eq (type $eqref_eqref_=>_i32) (param $a eqref) (param $b eqref) (result i32)
1519
;; TNH-NEXT: (ref.eq
1620
;; TNH-NEXT: (local.get $a)
@@ -603,6 +607,49 @@
603607
)
604608
)
605609

610+
;; TNH: (func $null.cast-other.effects (type $ref?|$struct|_=>_none) (param $x (ref null $struct))
611+
;; TNH-NEXT: (struct.set $struct 0
612+
;; TNH-NEXT: (local.get $x)
613+
;; TNH-NEXT: (call $import)
614+
;; TNH-NEXT: )
615+
;; TNH-NEXT: (struct.set $struct 0
616+
;; TNH-NEXT: (local.get $x)
617+
;; TNH-NEXT: (i32.const 10)
618+
;; TNH-NEXT: )
619+
;; TNH-NEXT: )
620+
;; NO_TNH: (func $null.cast-other.effects (type $ref?|$struct|_=>_none) (param $x (ref null $struct))
621+
;; NO_TNH-NEXT: (struct.set $struct 0
622+
;; NO_TNH-NEXT: (ref.as_non_null
623+
;; NO_TNH-NEXT: (local.get $x)
624+
;; NO_TNH-NEXT: )
625+
;; NO_TNH-NEXT: (call $import)
626+
;; NO_TNH-NEXT: )
627+
;; NO_TNH-NEXT: (struct.set $struct 0
628+
;; NO_TNH-NEXT: (local.get $x)
629+
;; NO_TNH-NEXT: (i32.const 10)
630+
;; NO_TNH-NEXT: )
631+
;; NO_TNH-NEXT: )
632+
(func $null.cast-other.effects (param $x (ref null $struct))
633+
(struct.set $struct 0
634+
;; We cannot remove this ref.as_non_null, even though the struct.set will
635+
;; trap if the ref is null, because that would move the trap from before
636+
;; the call to the import to be after it. But in TNH we can assume it does
637+
;; not trap, and remove it.
638+
(ref.as_non_null
639+
(local.get $x)
640+
)
641+
(call $import)
642+
)
643+
(struct.set $struct 0
644+
;; This one can be removed even without TNH, as there are no effects after
645+
;; it.
646+
(ref.as_non_null
647+
(local.get $x)
648+
)
649+
(i32.const 10)
650+
)
651+
)
652+
606653
;; Helper functions.
607654

608655
;; TNH: (func $get-i32 (type $none_=>_i32) (result i32)

0 commit comments

Comments
 (0)