Skip to content

Commit 8e1abd1

Browse files
authored
[Wasm GC] Support non-nullable locals in the "1a" form (#4959)
An overview of this is in the README in the diff here (conveniently, it is near the top of the diff). Basically, we fix up nn locals after each pass, by default. This keeps things easy to reason about - what validates is what is valid wasm - but there are some minor nuances as mentioned there, in particular, we ignore nameless blocks (which are commonly added by various passes; ignoring them means we can keep more locals non-nullable). The key addition here is LocalStructuralDominance which checks which local indexes have the "structural dominance" property of 1a, that is, that each get has a set in its block or an outer block that precedes it. I optimized that function quite a lot to reduce the overhead of running that logic after each pass. The overhead is something like 2% on J2Wasm and 0% on Dart (0%, because in this mode we shrink code size, so there is less work actually, and it balances out). Since we run fixups after each pass, this PR removes logic to manually call the fixup code from various places we used to call it (like eh-utils and various passes). Various passes are now marked as requiresNonNullableLocalFixups => false. That lets us skip running the fixups after them, which we normally do automatically. This helps avoid overhead. Most passes still need the fixups, though - any pass that adds a local, or a named block, or moves code around, likely does. This removes a hack in SimplifyLocals that is no longer needed. Before we worked to avoid moving a set into a try, as it might not validate. Now, we just do it and let fixups happen automatically if they need to: in the common code they probably don't, so the extra complexity seems not worth it. Also removes a hack from StackIR. That hack tried to avoid roundtrip adding a nondefaultable local. But we have the logic to fix that up now, and opts will likely keep it non-nullable as well. Various tests end up updated here because now a local can be non-nullable - previous fixups are no longer needed. Note that this doesn't remove the gc-nn-locals feature. That has been useful for testing, and may still be useful in the future - it basically just allows nn locals in all positions (that can't read the null default value at the entry). We can consider removing it separately. Fixes #4824
1 parent da24ef6 commit 8e1abd1

File tree

77 files changed

+1591
-447
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1591
-447
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ full changeset diff at the end of each section.
1414

1515
Current Trunk
1616
-------------
17+
- Add support for non-nullable locals in wasm GC.
1718
- Add a new flag to Directize, `--pass-arg=directize-initial-contents-immutable`
1819
which indicates the initial table contents are immutable. That is the case for
1920
LLVM, for example, and it allows us to optimize more indirect calls to direct

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ There are a few differences between Binaryen IR and the WebAssembly language:
117117
`(elem declare func $..)`. Binaryen will emit that data when necessary, but
118118
it does not represent it in IR. That is, IR can be worked on without needing
119119
to think about declaring function references.
120+
* Binaryen IR allows non-nullable locals in the form that the wasm spec does,
121+
(which was historically nicknamed "1a"), in which a `local.get` must be
122+
structurally dominated by a `local.set` in order to validate (that ensures
123+
we do not read the default value of null). Despite being aligned with the
124+
wasm spec, there are some minor details that you may notice:
125+
* A nameless `Block` in Binaryen IR does not interfere with validation.
126+
Nameless blocks are never emitted into the binary format (we just emit
127+
their contents), so we ignore them for purposes of non-nullable locals. As
128+
a result, if you read wasm text emitted by Binaryen then you may see what
129+
seems to be code that should not validate per the spec (and may not
130+
validate in wasm text parsers), but that difference will not exist in the
131+
binary format (binaries emitted by Binaryen will always work everywhere,
132+
aside for bugs of course).
133+
* The Binaryen pass runner will automatically fix up validation after each
134+
pass (finding things that do not validate and fixing them up, usually by
135+
demoting a local to be nullable). As a result you do not need to worry
136+
much about this when writing Binaryen passes. For more details see the
137+
`requiresNonNullableLocalFixups()` hook in `pass.h` and the
138+
`LocalStructuralDominance` class.
120139

121140
As a result, you might notice that round-trip conversions (wasm => Binaryen IR
122141
=> wasm) change code a little in some corner cases.

scripts/test/shared.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ def has_shell_timeout():
262262
'--experimental-wasm-gc',
263263
'--experimental-wasm-typed-funcref',
264264
'--experimental-wasm-memory64',
265-
'--experimental-wasm-extended-const'
265+
'--experimental-wasm-extended-const',
266+
'--experimental-wasm-nn-locals',
266267
]
267268

268269
# external tools

src/ir/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set(ir_SOURCES
1212
possible-contents.cpp
1313
properties.cpp
1414
LocalGraph.cpp
15+
LocalStructuralDominance.cpp
1516
ReFinalize.cpp
1617
stack-utils.cpp
1718
table-utils.cpp
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Copyright 2022 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "ir/iteration.h"
18+
#include "ir/local-structural-dominance.h"
19+
#include "support/small_vector.h"
20+
21+
namespace wasm {
22+
23+
LocalStructuralDominance::LocalStructuralDominance(Function* func,
24+
Module& wasm,
25+
Mode mode) {
26+
if (!wasm.features.hasReferenceTypes()) {
27+
// No references, so nothing to look at.
28+
return;
29+
}
30+
31+
bool hasRefVar = false;
32+
for (auto var : func->vars) {
33+
if (var.isRef()) {
34+
hasRefVar = true;
35+
break;
36+
}
37+
}
38+
if (!hasRefVar) {
39+
return;
40+
}
41+
42+
if (mode == NonNullableOnly) {
43+
bool hasNonNullableVar = false;
44+
for (auto var : func->vars) {
45+
// Check if we have any non-nullable vars at all.
46+
if (var.isNonNullable()) {
47+
hasNonNullableVar = true;
48+
break;
49+
}
50+
}
51+
if (!hasNonNullableVar) {
52+
return;
53+
}
54+
}
55+
56+
struct Scanner : public PostWalker<Scanner> {
57+
std::set<Index>& nonDominatingIndices;
58+
59+
// The locals that have been set, and so at the current time, they
60+
// structurally dominate.
61+
std::vector<bool> localsSet;
62+
63+
Scanner(Function* func, Mode mode, std::set<Index>& nonDominatingIndices)
64+
: nonDominatingIndices(nonDominatingIndices) {
65+
localsSet.resize(func->getNumLocals());
66+
67+
// Parameters always dominate.
68+
for (Index i = 0; i < func->getNumParams(); i++) {
69+
localsSet[i] = true;
70+
}
71+
72+
for (Index i = func->getNumParams(); i < func->getNumLocals(); i++) {
73+
auto type = func->getLocalType(i);
74+
// Mark locals we don't need to care about as "set". We never do any
75+
// work for such a local.
76+
if (!type.isRef() || (mode == NonNullableOnly && type.isNullable())) {
77+
localsSet[i] = true;
78+
}
79+
}
80+
81+
// Note that we do not need to start a scope for the function body.
82+
// Logically there is a scope there, but there is no code after it, so
83+
// there is nothing to clean up when that scope exits, so we may as well
84+
// not even create a scope. Just start walking the body now.
85+
walk(func->body);
86+
}
87+
88+
using Locals = SmallVector<Index, 5>;
89+
90+
// When we exit a control flow scope, we must undo the locals that it set.
91+
std::vector<Locals> cleanupStack;
92+
93+
static void doBeginScope(Scanner* self, Expression** currp) {
94+
self->cleanupStack.emplace_back();
95+
}
96+
97+
static void doEndScope(Scanner* self, Expression** currp) {
98+
for (auto index : self->cleanupStack.back()) {
99+
assert(self->localsSet[index]);
100+
self->localsSet[index] = false;
101+
}
102+
self->cleanupStack.pop_back();
103+
}
104+
105+
static void doLocalSet(Scanner* self, Expression** currp) {
106+
auto index = (*currp)->cast<LocalSet>()->index;
107+
if (!self->localsSet[index]) {
108+
// This local is now set until the end of this scope.
109+
self->localsSet[index] = true;
110+
// If we are not in the topmost scope, note this for later cleanup.
111+
if (!self->cleanupStack.empty()) {
112+
self->cleanupStack.back().push_back(index);
113+
}
114+
}
115+
}
116+
117+
static void scan(Scanner* self, Expression** currp) {
118+
// Use a loop to avoid recursing on the last child - we can just go
119+
// straight into a loop iteration for it.
120+
while (1) {
121+
Expression* curr = *currp;
122+
123+
switch (curr->_id) {
124+
case Expression::Id::InvalidId:
125+
WASM_UNREACHABLE("bad id");
126+
127+
// local.get can just be visited immediately, as it has no children.
128+
case Expression::Id::LocalGetId: {
129+
auto index = curr->cast<LocalGet>()->index;
130+
if (!self->localsSet[index]) {
131+
self->nonDominatingIndices.insert(index);
132+
}
133+
return;
134+
}
135+
case Expression::Id::LocalSetId: {
136+
auto* set = curr->cast<LocalSet>();
137+
if (!self->localsSet[set->index]) {
138+
self->pushTask(doLocalSet, currp);
139+
}
140+
// Immediately continue in the loop.
141+
currp = &set->value;
142+
continue;
143+
}
144+
145+
// Control flow structures.
146+
case Expression::Id::BlockId: {
147+
auto* block = curr->cast<Block>();
148+
// Blocks with no name are never emitted in the binary format, so do
149+
// not create a scope for them.
150+
if (block->name.is()) {
151+
self->pushTask(Scanner::doEndScope, currp);
152+
}
153+
auto& list = block->list;
154+
for (int i = int(list.size()) - 1; i >= 0; i--) {
155+
self->pushTask(Scanner::scan, &list[i]);
156+
}
157+
if (block->name.is()) {
158+
// Just call the task immediately.
159+
doBeginScope(self, currp);
160+
}
161+
return;
162+
}
163+
case Expression::Id::IfId: {
164+
if (curr->cast<If>()->ifFalse) {
165+
self->pushTask(Scanner::doEndScope, currp);
166+
self->maybePushTask(Scanner::scan, &curr->cast<If>()->ifFalse);
167+
self->pushTask(Scanner::doBeginScope, currp);
168+
}
169+
self->pushTask(Scanner::doEndScope, currp);
170+
self->pushTask(Scanner::scan, &curr->cast<If>()->ifTrue);
171+
self->pushTask(Scanner::doBeginScope, currp);
172+
// Immediately continue in the loop.
173+
currp = &curr->cast<If>()->condition;
174+
continue;
175+
}
176+
case Expression::Id::LoopId: {
177+
self->pushTask(Scanner::doEndScope, currp);
178+
// Just call the task immediately.
179+
doBeginScope(self, currp);
180+
// Immediately continue in the loop.
181+
currp = &curr->cast<Loop>()->body;
182+
continue;
183+
}
184+
case Expression::Id::TryId: {
185+
auto& list = curr->cast<Try>()->catchBodies;
186+
for (int i = int(list.size()) - 1; i >= 0; i--) {
187+
self->pushTask(Scanner::doEndScope, currp);
188+
self->pushTask(Scanner::scan, &list[i]);
189+
self->pushTask(Scanner::doBeginScope, currp);
190+
}
191+
self->pushTask(Scanner::doEndScope, currp);
192+
// Just call the task immediately.
193+
doBeginScope(self, currp);
194+
// Immediately continue in the loop.
195+
currp = &curr->cast<Try>()->body;
196+
continue;
197+
}
198+
199+
default: {
200+
// Control flow structures have been handled. This is an expression,
201+
// which we scan normally.
202+
assert(!Properties::isControlFlowStructure(curr));
203+
PostWalker<Scanner>::scan(self, currp);
204+
return;
205+
}
206+
}
207+
}
208+
}
209+
210+
// Only local.set needs to be visited.
211+
void pushTask(TaskFunc func, Expression** currp) {
212+
// Visits to anything but a set can be ignored, so only very specific
213+
// tasks need to actually be pushed here. In particular, we don't want to
214+
// push tasks to call doVisit* when those callbacks do nothing.
215+
if (func == scan || func == doLocalSet || func == doBeginScope ||
216+
func == doEndScope) {
217+
PostWalker<Scanner>::pushTask(func, currp);
218+
}
219+
}
220+
void maybePushTask(TaskFunc func, Expression** currp) {
221+
if (*currp) {
222+
pushTask(func, currp);
223+
}
224+
}
225+
};
226+
227+
Scanner(func, mode, nonDominatingIndices);
228+
}
229+
230+
} // namespace wasm

src/ir/eh-utils.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include "ir/eh-utils.h"
1818
#include "ir/branch-utils.h"
1919
#include "ir/find_all.h"
20-
#include "ir/type-updating.h"
2120

2221
namespace wasm {
2322

@@ -157,9 +156,6 @@ void handleBlockNestedPops(Function* func, Module& wasm) {
157156
for (auto* try_ : trys.list) {
158157
handleBlockNestedPop(try_, func, wasm);
159158
}
160-
// Pops we handled can be of non-defaultable types, so we may have created
161-
// non-nullable type locals. Fix them.
162-
TypeUpdating::handleNonDefaultableLocals(func, wasm);
163159
}
164160

165161
Pop* findPop(Expression* expr) {

src/ir/iteration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ template<class Specific> class AbstractChildIterator {
7676
public:
7777
// The vector of children in the order emitted by wasm-delegations-fields
7878
// (which is in reverse execution order).
79+
// TODO: rename this "reverseChildren"?
7980
SmallVector<Expression**, 4> children;
8081

8182
AbstractChildIterator(Expression* parent) {

src/ir/linear-execution.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> {
4949

5050
switch (curr->_id) {
5151
case Expression::Id::InvalidId:
52-
abort();
52+
WASM_UNREACHABLE("bad id");
5353
case Expression::Id::BlockId: {
5454
self->pushTask(SubType::doVisitBlock, currp);
5555
if (curr->cast<Block>()->name.is()) {

0 commit comments

Comments
 (0)