| /* | |
| * 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])->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 | |
| // assigning 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 |