Skip to content

Commit f826df6

Browse files
authored
[EH] Support reading/writing of delegate (WebAssembly#3561)
This adds support for reading/writing of the new `delegate` instruction in the folded wast format, the stack IR format, the poppy IR format, and the binary format in Binaryen. We don't have a formal spec written down yet, but please refer to WebAssembly/exception-handling#137 and WebAssembly/exception-handling#146 for the informal semantics. In the current version of spec `delegate` is basically a rethrow, but with branch-like immediate argument so that it can bypass other catches/delegates in between. `delegate` is not represented as a new `Expression`, but it is rather an option within a `Try` class, like `catch`/`catch_all`. One special thing about `delegate` is, even though it is written _within_ a `try` in the folded wat format, like ```wasm (try (do ... ) (delegate $l) ) ``` In the unfolded wat format or in the binary format, `delegate` serves as a scope end instruction so there is no separate `end`: ```wasm try ... delegate $l ``` `delegate` semantically targets an outer `catch` or `delegate`, but we write `delegate` target as a `try` label because we only give labels to block-like scoping expressions. So far we have not given `Try` a label and used inner blocks or a wrapping block in case a branch targets the `try`. But in case of `delegate`, it can syntactically only target `try` and if it targets blocks or loops it is a validation failure. So after discussions in WebAssembly#3497, we give `Try` a label but this label can only be targeted by `delegate`s. Unfortunately this makes parsing and writing of `Try` expression somewhat complicated. Also there is one special case; if the immediate argument of `try` is the same as the depth of control flow stack, this means the 'delegate' delegates to the caller. To handle this case this adds a fake label `DELEGATE_CALLER_TARGET`, and when writing it back to the wast format writes it as an immediate value, unlike other cases in which we write labels. This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label and `delegate`'s target. There are many cases that `try` and `delegate`'s labels need to be treated in the same way as block and branch labels, such as for hashing or comparing. But there are routines in which we automatically assume all label uses are branches. I thought about adding a new kind of defines such as `DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some duplication of existing routines or classes. So at the moment this PR chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for `try` and `delegate` labels and makes only necessary amount of changes in branch-utils. We can revisit this decision later if necessary. Many of changes to the existing test cases are because now all `try`s are automatically assigned a label. They will be removed in `RemoveUnusedNames` pass in the same way as block labels if not targeted by any delegates. This only supports reading and writing and has not been tested against any optimization passes yet. --- Original unfolded wat file to generate test/try-delegate.wasm: ```wasm (module (event $e) (func try try delegate 0 catch $e end) (func try try catch $e i32.const 0 drop try delegate 1 end catch $e end ) ) ```
1 parent 8369064 commit f826df6

Some content is hidden

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

42 files changed

+1061
-183
lines changed

src/ir/ExpressionAnalyzer.cpp

+13-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "ir/iteration.h"
1818
#include "ir/load-utils.h"
1919
#include "ir/utils.h"
20+
#include "shared-constants.h"
2021
#include "support/hash.h"
2122
#include "support/small_vector.h"
2223
#include "wasm-traversal.h"
@@ -261,6 +262,9 @@ size_t ExpressionAnalyzer::hash(Expression* curr) {
261262

262263
Hasher(Expression* curr) {
263264
stack.push_back(curr);
265+
// DELEGATE_CALLER_TARGET is a fake target used to denote delegating to
266+
// the caller. Add it here to prevent the unknown name error.
267+
noteScopeName(DELEGATE_CALLER_TARGET);
264268

265269
while (stack.size() > 0) {
266270
curr = stack.back();
@@ -327,13 +331,15 @@ size_t ExpressionAnalyzer::hash(Expression* curr) {
327331
}
328332
}
329333
void visitScopeName(Name curr) {
330-
// Names are relative, we give the same hash for
331-
// (block $x (br $x))
332-
// (block $y (br $y))
333-
static_assert(sizeof(Index) == sizeof(int32_t),
334-
"wasm64 will need changes here");
335-
assert(internalNames.find(curr) != internalNames.end());
336-
rehash(digest, internalNames[curr]);
334+
if (curr.is()) { // try's delegate target can be null
335+
// Names are relative, we give the same hash for
336+
// (block $x (br $x))
337+
// (block $y (br $y))
338+
static_assert(sizeof(Index) == sizeof(int32_t),
339+
"wasm64 will need changes here");
340+
assert(internalNames.find(curr) != internalNames.end());
341+
rehash(digest, internalNames[curr]);
342+
}
337343
}
338344
void visitNonScopeName(Name curr) { rehash(digest, uint64_t(curr.str)); }
339345
void visitType(Type curr) { rehash(digest, curr.getID()); }

src/ir/branch-utils.h

+44-4
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ inline bool isBranchReachable(Expression* expr) {
4040
return true;
4141
}
4242

43-
// Perform a generic operation on uses of scope names (branch targets) in an
44-
// expression. The provided function receives a Name& which it can modify if it
45-
// needs to.
43+
// Perform a generic operation on uses of scope names (branch + delegate
44+
// targets) in an expression. The provided function receives a Name& which it
45+
// can modify if it needs to.
4646
template<typename T> void operateOnScopeNameUses(Expression* expr, T func) {
4747
#define DELEGATE_ID expr->_id
4848

@@ -83,7 +83,7 @@ void operateOnScopeNameUsesAndSentTypes(Expression* expr, T func) {
8383
} else if (auto* br = expr->dynCast<BrOn>()) {
8484
func(name, br->getCastType());
8585
} else {
86-
WASM_UNREACHABLE("bad br type");
86+
assert(expr->is<Try>()); // delegate
8787
}
8888
});
8989
}
@@ -135,6 +135,46 @@ inline bool replacePossibleTarget(Expression* branch, Name from, Name to) {
135135
return worked;
136136
}
137137

138+
// Replace all delegate targets within the given AST.
139+
inline void replaceDelegateTargets(Expression* ast, Name from, Name to) {
140+
struct Replacer
141+
: public PostWalker<Replacer, UnifiedExpressionVisitor<Replacer>> {
142+
Name from, to;
143+
Replacer(Name from, Name to) : from(from), to(to) {}
144+
void visitExpression(Expression* curr) {
145+
if (curr->is<Try>()) {
146+
operateOnScopeNameUses(curr, [&](Name& name) {
147+
if (name == from) {
148+
name = to;
149+
}
150+
});
151+
}
152+
}
153+
};
154+
Replacer replacer(from, to);
155+
replacer.walk(ast);
156+
}
157+
158+
// Replace all branch targets within the given AST.
159+
inline void replaceBranchTargets(Expression* ast, Name from, Name to) {
160+
struct Replacer
161+
: public PostWalker<Replacer, UnifiedExpressionVisitor<Replacer>> {
162+
Name from, to;
163+
Replacer(Name from, Name to) : from(from), to(to) {}
164+
void visitExpression(Expression* curr) {
165+
if (Properties::isBranch(curr)) {
166+
operateOnScopeNameUses(curr, [&](Name& name) {
167+
if (name == from) {
168+
name = to;
169+
}
170+
});
171+
}
172+
}
173+
};
174+
Replacer replacer(from, to);
175+
replacer.walk(ast);
176+
}
177+
138178
// Returns the set of targets to which we branch that are
139179
// outside of an expression.
140180
inline NameSet getExitingBranches(Expression* ast) {

src/ir/properties.h

+8-2
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ inline bool isControlFlowStructure(Expression* curr) {
6969
curr->is<Try>();
7070
}
7171

72-
// Check if an expression is a control flow construct with a name,
73-
// which implies it may have breaks to it.
72+
// Check if an expression is a control flow construct with a name, which implies
73+
// it may have breaks or delegates to it.
7474
inline bool isNamedControlFlow(Expression* curr) {
7575
if (auto* block = curr->dynCast<Block>()) {
7676
return block->name.is();
7777
} else if (auto* loop = curr->dynCast<Loop>()) {
7878
return loop->name.is();
79+
} else if (auto* try_ = curr->dynCast<Try>()) {
80+
return try_->name.is();
7981
}
8082
return false;
8183
}
@@ -104,6 +106,10 @@ inline bool isConstantExpression(const Expression* curr) {
104106
return false;
105107
}
106108

109+
inline bool isBranch(const Expression* curr) {
110+
return curr->is<Break>() || curr->is<Switch>() || curr->is<BrOn>();
111+
}
112+
107113
inline Literal getLiteral(const Expression* curr) {
108114
if (auto* c = curr->dynCast<Const>()) {
109115
return c->value;

src/passes/Poppify.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ struct Poppifier : BinaryenIRWriter<Poppifier> {
128128
void emitIfElse(If* curr);
129129
void emitCatch(Try* curr, Index i);
130130
void emitCatchAll(Try* curr);
131+
void emitDelegate(Try* curr);
131132
void emitScopeEnd(Expression* curr);
132133
void emitFunctionEnd();
133134
void emitUnreachable();
@@ -272,6 +273,13 @@ void Poppifier::emitCatchAll(Try* curr) {
272273
scopeStack.emplace_back(Scope::Catch);
273274
}
274275

276+
void Poppifier::emitDelegate(Try* curr) {
277+
auto& scope = scopeStack.back();
278+
assert(scope.kind == Scope::Try);
279+
patchScope(curr->body);
280+
scopeStack.back().instrs.push_back(curr);
281+
}
282+
275283
void Poppifier::emitScopeEnd(Expression* curr) {
276284
switch (scopeStack.back().kind) {
277285
case Scope::Block:

src/passes/Print.cpp

+72-5
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,10 @@ struct PrintExpressionContents
17641764
void visitRefEq(RefEq* curr) { printMedium(o, "ref.eq"); }
17651765
void visitTry(Try* curr) {
17661766
printMedium(o, "try");
1767+
if (curr->name.is()) {
1768+
o << ' ';
1769+
printName(curr->name, o);
1770+
}
17671771
if (curr->type.isConcrete()) {
17681772
o << ' ' << ResultType(curr->type);
17691773
}
@@ -1955,6 +1959,9 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
19551959
Function::DebugLocation lastPrintedLocation;
19561960
bool debugInfo;
19571961

1962+
// Used to print delegate's depth argument when it throws to the caller
1963+
int controlFlowDepth = 0;
1964+
19581965
PrintSExpression(std::ostream& o) : o(o) {
19591966
setMinify(false);
19601967
if (!full) {
@@ -2100,6 +2107,9 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21002107
break; // that's all we can recurse, start to unwind
21012108
}
21022109
}
2110+
2111+
int startControlFlowDepth = controlFlowDepth;
2112+
controlFlowDepth += stack.size();
21032113
auto* top = stack.back();
21042114
while (stack.size() > 0) {
21052115
curr = stack.back();
@@ -2129,8 +2139,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21292139
o << ' ' << curr->name;
21302140
}
21312141
}
2142+
controlFlowDepth = startControlFlowDepth;
21322143
}
21332144
void visitIf(If* curr) {
2145+
controlFlowDepth++;
21342146
o << '(';
21352147
printExpressionContents(curr);
21362148
incIndent();
@@ -2147,8 +2159,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21472159
if (full) {
21482160
o << " ;; end if";
21492161
}
2162+
controlFlowDepth--;
21502163
}
21512164
void visitLoop(Loop* curr) {
2165+
controlFlowDepth++;
21522166
o << '(';
21532167
printExpressionContents(curr);
21542168
incIndent();
@@ -2160,6 +2174,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21602174
o << ' ' << curr->name;
21612175
}
21622176
}
2177+
controlFlowDepth--;
21632178
}
21642179
void visitBreak(Break* curr) {
21652180
o << '(';
@@ -2490,13 +2505,28 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
24902505
// (do
24912506
// ...
24922507
// )
2493-
// (catch
2494-
// ...
2508+
// (catch $e
2509+
// ...
2510+
// )
2511+
// ...
2512+
// (catch_all
2513+
// ...
24952514
// )
24962515
// )
2497-
// The parenthesis wrapping 'catch' is just a syntax and does not affect
2498-
// nested depths of instructions within.
2516+
// The parenthesis wrapping do/catch/catch_all is just a syntax and does not
2517+
// affect nested depths of instructions within.
2518+
//
2519+
// try-delegate is written in the forded format as
2520+
// (try
2521+
// (do
2522+
// ...
2523+
// )
2524+
// (delegate $label)
2525+
// )
2526+
// When the 'delegate' delegates to the caller, we write the argument as an
2527+
// immediate.
24992528
void visitTry(Try* curr) {
2529+
controlFlowDepth++;
25002530
o << '(';
25012531
printExpressionContents(curr);
25022532
incIndent();
@@ -2521,12 +2551,26 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
25212551
if (curr->hasCatchAll()) {
25222552
doIndent(o, indent);
25232553
printDebugDelimiterLocation(curr, curr->catchEvents.size());
2524-
o << "(catch_all";
2554+
o << '(';
2555+
printMedium(o, "catch_all");
25252556
incIndent();
25262557
maybePrintImplicitBlock(curr->catchBodies.back(), true);
25272558
decIndent();
25282559
o << "\n";
25292560
}
2561+
controlFlowDepth--;
2562+
2563+
if (curr->isDelegate()) {
2564+
doIndent(o, indent);
2565+
o << '(';
2566+
printMedium(o, "delegate ");
2567+
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
2568+
o << controlFlowDepth;
2569+
} else {
2570+
printName(curr->delegateTarget, o);
2571+
}
2572+
o << ")\n";
2573+
}
25302574
decIndent();
25312575
if (full) {
25322576
o << " ;; end try";
@@ -2913,6 +2957,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
29132957
} else {
29142958
printFullLine(curr->body);
29152959
}
2960+
assert(controlFlowDepth == 0);
29162961
} else {
29172962
// Print the stack IR.
29182963
printStackIR(curr->stackIR.get(), o, curr);
@@ -3324,6 +3369,11 @@ printStackInst(StackInst* inst, std::ostream& o, Function* func) {
33243369
printMedium(o, "catch_all");
33253370
break;
33263371
}
3372+
case StackInst::Delegate: {
3373+
printMedium(o, "delegate ");
3374+
printName(inst->origin->cast<Try>()->delegateTarget, o);
3375+
break;
3376+
}
33273377
default:
33283378
WASM_UNREACHABLE("unexpeted op");
33293379
}
@@ -3339,6 +3389,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33393389
}
33403390
};
33413391

3392+
int controlFlowDepth = 0;
33423393
// Stack to track indices of catches within a try
33433394
SmallVector<Index, 4> catchIndexStack;
33443395
for (Index i = 0; i < (*ir).size(); i++) {
@@ -3364,6 +3415,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33643415
case StackInst::BlockBegin:
33653416
case StackInst::IfBegin:
33663417
case StackInst::LoopBegin: {
3418+
controlFlowDepth++;
33673419
doIndent();
33683420
PrintExpressionContents(func, o).visit(inst->origin);
33693421
indent++;
@@ -3375,6 +3427,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33753427
case StackInst::BlockEnd:
33763428
case StackInst::IfEnd:
33773429
case StackInst::LoopEnd: {
3430+
controlFlowDepth--;
33783431
indent--;
33793432
doIndent();
33803433
printMedium(o, "end");
@@ -3403,11 +3456,25 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
34033456
indent++;
34043457
break;
34053458
}
3459+
case StackInst::Delegate: {
3460+
controlFlowDepth--;
3461+
indent--;
3462+
doIndent();
3463+
printMedium(o, "delegate ");
3464+
Try* curr = inst->origin->cast<Try>();
3465+
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
3466+
o << controlFlowDepth;
3467+
} else {
3468+
printName(curr->delegateTarget, o);
3469+
}
3470+
break;
3471+
}
34063472
default:
34073473
WASM_UNREACHABLE("unexpeted op");
34083474
}
34093475
std::cout << '\n';
34103476
}
3477+
assert(controlFlowDepth == 0);
34113478
return o;
34123479
}
34133480

