Skip to content

Commit

Permalink
[EH] Support reading/writing of delegate
Browse files Browse the repository at this point in the history
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 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.
  • Loading branch information
aheejin committed Feb 8, 2021
1 parent 51c8f24 commit 0ea76ae
Show file tree
Hide file tree
Showing 38 changed files with 980 additions and 182 deletions.
20 changes: 13 additions & 7 deletions src/ir/ExpressionAnalyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "ir/iteration.h"
#include "ir/load-utils.h"
#include "ir/utils.h"
#include "shared-constants.h"
#include "support/hash.h"
#include "support/small_vector.h"
#include "wasm-traversal.h"
Expand Down Expand Up @@ -261,6 +262,9 @@ size_t ExpressionAnalyzer::hash(Expression* curr) {

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

while (stack.size() > 0) {
curr = stack.back();
Expand Down Expand Up @@ -327,13 +331,15 @@ size_t ExpressionAnalyzer::hash(Expression* curr) {
}
}
void visitScopeName(Name curr) {
// Names are relative, we give the same hash for
// (block $x (br $x))
// (block $y (br $y))
static_assert(sizeof(Index) == sizeof(int32_t),
"wasm64 will need changes here");
assert(internalNames.find(curr) != internalNames.end());
rehash(digest, internalNames[curr]);
if (curr.is()) { // try's delegate target can be null
// Names are relative, we give the same hash for
// (block $x (br $x))
// (block $y (br $y))
static_assert(sizeof(Index) == sizeof(int32_t),
"wasm64 will need changes here");
assert(internalNames.find(curr) != internalNames.end());
rehash(digest, internalNames[curr]);
}
}
void visitNonScopeName(Name curr) { rehash(digest, uint64_t(curr.str)); }
void visitType(Type curr) { rehash(digest, curr.getID()); }
Expand Down
48 changes: 44 additions & 4 deletions src/ir/branch-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ inline bool isBranchReachable(Expression* expr) {
return true;
}

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

Expand Down Expand Up @@ -83,7 +83,7 @@ void operateOnScopeNameUsesAndSentTypes(Expression* expr, T func) {
} else if (auto* br = expr->dynCast<BrOn>()) {
func(name, br->getCastType());
} else {
WASM_UNREACHABLE("bad br type");
assert(expr->is<Try>()); // delegate
}
});
}
Expand Down Expand Up @@ -135,6 +135,46 @@ inline bool replacePossibleTarget(Expression* branch, Name from, Name to) {
return worked;
}

// Replace all delegate targets within the given AST.
inline void replaceDelegateTargets(Expression* ast, Name from, Name to) {
struct Replacer
: public PostWalker<Replacer, UnifiedExpressionVisitor<Replacer>> {
Name from, to;
Replacer(Name from, Name to) : from(from), to(to) {}
void visitExpression(Expression* curr) {
if (curr->is<Try>()) {
operateOnScopeNameUses(curr, [&](Name& name) {
if (name == from) {
name = to;
}
});
}
}
};
Replacer replacer(from, to);
replacer.walk(ast);
}

// Replace all branch targets within the given AST.
inline void replaceBranchTargets(Expression* ast, Name from, Name to) {
struct Replacer
: public PostWalker<Replacer, UnifiedExpressionVisitor<Replacer>> {
Name from, to;
Replacer(Name from, Name to) : from(from), to(to) {}
void visitExpression(Expression* curr) {
if (Properties::isBranch(curr)) {
operateOnScopeNameUses(curr, [&](Name& name) {
if (name == from) {
name = to;
}
});
}
}
};
Replacer replacer(from, to);
replacer.walk(ast);
}

