blob: a3411cda967b1a5a9a0b8ab09cd9eeaed9619d40 [file] [log] [blame] [edit]
/*
* Copyright 2024 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// TranslateToExnref translates the old Phase 3 EH instructions, which include
// try, catch, catch_all, delegate, and rethrow, into the new EH instructions,
// which include try_table (with catch / catch_ref / catch_all / catch_all_ref)
// and throw_ref, passed at the Oct 2023 CG meeting. This translator can be used
// as a standalone tool by users of the previous EH toolchain to generate
// binaries for the new spec without recompiling, and also can be used at the
// end of the Binaryen pipeline to produce binaries for the new spec while the
// end-to-end toolchain implementation for the new spec is in progress.
//
#include <ir/drop.h>
#include <ir/find_all.h>
#include <ir/label-utils.h>
#include <ir/utils.h>
#include <pass.h>
#include <wasm.h>
namespace wasm {
namespace {
// Translates the old EH instructions (try / catch / catch_all / delegate /
// rethrow) into the new ones (try_table (+ catch / catch_ref / catch_all /
// catch_all_ref) / throw_ref).
struct TranslateToExnref : public WalkerPass<PostWalker<TranslateToExnref>> {
bool isFunctionParallel() override { return true; }
// Scans and records which try labels are targeted by delegates and rethrows.
struct TargetTryLabelScanner : public PostWalker<TargetTryLabelScanner> {
TargetTryLabelScanner(Function* func) { walkFunction(func); }
std::set<Name> delegateTargets;
std::set<Name> rethrowTargets;
bool isTargetedByDelegates(Try* curr) const {
return delegateTargets.find(curr->name) != delegateTargets.end();
}
bool isTargetedByRethrows(Try* curr) const {
return rethrowTargets.find(curr->name) != rethrowTargets.end();
}
void visitTry(Try* curr) {
if (curr->isDelegate()) {
delegateTargets.insert(curr->delegateTarget);
}
}
void visitRethrow(Rethrow* curr) { rethrowTargets.insert(curr->target); }
};
// For each try label targeted by rethrows, assign a local that will contain
// its exnref in the new spec (via try + catch(_all)_ref instruction), so that
// every 'rethrow $somelabel' can later be translated into
// 'throw_ref $somelocal'.
//
// In the examples below, try's (do) bodies are omitted for the sake of
// simplicity.
//
// For example,
// (try $l0
// (catch
// (try $l1
// (catch
// (rethrow $l0) ;; will become (throw_ref $local0)
// )
// (catch
// (rethrow $l1) ;; will become (throw_ref $local1)
// )
// )
// )
// )
// In this case, we need two locals for exnrefs, each for $l0 and $l1.
//
// Note that the number of locals required is the maximum depth of try-catch
// nests, when only counting 'try's that are targeted by rethrows, which means
// the pattern below only needs one extra local:
// (try $l0
// (catch
// (rethrow $l0)
// )
// )
// (try $l1
// (catch
// (rethrow $l1)
// )
// )
// Because the two trys are not nested within each other, one local can be
// reused in both trys.
//
// Also even if there is a try-catch nest with depth N, if only some of them
// are targeted by rethrows, we only need that many extra locals for the whole
// nest:
// (try $l0
// (catch
// (try $l1
// (catch
// ...
// ...
// (try $lN
// (catch
// ;; If this is the only rethrow in this nest, we only need one
// ;; extra exnref local for all N try-catches
// (rethrow $l1)
// )
// )
// ...
// ...
// )
// )
// )
// )
struct ExnrefLocalAssigner : public PostWalker<ExnrefLocalAssigner> {
TargetTryLabelScanner* labelScanner = nullptr;
std::vector<Index> exnrefLocals;
std::unordered_map<Name, Index> rethrowTargetToExnrefLocal;
// Depth of the current try nest, when only counting trys targeted with
// rethrows.
size_t rethrowTryDepth = 0;
ExnrefLocalAssigner(Function* func, TargetTryLabelScanner* labelScanner)
: labelScanner(labelScanner) {
walkFunction(func);
}
std::optional<Index> getExnrefLocal(Name rethrowTarget) const {
auto it = rethrowTargetToExnrefLocal.find(rethrowTarget);
if (it == rethrowTargetToExnrefLocal.end()) {
return {};
} else {
return it->second;
}
}
static void incrementRethrowTryDepth(ExnrefLocalAssigner* self,
Expression** currp) {
self->rethrowTryDepth++;
}
static void decrementRethrowTryDepth(ExnrefLocalAssigner* self,
Expression** currp) {
self->rethrowTryDepth--;
}
static void scan(ExnrefLocalAssigner* self, Expression** currp) {
auto* curr = *currp;
if (auto* tryy = curr->dynCast<Try>()) {
if (self->labelScanner->isTargetedByRethrows(tryy)) {
self->pushTask(decrementRethrowTryDepth, currp);
}
}
PostWalker<ExnrefLocalAssigner>::scan(self, currp);
if (auto* tryy = curr->dynCast<Try>()) {
if (self->labelScanner->isTargetedByRethrows(tryy)) {
self->pushTask(incrementRethrowTryDepth, currp);
}
}
}
void visitTry(Try* curr) {
if (labelScanner->isTargetedByRethrows(curr)) {
while (exnrefLocals.size() < rethrowTryDepth) {
exnrefLocals.push_back(
Builder::addVar(getFunction(), Type(HeapType::exn, Nullable)));
}
rethrowTargetToExnrefLocal[curr->name] =
exnrefLocals[rethrowTryDepth - 1];
}
}
};
std::optional<LabelUtils::LabelManager> labels;
std::optional<TargetTryLabelScanner> labelScanner;
std::optional<ExnrefLocalAssigner> localAssigner;
std::unordered_map<Name, Name> delegateTargetToTrampoline;
// Scratch locals used to contain extracted values and (extracted values,
// exnref) tuples for a short time.
std::unordered_map<Type, Index> typeToScratchLocal;
std::unique_ptr<Pass> create() override {
return std::make_unique<TranslateToExnref>();
}
// Get a scratch local for a given type. These locals are used to contain
// extracted value(s) and (extracted value(s), exnref) tuples for a short
// time. Because these locals are written and read right after that, we can
// reuse these for all extracted values and (extracted values, exnref) tuples
// as long as the type matches.
Index getScratchLocal(Type type) {
auto [it, inserted] = typeToScratchLocal.insert({type, 0});
if (inserted) {
it->second = Builder::addVar(getFunction(), type);
}
return it->second;
}
// Process try labels targeted by rethrows. This does NOT transform the
// current 'try' into 'try_table' yet; it only adds block, br, and throw_ref
// instructions to complete the conversions of inner try~delegates that target
// the current try.
void processDelegateTarget(Try* curr,
Block* outerBlock,
bool& outerBlockUsedSoFar) {
Builder builder(*getModule());
// Convert
//
// (try $delegate_target (result sometype)
// (do
// ...
// ;; This had originally been an inner try~delegate and has been
// ;; already translated to try_table at this point. See
// ;; processDelegate() for how it is done.
// (try_table (catch_all_ref $delegate_trampoline)
// ...
// )
// ...
// (catch
// ...
// )
// )
//
// to =>
//
// If sometype (try's type) is none:
// (block $outer (result sometype)
// (try (result sometype)
// (do
// (throw_ref
// (block $delegate_trampoline (result exnref)
// ...
// (try_table (catch_all_ref $delegate_trampoline)
// ...
// )
// ...
// (br $outer)
// )
// )
// )
// (catch
// ...
// )
// )
// )
//
// If sometype (try's type) is concrete:
// (block $outer (result sometype)
// (try (result sometype)
// (do
// (throw_ref
// (block $delegate_trampoline (result exnref)
// (br $outer ;; Now has the try_table as a child.
// ...
// (try_table (catch_all_ref $delegate_trampoline)
// ...
// )
// ...
// )
// )
// )
// )
// (catch
// ...
// )
// )
// )
//
// Note that the current try-catch (or try-delegate) stays as is for now; it
// will be converted to a try_table later in processDelegate() and
// processCatches().
//
// Also note that even in case there are multiple inner try~delegates
// targeting this try, we need to do this only once per try target. Those
// multiple try~delegates that used to target the same delegate target now
// jump to the same $delegate_trampoline using catch_all_ref.
Name delegateTrampoline = delegateTargetToTrampoline[curr->name];
Expression* innerBody = nullptr;
if (curr->type.isConcrete()) {
outerBlockUsedSoFar = true;
auto* brToOuter = builder.makeBreak(outerBlock->name, curr->body);
innerBody = builder.blockifyWithName(
brToOuter, delegateTrampoline, nullptr, Type(HeapType::exn, Nullable));
} else {
outerBlockUsedSoFar = curr->body->type != Type::unreachable;
auto* brToOuter = curr->body->type == Type::unreachable
? nullptr
: builder.makeBreak(outerBlock->name);
innerBody = builder.blockifyWithName(curr->body,
delegateTrampoline,
brToOuter,
Type(HeapType::exn, Nullable));
}
curr->body = builder.makeThrowRef(innerBody);
}
void processDelegate(Try* curr, Block* outerBlock, bool outerBlockUsedSoFar) {
Builder builder(*getModule());
// Convert
// (try
// (do
// ...
// )
// (delegate $delegate_target)
// )
//
// to =>
//
// (try_table (catch_ref $delegate_trampoline)
// ...
// )
//
// $delegate_trampoline is a block label that will be created in
// processDelegateTarget(), when we process the 'try' that is the target of
// this try~delegate. See processDelegateTarget() for how the rest of the
// conversion is completed.
auto* tryTable =
builder.makeTryTable(curr->body,
{Name()},
{delegateTargetToTrampoline[curr->delegateTarget]},
{true});
// If we need an outer block for other reasons (if this is a target of a
// delegate), we insert the new try_table into it. If not we just replace
// the current try with the new try_table.
if (outerBlock && outerBlockUsedSoFar) {
outerBlock->list.push_back(tryTable);
replaceCurrent(outerBlock);
} else {
replaceCurrent(tryTable);
}
}
void processCatches(Try* curr, Block* outerBlock, bool outerBlockUsedSoFar) {
Module* wasm = getModule();
Builder builder(*wasm);
// Determine whether a given catch body should be translated to
// catch/catch_all vs. catch_ref/catch_all_ref.
auto shouldBeRef = [&](Expression* catchBody) -> bool {
// If this try is targeted by rethrows and those rethrows exist in the
// current catch body, we need to use catch_ref/catch_all_ref. By this
// point, all rethrows in the catch bodies have been already converted to
// throw_refs, so we check the local numbers to see if those (original)
// rethrows used to target the current try label.
std::optional<Index> local = localAssigner->getExnrefLocal(curr->name);
if (local) {
for (auto* throwRef : FindAll<ThrowRef>(catchBody).list) {
// All rethrows within this catch body have already been converted to
// throw_refs, which contains a local.get as its child.(See
// visitRethrow() for details).
if (auto* localGet = throwRef->exnref->dynCast<LocalGet>()) {
if (localGet->index == *local) {
return true;
}
}
}
}
return false;
};
// Create try_table instruction with catch clauses.
std::vector<Name> catchTags;
std::vector<Name> catchDests;
std::vector<bool> catchRefs;
for (Index i = 0; i < curr->catchTags.size(); i++) {
catchTags.push_back(curr->catchTags[i]);
catchDests.push_back(labels->getUnique("catch"));
catchRefs.push_back(shouldBeRef(curr->catchBodies[i]));
}
if (curr->hasCatchAll()) {
catchTags.push_back(Name());
catchDests.push_back(labels->getUnique("catch_all"));
catchRefs.push_back(shouldBeRef(curr->catchBodies.back()));
}
auto* tryTable = builder.makeTryTable(
curr->body, catchTags, catchDests, catchRefs, curr->type);
// If we don't have any catches, we don't need to do more.
if (curr->catchBodies.empty()) { // catch-less try
// If we need an outer block for other reasons (if this is a target of a
// delegate), we insert the new try_table into it. If not we just replace
// the current try with the new try_table.
if (outerBlock && outerBlockUsedSoFar) {
outerBlock->list.push_back(tryTable);
replaceCurrent(outerBlock);
} else {
replaceCurrent(tryTable);
}
return;
}
// Now we convert catch bodies.
//
// For simplicity, let's assume all tags have the none type and there are no
// rethrows (which have converted to throw_refs at this point) and the try's
// type is none as well. Then
//
// (try
// (catch $e
// (catch_body)
// )
// (catch_all
// (catch_all_body)
// )
// )
//
// becomes
//
// (block $outer
// (block $catch_all
// (block $catch
// (try_table (catch $e $catch) (catch_all $catch_all)
// ...
// )
// (br $outer)
// )
// (catch_body)
// (br $outer)
// )
// (catch_all_body) ;; We don't need (br $outer) for the last catch
// )
//
// Here (block $outer) has been already created and given as an argument to
// this function. This is the outer block that will contain the whole
// structure.
//
// If there are more catch clauses, there will be more layers of blocks,
// catch bodies, and (br $outer)s.
//
// If try's type is concrete, (br $outer)s will contain try_table and catch
// bodies as values:
//
// (block $outer
// (block $catch_all
// (block $catch
// (br $outer
// (try_table (result sometype)
// (catch $e $catch) (catch_all $catch_all)
// ...
// )
// )
// )
// (br $outer
// (catch_body)
// )
// )
// (catch_all_body) ;; We don't need (br $outer) for the last catch
// )
//
// ---
//
// When a tag has a concrete type, we assign it to a scratch local of that
// type, and (pop tagtype) would have been already converted to
// (local.get $scratch) in visitPop(). These scratch locals have extremely
// short lifetimes; basically they are read right after they are written, so
// we can reuse them throughout the function.
//
// So, when there is no rethrows (throw_refs), and if we assume that try's
// type is none for simplicity,
// (try
// ...
// (catch $e-i32 ;; concrete type
// (use_inst
// (pop i32)
// )
// )
// )
//
// becomes
// (block $outer
// (local.set $scratch-i32
// (block $catch (result tagtype)
// (try_table (catch $e-i32 $catch)
// ...
// )
// (br $outer)
// )
// )
// (use_inst
// (local.get $scratch-i32)
// )
// )
//
// When the tag type is none or it is a catch_all, but there are rethrows
// (throws_refs at this point) within the catch body that targets this try's
// label, we use catch_ref (or catch_all_ref in case of catch_all), and
// assign the block return value to a exnref local. rethrows would have been
// converted to (local.get $exn) in visitRethrow() already. Unlike scratch
// locals used for pops, exnref locals can have longer lifetimes and are
// assigned in ExnrefLocalAssigner. So for example,
// (try $l0
// ...
// (catch_all
// (rethrow $l0)
// )
// )
//
// becomes
//
// (block $outer
// (local.set $exn
// (block $catch_all (result exnref)
// (try_table (catch_all_ref $catch_all)
// ...
// )
// (br $outer)
// )
// )
// (throw_ref
// (local.get $exn)
// )
// )
//
// When the tag type is concrete and also there are rethrows within the
// catch body that target this try's label, (extracted values, exnref)
// is saved in a tuple local matching its tuple type, and tuple.extract
// instructions are added to extract each of them and set them to a scratch
// local for a later pop and exnref local for later throw_ref.
//
// (try $l0
// ...
// (catch $e-i32 ;; concrete type
// (use_inst
// (pop i32)
// )
// (rethrow $l0)
// )
// )
//
// becomes
//
// (block $outer
// (local.set $tuple
// (block $catch (result i32 exnref)
// (try_table (catch_ref $e-i32 $catch)
// ...
// )
// (br $outer)
// )
// )
// (local.set $scratch-i32
// (tuple.extract 2 0
// (local.get $tuple)
// )
// )
// (local.set $exn
// (tuple.extract 2 1
// (local.get $tuple)
// )
// )
// (block
// (use_inst
// (local.get $scratch-i32)
// )
// (throw_ref
// (local.get $exn)
// )
// )
// )
//
// The transformation is similar when the tag type itself is a tuple and
// there are rethrows. We store the whole (tag types, exnref) in a tuple
// scratch local, and tuple.extract 1~(n-1)th elements and set them in
// another scratch local and nth element in an exnref local.
// Make the body for the innermost block
std::vector<Expression*> items;
if (tryTable->type.isConcrete()) {
items.push_back(builder.makeBreak(outerBlock->name, tryTable));
} else {
items.push_back(tryTable);
if (tryTable->type != Type::unreachable) {
items.push_back(builder.makeBreak(outerBlock->name));
}
}
// Convert each catch body to a wrapping block + catch body + br
for (Index i = 0; i < tryTable->catchTags.size(); i++) {
Type sentType = tryTable->sentTypes[i];
auto* catchBody = curr->catchBodies[i];
Type tagType = Type::none;
if (tryTable->catchTags[i]) {
tagType = wasm->getTag(tryTable->catchTags[i])->sig.params;
}
// This is to be the body of the next(outer) level block
std::vector<Expression*> nextItems;
auto* block = builder.makeBlock(tryTable->catchDests[i], items, sentType);
if (tryTable->catchRefs[i]) {
// When we use the exnref (i.e., there are throw_refs in the catch body)
Index exnrefLocal = *localAssigner->getExnrefLocal(curr->name);
if (tagType.isConcrete()) {
// If the tag type is single and we use the exnref, the block
// returns (tagtype, exnref). Get a scratch local to contain this
// tuple and reassign its elements to a pop scratch local and this
// try's corresponding exnref local respectively.
//
// If the tag type is a tuple and we use the exnref, the block returns
// (tagtype0, ..., tagtypeN, exnref). Assign (tagtype0, ..., tagtypeN)
// to a scratch (tuple) local and the exnref to this try's
// corresponding exnref local respectively.
Index allLocal = getScratchLocal(sentType);
Index popLocal = getScratchLocal(tagType);
auto* allLocalSet = builder.makeLocalSet(allLocal, block);
nextItems.push_back(allLocalSet);
Expression* popLocalSet = nullptr;
if (tagType.isTuple()) {
std::vector<Expression*> popVals;
for (Index j = 0; j < tagType.size(); j++) {
popVals.push_back(builder.makeTupleExtract(
builder.makeLocalGet(allLocal, sentType), j));
}
popLocalSet =
builder.makeLocalSet(popLocal, builder.makeTupleMake(popVals));
} else {
popLocalSet = builder.makeLocalSet(
popLocal,
builder.makeTupleExtract(builder.makeLocalGet(allLocal, sentType),
0));
}
nextItems.push_back(popLocalSet);
auto* exnrefLocalSet = builder.makeLocalSet(
exnrefLocal,
builder.makeTupleExtract(builder.makeLocalGet(allLocal, sentType),
sentType.size() - 1));
nextItems.push_back(exnrefLocalSet);
} else {
// If the tag type is none and we use the exnref, the block only
// returns exnref. Assign in to this try's corresponding exnref local.
auto* exnrefLocalSet = builder.makeLocalSet(exnrefLocal, block);
nextItems.push_back(exnrefLocalSet);
}
} else {
// When we don't use exnref and the tag type is concrete, we get a
// scratch local of the tag type and assign the block return value to
// that local. This process is the same for single and tuple tag types.
// If the tag type is none, we don't need to use any locals.
if (tagType.isConcrete()) {
Index popLocal = getScratchLocal(tagType);
auto* popLocalSet = builder.makeLocalSet(popLocal, block);
nextItems.push_back(popLocalSet);
} else {
nextItems.push_back(block);
}
}
if (catchBody->type.isConcrete()) {
// If this is the last catch body, we can omit the br and fall through
if (i < tryTable->catchTags.size() - 1) {
nextItems.push_back(builder.makeBreak(outerBlock->name, catchBody));
} else {
nextItems.push_back(catchBody);
}
} else {
nextItems.push_back(catchBody);
// If this is the last catch body, we can omit the br and fall through
if (i < tryTable->catchTags.size() - 1 &&
catchBody->type != Type::unreachable) {
nextItems.push_back(builder.makeBreak(outerBlock->name));
}
}
items.swap(nextItems);
}
for (auto* item : items) {
outerBlock->list.push_back(item);
}
replaceCurrent(outerBlock);
}
void visitTry(Try* curr) {
Builder builder(*getModule());
Block* outerBlock = nullptr;
auto it = delegateTargetToTrampoline.find(curr->name);
if (it != delegateTargetToTrampoline.end() || curr->isCatch()) {
outerBlock =
builder.makeBlock(labels->getUnique("outer"), {}, curr->type);
}
bool outerBlockUsedSoFar = false;
if (it != delegateTargetToTrampoline.end()) {
processDelegateTarget(curr, outerBlock, outerBlockUsedSoFar);
}
if (curr->isDelegate()) {
processDelegate(curr, outerBlock, outerBlockUsedSoFar);
} else { // try-catch or catch-less try
processCatches(curr, outerBlock, outerBlockUsedSoFar);
}
}
void visitPop(Pop* curr) {
// We can assume a 'pop' value is always in a scratch local that matches
// its type, because these locals are read right after being written and can
// be reused for all pops. This applies to tuple type pops as well.
Builder builder(*getModule());
Index popLocal = getScratchLocal(curr->type);
replaceCurrent(builder.makeLocalGet(popLocal, curr->type));
}
void visitRethrow(Rethrow* curr) {
// After we assigned an exnref local for each try label targeted by
// rethrows, we can assume the exnref we want to rethrow is located in that
// exnref local at this point. We ensure this to happen when converting the
// corresponding 'try' to 'try_table' by using catch_ref/catch_all_ref and
// assining the exnref to that local.
Builder builder(*getModule());
Index exnrefLocal = *localAssigner->getExnrefLocal(curr->target);
replaceCurrent(builder.makeThrowRef(
builder.makeLocalGet(exnrefLocal, Type(HeapType::exn, Nullable))));
}
// Similar to processDelegateTarget(), but does it for the caller delegate
// target, which means we should rethrow to the caller.
void processCallerDelegateTarget() {
Name callerDelegateTrampoline =
delegateTargetToTrampoline[DELEGATE_CALLER_TARGET];
Builder builder(*getModule());
Function* func = getFunction();
// The transformation is similar to that of normal delegate targets, but
// instead of branching to an outer label in case no exception is thrown, we
// just return.
//
// Convert
//
// (func $test (result sometype)
// ...
// (try_table (catch_all_ref $caller_delegate_trampoline)
// ...
// )
// ...
// )
//
// to =>
//
// If sometype (func's type) is none:
// (func $test (result sometype)
// (throw_ref
// (block $caller_delegate_trampoline (result exnref)
// ...
// (try_table (catch_all_ref $caller_delegate_trampoline)
// ...
// )
// ...
// (return)
// )
// )
// )
//
// If sometype (func's type) is concrete:
// (throw_ref
// (block $caller_delegate_trampoline (result exnref)
// (return
// ...
// (try_table (catch_all_ref $caller_delegate_trampoline)
// ...
// )
// ...
// )
// )
// )
// )
Expression* innerBody = nullptr;
if (func->getResults().isConcrete()) {
auto* ret = builder.makeReturn(func->body);
innerBody = builder.blockifyWithName(
ret, callerDelegateTrampoline, nullptr, Type(HeapType::exn, Nullable));
} else {
auto* ret = builder.makeReturn();
innerBody = builder.blockifyWithName(func->body,
callerDelegateTrampoline,
ret,
Type(HeapType::exn, Nullable));
}
func->body = builder.makeThrowRef(innerBody);
}
void doWalkFunction(Function* func) {
labels = std::make_optional<LabelUtils::LabelManager>(func);
labelScanner = std::make_optional<TargetTryLabelScanner>(func);
localAssigner =
std::make_optional<ExnrefLocalAssigner>(func, &labelScanner.value());
// Create a unique br target label for each existing delegate target label,
// because we are going to achieve 'delegate's effects with 'br's. See
// processDelegateTarget() for details.
for (auto& target : labelScanner->delegateTargets) {
delegateTargetToTrampoline[target] = labels->getUnique(target.toString());
}
Super::doWalkFunction(func);
// Similar to processDelegateTarget(), but for the caller target.
if (delegateTargetToTrampoline.find(DELEGATE_CALLER_TARGET) !=
delegateTargetToTrampoline.end()) {
processCallerDelegateTarget();
}
}
};
} // namespace
Pass* createTranslateToExnrefPass() { return new TranslateToExnref(); }
} // namespace wasm