src/passes/RemoveUnusedNames.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ struct RemoveUnusedNames
8383
}
8484
}
8585

86+
void visitTry(Try* curr) { handleBreakTarget(curr->name); }
87+
8688
void visitFunction(Function* curr) { assert(branchesSeen.empty()); }
8789
};
8890

src/passes/StackIR.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class StackIROptimizer {
258258
case StackInst::LoopEnd:
259259
case StackInst::Catch:
260260
case StackInst::CatchAll:
261+
case StackInst::Delegate:
261262
case StackInst::TryEnd: {
262263
return true;
263264
}
@@ -284,7 +285,8 @@ class StackIROptimizer {
284285
case StackInst::BlockEnd:
285286
case StackInst::IfEnd:
286287
case StackInst::LoopEnd:
287-
case StackInst::TryEnd: {
288+
case StackInst::TryEnd:
289+
case StackInst::Delegate: {
288290
return true;
289291
}
290292
default: { return false; }

src/shared-constants.h

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ extern Name CASE;
6060
extern Name BR;
6161
extern Name FUNCREF;
6262
extern Name FAKE_RETURN;
63+
extern Name DELEGATE_CALLER_TARGET;
6364
extern Name MUT;
6465
extern Name SPECTEST;
6566
extern Name PRINT;

0 commit comments

Comments
 (0)