Skip to content

Commit 7e53511

Browse files
committed
[EH] Support reading/writing of delegate
This adds support for reading/writing of the new 'delegate' instruction in the folded wast format, the stack IR format, and the binary format in Binaryen. We don't have a format 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 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 has 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 #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.
1 parent ed9d1a1 commit 7e53511

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

+1048
-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

+71-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,8 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
19551959
Function::DebugLocation lastPrintedLocation;
19561960
bool debugInfo;
19571961

1962+
int controlFlowDepth = 0;
1963+
19581964
PrintSExpression(std::ostream& o) : o(o) {
19591965
setMinify(false);
19601966
if (!full) {
@@ -2100,6 +2106,9 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21002106
break; // that's all we can recurse, start to unwind
21012107
}
21022108
}
2109+
2110+
int startControlFlowDepth = controlFlowDepth;
2111+
controlFlowDepth += stack.size();
21032112
auto* top = stack.back();
21042113
while (stack.size() > 0) {
21052114
curr = stack.back();
@@ -2129,8 +2138,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21292138
o << ' ' << curr->name;
21302139
}
21312140
}
2141+
controlFlowDepth = startControlFlowDepth;
21322142
}
21332143
void visitIf(If* curr) {
2144+
controlFlowDepth++;
21342145
o << '(';
21352146
printExpressionContents(curr);
21362147
incIndent();
@@ -2147,8 +2158,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21472158
if (full) {
21482159
o << " ;; end if";
21492160
}
2161+
controlFlowDepth--;
21502162
}
21512163
void visitLoop(Loop* curr) {
2164+
controlFlowDepth++;
21522165
o << '(';
21532166
printExpressionContents(curr);
21542167
incIndent();
@@ -2160,6 +2173,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
21602173
o << ' ' << curr->name;
21612174
}
21622175
}
2176+
controlFlowDepth--;
21632177
}
21642178
void visitBreak(Break* curr) {
21652179
o << '(';
@@ -2490,13 +2504,28 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
24902504
// (do
24912505
// ...
24922506
// )
2493-
// (catch
2494-
// ...
2507+
// (catch $e
2508+
// ...
2509+
// )
2510+
// ...
2511+
// (catch_all
2512+
// ...
24952513
// )
24962514
// )
2497-
// The parenthesis wrapping 'catch' is just a syntax and does not affect
2498-
// nested depths of instructions within.
2515+
// The parenthesis wrapping do/catch/catch_all is just a syntax and does not
2516+
// affect nested depths of instructions within.
2517+
//
2518+
// try-delegate is written in the forded format as
2519+
// (try
2520+
// (do
2521+
// ...
2522+
// )
2523+
// (delegate $label)
2524+
// )
2525+
// When the 'delegate' delegates to the caller, we write the argument as an
2526+
// immediate.
24992527
void visitTry(Try* curr) {
2528+
controlFlowDepth++;
25002529
o << '(';
25012530
printExpressionContents(curr);
25022531
incIndent();
@@ -2521,12 +2550,26 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
25212550
if (curr->hasCatchAll()) {
25222551
doIndent(o, indent);
25232552
printDebugDelimiterLocation(curr, curr->catchEvents.size());
2524-
o << "(catch_all";
2553+
o << '(';
2554+
printMedium(o, "catch_all");
25252555
incIndent();
25262556
maybePrintImplicitBlock(curr->catchBodies.back(), true);
25272557
decIndent();
25282558
o << "\n";
25292559
}
2560+
controlFlowDepth--;
2561+
2562+
if (curr->isDelegate()) {
2563+
doIndent(o, indent);
2564+
o << '(';
2565+
printMedium(o, "delegate ");
2566+
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
2567+
o << controlFlowDepth;
2568+
} else {
2569+
printName(curr->delegateTarget, o);
2570+
}
2571+
o << ")\n";
2572+
}
25302573
decIndent();
25312574
if (full) {
25322575
o << " ;; end try";
@@ -2913,6 +2956,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
29132956
} else {
29142957
printFullLine(curr->body);
29152958
}
2959+
assert(controlFlowDepth == 0);
29162960
} else {
29172961
// Print the stack IR.
29182962
printStackIR(curr->stackIR.get(), o, curr);
@@ -3324,6 +3368,11 @@ printStackInst(StackInst* inst, std::ostream& o, Function* func) {
33243368
printMedium(o, "catch_all");
33253369
break;
33263370
}
3371+
case StackInst::Delegate: {
3372+
printMedium(o, "delegate ");
3373+
printName(inst->origin->cast<Try>()->delegateTarget, o);
3374+
break;
3375+
}
33273376
default:
33283377
WASM_UNREACHABLE("unexpeted op");
33293378
}
@@ -3339,6 +3388,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33393388
}
33403389
};
33413390

3391+
int controlFlowDepth = 0;
33423392
// Stack to track indices of catches within a try
33433393
SmallVector<Index, 4> catchIndexStack;
33443394
for (Index i = 0; i < (*ir).size(); i++) {
@@ -3364,6 +3414,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33643414
case StackInst::BlockBegin:
33653415
case StackInst::IfBegin:
33663416
case StackInst::LoopBegin: {
3417+
controlFlowDepth++;
33673418
doIndent();
33683419
PrintExpressionContents(func, o).visit(inst->origin);
33693420
indent++;
@@ -3375,6 +3426,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
33753426
case StackInst::BlockEnd:
33763427
case StackInst::IfEnd:
33773428
case StackInst::LoopEnd: {
3429+
controlFlowDepth--;
33783430
indent--;
33793431
doIndent();
33803432
printMedium(o, "end");
@@ -3403,11 +3455,25 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
34033455
indent++;
34043456
break;
34053457
}
3458+
case StackInst::Delegate: {
3459+
controlFlowDepth--;
3460+
indent--;
3461+
doIndent();
3462+
printMedium(o, "delegate ");
3463+
Try* curr = inst->origin->cast<Try>();
3464+
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
3465+
o << controlFlowDepth;
3466+
} else {
3467+
printName(curr->delegateTarget, o);
3468+
}
3469+
break;
3470+
}
34063471
default:
34073472
WASM_UNREACHABLE("unexpeted op");
34083473
}
34093474
std::cout << '\n';
34103475
}
3476+
assert(controlFlowDepth == 0);
34113477
return o;
34123478
}
34133479

src/passes/RemoveUnusedNames.cpp

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

86+
void visitTry(Try* curr) {
87+
handleBreakTarget(curr->name);
88+
}
89+
8690
void visitFunction(Function* curr) { assert(branchesSeen.empty()); }
8791
};
8892

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)