// Returns the set of targets to which we branch that are
// outside of an expression.
inline NameSet getExitingBranches(Expression* ast) {
Expand Down
10 changes: 8 additions & 2 deletions src/ir/properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ inline bool isControlFlowStructure(Expression* curr) {
curr->is<Try>();
}

// Check if an expression is a control flow construct with a name,
// which implies it may have breaks to it.
// Check if an expression is a control flow construct with a name, which implies
// it may have breaks or delegates to it.
inline bool isNamedControlFlow(Expression* curr) {
if (auto* block = curr->dynCast<Block>()) {
return block->name.is();
} else if (auto* loop = curr->dynCast<Loop>()) {
return loop->name.is();
} else if (auto* try_ = curr->dynCast<Try>()) {
return try_->name.is();
}
return false;
}
Expand Down Expand Up @@ -104,6 +106,10 @@ inline bool isConstantExpression(const Expression* curr) {
return false;
}

inline bool isBranch(const Expression* curr) {
return curr->is<Break>() || curr->is<Switch>() || curr->is<BrOn>();
}

inline Literal getLiteral(const Expression* curr) {
if (auto* c = curr->dynCast<Const>()) {
return c->value;
Expand Down
76 changes: 71 additions & 5 deletions src/passes/Print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,10 @@ struct PrintExpressionContents
void visitRefEq(RefEq* curr) { printMedium(o, "ref.eq"); }
void visitTry(Try* curr) {
printMedium(o, "try");
if (curr->name.is()) {
o << ' ';
printName(curr->name, o);
}
if (curr->type.isConcrete()) {
o << ' ' << ResultType(curr->type);
}
Expand Down Expand Up @@ -1947,6 +1951,8 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
Function::DebugLocation lastPrintedLocation;
bool debugInfo;

int controlFlowDepth = 0;

PrintSExpression(std::ostream& o) : o(o) {
setMinify(false);
if (!full) {
Expand Down Expand Up @@ -2092,6 +2098,9 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
break; // that's all we can recurse, start to unwind
}
}

int startControlFlowDepth = controlFlowDepth;
controlFlowDepth += stack.size();
auto* top = stack.back();
while (stack.size() > 0) {
curr = stack.back();
Expand Down Expand Up @@ -2121,8 +2130,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
o << ' ' << curr->name;
}
}
controlFlowDepth = startControlFlowDepth;
}
void visitIf(If* curr) {
controlFlowDepth++;
o << '(';
printExpressionContents(curr);
incIndent();
Expand All @@ -2139,8 +2150,10 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
if (full) {
o << " ;; end if";
}
controlFlowDepth--;
}
void visitLoop(Loop* curr) {
controlFlowDepth++;
o << '(';
printExpressionContents(curr);
incIndent();
Expand All @@ -2152,6 +2165,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
o << ' ' << curr->name;
}
}
controlFlowDepth--;
}
void visitBreak(Break* curr) {
o << '(';
Expand Down Expand Up @@ -2482,13 +2496,28 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
// (do
// ...
// )
// (catch
// ...
// (catch $e
// ...
// )
// ...
// (catch_all
// ...
// )
// )
// The parenthesis wrapping 'catch' is just a syntax and does not affect
// nested depths of instructions within.
// The parenthesis wrapping do/catch/catch_all is just a syntax and does not
// affect nested depths of instructions within.
//
// try-delegate is written in the forded format as
// (try
// (do
// ...
// )
// (delegate $label)
// )
// When the 'delegate' delegates to the caller, we write the argument as an
// immediate.
void visitTry(Try* curr) {
controlFlowDepth++;
o << '(';
printExpressionContents(curr);
incIndent();
Expand All @@ -2513,12 +2542,26 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
if (curr->hasCatchAll()) {
doIndent(o, indent);
printDebugDelimiterLocation(curr, curr->catchEvents.size());
o << "(catch_all";
o << '(';
printMedium(o, "catch_all");
incIndent();
maybePrintImplicitBlock(curr->catchBodies.back(), true);
decIndent();
o << "\n";
}
controlFlowDepth--;

if (curr->isDelegate()) {
doIndent(o, indent);
o << '(';
printMedium(o, "delegate ");
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
o << controlFlowDepth;
} else {
printName(curr->delegateTarget, o);
}
o << ")\n";
}
decIndent();
if (full) {
o << " ;; end try";
Expand Down Expand Up @@ -2905,6 +2948,7 @@ struct PrintSExpression : public OverriddenVisitor<PrintSExpression> {
} else {
printFullLine(curr->body);
}
assert(controlFlowDepth == 0);
} else {
// Print the stack IR.
printStackIR(curr->stackIR.get(), o, curr);
Expand Down Expand Up @@ -3304,6 +3348,11 @@ printStackInst(StackInst* inst, std::ostream& o, Function* func) {
printMedium(o, "catch_all");
break;
}
case StackInst::Delegate: {
printMedium(o, "delegate ");
printName(inst->origin->cast<Try>()->delegateTarget, o);
break;
}
default:
WASM_UNREACHABLE("unexpeted op");
}
Expand All @@ -3319,6 +3368,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
}
};

