Skip to content

Commit 8844b1f

Browse files
authored
[Custom Descriptors] Handle global effects in GTO (#7698)
GTO uses ChildLocalizer to ensure that removing children from a struct.new does not drop any side effects. But that does not work outside function contexts. This was previously not a problem because there were no side effects possible outside function contexts, but now with custom descriptors it is possible for struct.news in constant expressions to trap due to being given null descriptors. To preserve side effects during instantiation, put expressions that might trap and that have been removed outside function contexts into new globals. This ensures that their side effects, if any, are still executed during instantiation.
1 parent 99cf922 commit 8844b1f

File tree

3 files changed

+163
-7
lines changed

3 files changed

+163
-7
lines changed

scripts/test/fuzzing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
'gc-desc.wast',
128128
'simplify-locals-desc.wast',
129129
'optimize-instructions-desc.wast',
130+
'gto-desc.wast',
130131
# TODO: fix split_wast() on tricky escaping situations like a string ending
131132
# in \\" (the " is not escaped - there is an escaped \ before it)
132133
'string-lifting-section.wast',

src/passes/GlobalTypeOptimization.cpp

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "ir/eh-utils.h"
2626
#include "ir/localize.h"
2727
#include "ir/module-utils.h"
28+
#include "ir/names.h"
2829
#include "ir/ordering.h"
2930
#include "ir/struct-utils.h"
3031
#include "ir/subtypes.h"
@@ -461,6 +462,11 @@ struct GlobalTypeOptimization : public Pass {
461462

462463
bool needEHFixups = false;
463464

465+
// Expressions that might trap that have been removed from module-level
466+
// initializers. These need to be placed in new globals to preserve any
467+
// instantiation-time traps.
468+
std::vector<Expression*> removedTrappingInits;
469+
464470
void visitStructNew(StructNew* curr) {
465471
if (curr->type == Type::unreachable) {
466472
return;
@@ -481,22 +487,31 @@ struct GlobalTypeOptimization : public Pass {
481487

482488
// Ensure any children with non-trivial effects are replaced with
483489
// local.gets, so that we can remove/reorder to our hearts' content.
484-
ChildLocalizer localizer(
485-
curr, getFunction(), *getModule(), getPassOptions());
486-
replaceCurrent(localizer.getReplacement());
487-
// Adding a block here requires EH fixups.
488-
needEHFixups = true;
490+
// We can only do this inside functions. Outside of functions, we
491+
// preserve traps during instantiation by creating new globals to hold
492+
// removed and potentially-trapping operands instead.
493+
auto* func = getFunction();
494+
if (func) {
495+
ChildLocalizer localizer(curr, func, *getModule(), getPassOptions());
496+
replaceCurrent(localizer.getReplacement());
497+
// Adding a block here requires EH fixups.
498+
needEHFixups = true;
499+
}
489500

490501
// Remove and reorder operands.
491502
Index removed = 0;
492503
std::vector<Expression*> old(operands.begin(), operands.end());
493-
for (Index i = 0; i < operands.size(); i++) {
504+
for (Index i = 0; i < operands.size(); ++i) {
494505
auto newIndex = indexesAfterRemoval[i];
495506
if (newIndex != RemovedField) {
496507
assert(newIndex < operands.size());
497508
operands[newIndex] = old[i];
498509
} else {
499-
removed++;
510+
++removed;
511+
if (!func &&
512+
EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) {
513+
removedTrappingInits.push_back(old[i]);
514+
}
500515
}
501516
}
502517
if (removed) {
@@ -573,6 +588,16 @@ struct GlobalTypeOptimization : public Pass {
573588
FieldRemover remover(*this);
574589
remover.run(getPassRunner(), &wasm);
575590
remover.runOnModuleCode(getPassRunner(), &wasm);
591+
592+
// Insert globals necessary to preserve instantiation-time trapping of
593+
// removed expressions.
594+
for (Index i = 0; i < remover.removedTrappingInits.size(); ++i) {
595+
auto* curr = remover.removedTrappingInits[i];
596+
auto name = Names::getValidGlobalName(
597+
wasm, std::string("gto-removed-") + std::to_string(i));
598+
wasm.addGlobal(
599+
Builder::makeGlobal(name, curr->type, curr, Builder::Immutable));
600+
}
576601
}
577602
};
578603

test/lit/passes/gto-desc.wast

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2+
3+
;; RUN: wasm-opt %s -all --closed-world --gto --preserve-type-order -S -o - | filecheck %s
4+
5+
(module
6+
(rec
7+
;; CHECK: (rec
8+
;; CHECK-NEXT: (type $struct (descriptor $desc (struct (field i32))))
9+
(type $struct (descriptor $desc (struct (field i32))))
10+
;; CHECK: (type $desc (describes $struct (struct)))
11+
(type $desc (describes $struct (struct)))
12+
;; CHECK: (type $pair (struct))
13+
(type $pair (struct (field (ref $struct)) (field (ref $struct))))
14+
;; CHECK: (type $used-pair (struct (field (ref $struct)) (field (ref $struct))))
15+
(type $used-pair (struct (field (ref $struct)) (field (ref $struct))))
16+
)
17+
18+
;; CHECK: (type $4 (func (param (ref $struct) (ref $used-pair))))
19+
20+
;; CHECK: (global $nullable-desc (ref null (exact $desc)) (struct.new_default $desc))
21+
(global $nullable-desc (ref null (exact $desc)) (struct.new $desc))
22+
23+
;; Check that we generate fresh names for added globals.
24+
;; CHECK: (global $gto-removed-1 nullref (ref.null none))
25+
(global $gto-removed-1 nullref (ref.null none))
26+
27+
;; CHECK: (global $second-traps (ref $pair) (struct.new_default $pair))
28+
(global $second-traps (ref $pair)
29+
;; Both fields will be removed, but only the second can trap, so only the
30+
;; second will be moved to a new global.
31+
(struct.new $pair
32+
(struct.new $struct
33+
(i32.const 0)
34+
(struct.new $desc)
35+
)
36+
(struct.new $struct
37+
(i32.const 1)
38+
(ref.null none)
39+
)
40+
)
41+
)
42+
43+
;; CHECK: (global $first-traps (ref $pair) (struct.new_default $pair))
44+
(global $first-traps (ref $pair)
45+
;; Same as above, but now the first traps (or at least we assume it can
46+
;; based on its type).
47+
(struct.new $pair
48+
(struct.new $struct
49+
(i32.const 2)
50+
(global.get $nullable-desc)
51+
)
52+
(struct.new $struct
53+
(i32.const 3)
54+
(struct.new $desc)
55+
)
56+
)
57+
)
58+
59+
;; CHECK: (global $used-traps (ref $used-pair) (struct.new $used-pair
60+
;; CHECK-NEXT: (struct.new $struct
61+
;; CHECK-NEXT: (i32.const 4)
62+
;; CHECK-NEXT: (ref.null none)
63+
;; CHECK-NEXT: )
64+
;; CHECK-NEXT: (struct.new $struct
65+
;; CHECK-NEXT: (i32.const 5)
66+
;; CHECK-NEXT: (ref.null none)
67+
;; CHECK-NEXT: )
68+
;; CHECK-NEXT: ))
69+
(global $used-traps (ref $used-pair)
70+
;; Now both trap, but they are also used, so they will not be removed and no
71+
;; globals will be created.
72+
(struct.new $used-pair
73+
(struct.new $struct
74+
(i32.const 4)
75+
(ref.null none)
76+
)
77+
(struct.new $struct
78+
(i32.const 5)
79+
(ref.null none)
80+
)
81+
)
82+
)
83+
84+
;; CHECK: (global $gto-removed-0 (ref (exact $struct)) (struct.new $struct
85+
;; CHECK-NEXT: (i32.const 1)
86+
;; CHECK-NEXT: (ref.null none)
87+
;; CHECK-NEXT: ))
88+
89+
;; CHECK: (global $gto-removed-1_6 (ref (exact $struct)) (struct.new $struct
90+
;; CHECK-NEXT: (i32.const 2)
91+
;; CHECK-NEXT: (global.get $nullable-desc)
92+
;; CHECK-NEXT: ))
93+
94+
;; CHECK: (func $use-struct-fields (type $4) (param $0 (ref $struct)) (param $1 (ref $used-pair))
95+
;; CHECK-NEXT: (drop
96+
;; CHECK-NEXT: (struct.get $struct 0
97+
;; CHECK-NEXT: (local.get $0)
98+
;; CHECK-NEXT: )
99+
;; CHECK-NEXT: )
100+
;; CHECK-NEXT: (drop
101+
;; CHECK-NEXT: (struct.get $used-pair 0
102+
;; CHECK-NEXT: (local.get $1)
103+
;; CHECK-NEXT: )
104+
;; CHECK-NEXT: )
105+
;; CHECK-NEXT: (drop
106+
;; CHECK-NEXT: (struct.get $used-pair 1
107+
;; CHECK-NEXT: (local.get $1)
108+
;; CHECK-NEXT: )
109+
;; CHECK-NEXT: )
110+
;; CHECK-NEXT: )
111+
(func $use-struct-fields (param (ref $struct)) (param (ref $used-pair))
112+
;; Prevent the i32s in the initializers from being removed.
113+
(drop
114+
(struct.get $struct 0
115+
(local.get 0)
116+
)
117+
)
118+
;; Prevent the fields in used-pair from being removed.
119+
(drop
120+
(struct.get $used-pair 0
121+
(local.get 1)
122+
)
123+
)
124+
(drop
125+
(struct.get $used-pair 1
126+
(local.get 1)
127+
)
128+
)
129+
)
130+
)

0 commit comments

Comments
 (0)