int controlFlowDepth = 0;
// Stack to track indices of catches within a try
SmallVector<Index, 4> catchIndexStack;
for (Index i = 0; i < (*ir).size(); i++) {
Expand All @@ -3343,6 +3393,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
case StackInst::BlockBegin:
case StackInst::IfBegin:
case StackInst::LoopBegin: {
controlFlowDepth++;
doIndent();
PrintExpressionContents(func, o).visit(inst->origin);
indent++;
Expand All @@ -3354,6 +3405,7 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
case StackInst::BlockEnd:
case StackInst::IfEnd:
case StackInst::LoopEnd: {
controlFlowDepth--;
indent--;
doIndent();
printMedium(o, "end");
Expand Down Expand Up @@ -3382,11 +3434,25 @@ printStackIR(StackIR* ir, std::ostream& o, Function* func) {
indent++;
break;
}
case StackInst::Delegate: {
controlFlowDepth--;
indent--;
doIndent();
printMedium(o, "delegate ");
Try* curr = inst->origin->cast<Try>();
if (curr->delegateTarget == DELEGATE_CALLER_TARGET) {
o << controlFlowDepth;
} else {
printName(curr->delegateTarget, o);
}
break;
}
default:
WASM_UNREACHABLE("unexpeted op");
}
std::cout << '\n';
}
assert(controlFlowDepth == 0);
return o;
}

Expand Down
4 changes: 4 additions & 0 deletions src/passes/RemoveUnusedNames.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ struct RemoveUnusedNames
}
}

void visitTry(Try* curr) {
handleBreakTarget(curr->name);
}

void visitFunction(Function* curr) { assert(branchesSeen.empty()); }
};

Expand Down
4 changes: 3 additions & 1 deletion src/passes/StackIR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ class StackIROptimizer {
case StackInst::LoopEnd:
case StackInst::Catch:
case StackInst::CatchAll:
case StackInst::Delegate:
case StackInst::TryEnd: {
return true;
}
Expand All @@ -284,7 +285,8 @@ class StackIROptimizer {
case StackInst::BlockEnd:
case StackInst::IfEnd:
case StackInst::LoopEnd:
case StackInst::TryEnd: {
case StackInst::TryEnd:
case StackInst::Delegate: {
return true;
}
default: { return false; }
Expand Down
1 change: 1 addition & 0 deletions src/shared-constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ extern Name CASE;
extern Name BR;
extern Name FUNCREF;
extern Name FAKE_RETURN;
extern Name DELEGATE_CALLER_TARGET;
extern Name MUT;
extern Name SPECTEST;
extern Name PRINT;
Expand Down
4 changes: 4 additions & 0 deletions src/wasm-binary.h
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ enum ASTNodes {
Try = 0x06,
Catch = 0x07,
CatchAll = 0x05,
Delegate = 0x18,
Throw = 0x08,
Rethrow = 0x09,

Expand Down Expand Up @@ -1385,6 +1386,8 @@ class WasmBinaryBuilder {
// the names that breaks target. this lets us know if a block has breaks to it
// or not.
std::unordered_set<Name> breakTargetNames;
// the names that delegates target.
std::unordered_set<Name> delegateTargetNames;

std::vector<Expression*> expressionStack;

Expand Down Expand Up @@ -1488,6 +1491,7 @@ class WasmBinaryBuilder {
Expression* getBlockOrSingleton(Type type);

BreakTarget getBreakTarget(int32_t offset);
Name getDelegateTargetName(int32_t offset);

void readMemoryAccess(Address& alignment, Address& offset);

Expand Down
Loading

0 comments on commit 0ea76ae

Please sign in to comment.