| /* | |
| * Copyright 2023 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. | |
| */ | |
| // IRBuilder constructs valid Binaryen IR expression trees from a flat stream of | |
| // WebAssembly instructions (e.g. from the binary or text parsers). | |
| // | |
| // Core Mechanism: | |
| // | |
| // - Maintain a stack of control scopes (`ScopeCtx`). | |
| // | |
| // - Track an expression stack (`exprStack`) within each scope. | |
| // | |
| // - Pop children from `exprStack` (via `ChildPopper`), set them as operands on | |
| // the new Expression, and push the Expression back onto `exprStack`. | |
| // | |
| // - At scope end (`visitEnd`), the scope's expressions are inserted into the | |
| // scope's control flow Expression, which is pushed to the parent scope. | |
| // | |
| // There are extra complications necessary to bridge the gap between WebAssembly | |
| // and Binaryen IR: | |
| // | |
| // - Unreachability: When the instructions before an unreachable-typed | |
| // Expression would be popped but do not satisfy the parent Expression's type | |
| // requirements, they are instead dropped and new `unreachable` instructions | |
| // are synthesized instead (`popConstrainedChildren`). | |
| // | |
| // - Branch targets: Wasm allows branching to any control structure (if, try, | |
| // try_table, loop, block), whereas Binaryen IR branches can target only | |
| // `Block` or `Loop`. IRBuilder wraps other control flow structures in | |
| // synthetic named `Block`s when targeted by branches (`maybeWrapForLabel`). | |
| // | |
| // - Branch values and block inputs: Wasm supports sending extra values with any | |
| // branch instruction and also supports input parameters to any control flow | |
| // structure. Binaryen IR only supports sending extra values on br and br_if | |
| // and never supports input parameters to control flow structures. IRBuilder | |
| // lowers away sent values and control flow parameters using scratch locals | |
| // and trampoline blocks (`fixLoopWithInput`, `fixExtraOutput`). | |
| // | |
| // - Exception handling pops: Wasm EH `catch` blocks implicitly pop exception | |
| // payloads from the value stack. `IRBuilder` generates synthetic `Pop` | |
| // Expressions and performs fixups for pops within nested blocks | |
| // (`handleBlockNestedPops`). | |
| #include <cassert> | |
| #include "ir/child-typer.h" | |
| #include "ir/eh-utils.h" | |
| #include "ir/names.h" | |
| #include "ir/principal-type.h" | |
| #include "ir/properties.h" | |
| #include "ir/utils.h" | |
| #include "wasm-ir-builder.h" | |
| #include "wasm.h" | |
| #ifndef IR_BUILDER_DEBUG | |
| #define IR_BUILDER_DEBUG 0 | |
| #endif | |
| #if IR_BUILDER_DEBUG | |
| #define DBG(statement) statement | |
| #else | |
| #define DBG(statement) | |
| #endif | |
| using namespace std::string_literals; | |
| namespace wasm { | |
| namespace { | |
| Result<> validateTypeAnnotation(Type type, Expression* child) { | |
| if (!Type::isSubType(child->type, type)) { | |
| return Err{"invalid type on stack"}; | |
| } | |
| return Ok{}; | |
| } | |
| Result<> validateTypeAnnotation(HeapType type, Expression* child) { | |
| return validateTypeAnnotation(Type(type, Nullable), child); | |
| } | |
| Result<> requireNaturalAtomicAlign(Address align, Address natural) { | |
| if (align != natural) { | |
| return Err{"atomic accesses must have natural alignment"}; | |
| } | |
| return Ok{}; | |
| } | |
| } // anonymous namespace | |
| Result<Index> IRBuilder::addScratchLocal(Type type) { | |
| if (!func) { | |
| return Err{"scratch local required, but there is no function context"}; | |
| } | |
| Name name = Names::getValidLocalName(*func, "scratch"); | |
| return Builder::addVar(func, name, type); | |
| } | |
| MaybeResult<IRBuilder::HoistedVal> IRBuilder::hoistLastValue(bool greedy) { | |
| auto& stack = getScope().exprStack; | |
| int valIndex = stack.size() - 1; | |
| for (; valIndex >= 0; --valIndex) { | |
| if (stack[valIndex]->type != Type::none) { | |
| break; | |
| } | |
| } | |
| if (valIndex < 0) { | |
| // There is no value-producing or unreachable expression. | |
| return {}; | |
| } | |
| int hoistIndex = valIndex; | |
| if (greedy) { | |
| while (hoistIndex > 0 && stack[hoistIndex - 1]->type == Type::none) { | |
| --hoistIndex; | |
| } | |
| } | |
| if (unsigned(valIndex) == stack.size() - 1) { | |
| // Value-producing expression already on top of the stack. | |
| return HoistedVal{Index(hoistIndex), nullptr}; | |
| } | |
| auto*& expr = stack[valIndex]; | |
| if (expr->type == Type::unreachable) { | |
| // No need for a scratch local to hoist an unreachable. | |
| return HoistedVal{Index(hoistIndex), nullptr}; | |
| } | |
| // Hoist with a scratch local. Normally the scratch local is the same type as | |
| // the hoisted expression, but we may need to adjust it given the enabled | |
| // features. Otherwise, if the expression has a tuple type with a more refined | |
| // element than would be written to a binary, then that refined element type | |
| // would end up in a multivalue block return. But that could cause us to fail | |
| // text roundtripping if the block type would conflict after binary writing | |
| // with another function type in the module. Avoid this problem by | |
| // generalizing the scratch local type eagerly. | |
| auto type = expr->type.asWrittenGivenFeatures(wasm.features); | |
| auto scratchIdx = addScratchLocal(type); | |
| CHECK_ERR(scratchIdx); | |
| expr = builder.makeLocalSet(*scratchIdx, expr); | |
| auto* get = builder.makeLocalGet(*scratchIdx, type); | |
| pushSynthetic(get); | |
| return HoistedVal{Index(hoistIndex), get}; | |
| } | |
| Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, | |
| size_t sizeHint) { | |
| auto& scope = getScope(); | |
| assert(!scope.exprStack.empty()); | |
| auto packageAsBlock = [&](Type type) { | |
| // Create a block containing the producer of the hoisted value, the final | |
| // get of the hoisted value, and everything in between. Record the fact that | |
| // we are synthesizing a block to help us determine later whether we need to | |
| // run the nested pop fixup. | |
| scopeStack[0].noteSyntheticBlock(); | |
| std::vector<Expression*> exprs(scope.exprStack.begin() + hoisted.hoistIndex, | |
| scope.exprStack.end()); | |
| auto* block = builder.makeBlock(exprs, type); | |
| scope.exprStack.resize(hoisted.hoistIndex); | |
| pushSynthetic(block); | |
| }; | |
| auto type = scope.exprStack.back()->type; | |
| if (type == Type::none) { | |
| // If we did not have a value on top of the stack and did not add a scratch | |
| // local, then there must have been an unreachable. | |
| type = Type::unreachable; | |
| } | |
| if (type.size() == sizeHint || type.size() <= 1) { | |
| if (hoisted.hoistIndex < scope.exprStack.size() - 1) { | |
| packageAsBlock(type); | |
| } | |
| return Ok{}; | |
| } | |
| // We need to break up the hoisted tuple. Create and push an expression | |
| // setting the tuple to a local and returning its first element, then push | |
| // additional gets of each of its subsequent elements. Reuse the scratch local | |
| // we used for hoisting, if it exists. | |
| Index scratchIdx; | |
| if (hoisted.get) { | |
| // Update the get on top of the stack to just return the first element. | |
| scope.exprStack.back() = builder.makeTupleExtract(hoisted.get, 0); | |
| packageAsBlock(type[0]); | |
| scratchIdx = hoisted.get->index; | |
| } else { | |
| auto scratch = addScratchLocal(type); | |
| CHECK_ERR(scratch); | |
| scope.exprStack.back() = builder.makeTupleExtract( | |
| builder.makeLocalTee(*scratch, scope.exprStack.back(), type), 0); | |
| scratchIdx = *scratch; | |
| } | |
| for (Index i = 1, size = type.size(); i < size; ++i) { | |
| pushSynthetic( | |
| builder.makeTupleExtract(builder.makeLocalGet(scratchIdx, type), i)); | |
| } | |
| return Ok{}; | |
| } | |
| void IRBuilder::push(Expression* expr, Origin origin) { | |
| auto& scope = getScope(); | |
| if (expr->type == Type::unreachable) { | |
| scope.unreachable = true; | |
| } | |
| scope.exprStack.push_back(expr); | |
| if (origin == Origin::Binary) { | |
| applyDebugLoc(expr); | |
| if (binaryPos && func && lastBinaryPos != *binaryPos) { | |
| auto span = | |
| BinaryLocations::Span{BinaryLocation(lastBinaryPos - codeSectionOffset), | |
| BinaryLocation(*binaryPos - codeSectionOffset)}; | |
| // Some expressions already have their start noted, and we are just seeing | |
| // their last segment (like an Else). | |
| auto [iter, inserted] = func->expressionLocations.insert({expr, span}); | |
| if (!inserted) { | |
| // Just update the end. | |
| iter->second.end = span.end; | |
| // The true start from before is before the start of the current | |
| // segment. | |
| assert(iter->second.start < span.start); | |
| } | |
| lastBinaryPos = *binaryPos; | |
| } | |
| } | |
| DBG(std::cerr << "After pushing " << ShallowExpression{expr} << ":\n"); | |
| DBG(dump()); | |
| } | |
| Result<Expression*> IRBuilder::build() { | |
| if (scopeStack.empty()) { | |
| return builder.makeBlock(); | |
| } | |
| if (scopeStack.size() > 1 || !scopeStack.back().isNone()) { | |
| return Err{"unfinished block context"}; | |
| } | |
| if (scopeStack.back().exprStack.size() > 1) { | |
| return Err{"unused expressions without block context"}; | |
| } | |
| assert(scopeStack.back().exprStack.size() == 1); | |
| auto* expr = scopeStack.back().exprStack.back(); | |
| scopeStack.clear(); | |
| labelDepths.clear(); | |
| return expr; | |
| } | |
| void IRBuilder::setDebugLocation( | |
| const std::optional<Function::DebugLocation>& loc) { | |
| if (loc) { | |
| DBG(std::cerr << "setting debugloc " << loc->fileIndex << ":" | |
| << loc->lineNumber << ":" << loc->columnNumber << "\n";); | |
| } else { | |
| DBG(std::cerr << "setting debugloc to none\n";); | |
| } | |
| if (loc) { | |
| debugLoc = *loc; | |
| } else { | |
| debugLoc = NoDebug(); | |
| } | |
| } | |
| void IRBuilder::applyDebugLoc(Expression* expr) { | |
| if (!std::get_if<CanReceiveDebug>(&debugLoc)) { | |
| if (func) { | |
| if (auto* loc = std::get_if<Function::DebugLocation>(&debugLoc)) { | |
| DBG(std::cerr << "applying debugloc " << loc->fileIndex << ":" | |
| << loc->lineNumber << ":" << loc->columnNumber | |
| << " to expression " << ShallowExpression{expr} << "\n"); | |
| func->debugLocations[expr] = *loc; | |
| } else { | |
| assert(std::get_if<NoDebug>(&debugLoc)); | |
| DBG(std::cerr << "applying debugloc to expression " | |
| << ShallowExpression{expr} << "\n"); | |
| func->debugLocations[expr] = std::nullopt; | |
| } | |
| } | |
| debugLoc = CanReceiveDebug(); | |
| } | |
| } | |
| void IRBuilder::dump() { | |
| #if IR_BUILDER_DEBUG | |
| std::cerr << "Scope stack"; | |
| if (func) { | |
| std::cerr << " in function $" << func->name; | |
| } | |
| std::cerr << ":\n"; | |
| for (auto& scope : scopeStack) { | |
| std::cerr << " scope "; | |
| if (scope.isNone()) { | |
| std::cerr << "none"; | |
| } else if (auto* f = scope.getFunction()) { | |
| std::cerr << "func " << f->name; | |
| } else if (scope.getBlock()) { | |
| std::cerr << "block"; | |
| } else if (scope.getIf()) { | |
| std::cerr << "if"; | |
| } else if (scope.getElse()) { | |
| std::cerr << "else"; | |
| } else if (scope.getLoop()) { | |
| std::cerr << "loop"; | |
| } else if (auto* tryy = scope.getTry()) { | |
| std::cerr << "try"; | |
| if (tryy->name) { | |
| std::cerr << " " << tryy->name; | |
| } | |
| } else if (auto* tryy = scope.getCatch()) { | |
| std::cerr << "catch"; | |
| if (tryy->name) { | |
| std::cerr << " " << tryy->name; | |
| } | |
| } else if (auto* tryy = scope.getCatchAll()) { | |
| std::cerr << "catch_all"; | |
| if (tryy->name) { | |
| std::cerr << " " << tryy->name; | |
| } | |
| } else if (scope.getTryTable()) { | |
| std::cerr << "try_table"; | |
| } else { | |
| WASM_UNREACHABLE("unexpected scope"); | |
| } | |
| if (auto name = scope.getOriginalLabel()) { | |
| std::cerr << " (original label: " << name << ")"; | |
| } | |
| if (scope.label) { | |
| std::cerr << " (label: " << scope.label << ")"; | |
| } | |
| if (scope.branchLabel) { | |
| std::cerr << " (branch label: " << scope.branchLabel << ")"; | |
| } | |
| if (scope.unreachable) { | |
| std::cerr << " (unreachable)"; | |
| } | |
| std::cerr << ":\n"; | |
| for (auto* expr : scope.exprStack) { | |
| std::cerr << " " << ShallowExpression{expr} << " (; " | |
| << expr->type.toString() << " ;)\n"; | |
| } | |
| } | |
| #endif // IR_BUILDER_DEBUG | |
| } | |
| struct IRBuilder::ChildPopper | |
| : UnifiedExpressionVisitor<ChildPopper, Result<>> { | |
| struct ConstraintCollector; | |
| using Constraints = ChildTyper<ConstraintCollector>::Constraints; | |
| struct Child { | |
| Expression** childp; | |
| Constraints constraint; | |
| }; | |
| struct ConstraintCollector : ChildTyper<ConstraintCollector> { | |
| IRBuilder& builder; | |
| std::vector<Child>& children; | |
| ConstraintCollector(IRBuilder& builder, std::vector<Child>& children) | |
| : ChildTyper(builder.wasm, builder.func), builder(builder), | |
| children(children) {} | |
| void note(Expression** childp, Constraints type) { | |
| children.push_back({childp, type}); | |
| } | |
| Type getLabelType(Name label) { | |
| WASM_UNREACHABLE("labels should be explicitly provided"); | |
| }; | |
| void visitIf(If* curr) { | |
| // Skip the control flow children because we only want to pop the | |
| // condition. | |
| children.push_back({&curr->condition, {Type(Type::i32)}}); | |
| } | |
| // It is a bug if we ever have insufficient type information. | |
| void noteUnknown() { | |
| WASM_UNREACHABLE("unexpected insufficient type information"); | |
| } | |
| }; | |
| IRBuilder& builder; | |
| ChildPopper(IRBuilder& builder) : builder(builder) {} | |
| private: | |
| Result<> popConstrainedChildren(std::vector<Child>& children) { | |
| auto& scope = builder.getScope(); | |
| // The stack size at which we are about to pop an unreachable instruction | |
| // that we must not pop past because the expressions underneath it do not | |
| // have the types we need. | |
| std::optional<size_t> unreachableFallbackSize; | |
| // We only need to check requirements if there is an unreachable. | |
| // Otherwise the validator will catch any problems. | |
| if (scope.unreachable) { | |
| unreachableFallbackSize = checkNeedsUnreachableFallback(children); | |
| } | |
| // We have checked all the constraints, so we are ready to pop children. | |
| for (int i = children.size() - 1; i >= 0; --i) { | |
| if (unreachableFallbackSize && | |
| scope.exprStack.size() == *unreachableFallbackSize && i > 0) { | |
| // The next item on the stack is the unreachable instruction we must | |
| // not pop past. We could pop it as child 0, but we are not ready to pop | |
| // child 0 yet, so synthesize an unreachable instead of popping. The | |
| // deeper instructions that would otherwise have been popped will remain | |
| // on the stack to become prior children of future expressions or to be | |
| // implicitly dropped at the end of the scope. | |
| *children[i].childp = builder.builder.makeUnreachable(); | |
| continue; | |
| } | |
| // Pop a child normally. Pop greedily for children other than the first | |
| // (i.e. the last to be popped). | |
| bool greedy = i > 0; | |
| auto val = pop(children[i].constraint.size(), greedy); | |
| CHECK_ERR(val); | |
| *children[i].childp = *val; | |
| } | |
| return Ok{}; | |
| } | |
| std::optional<size_t> | |
| checkNeedsUnreachableFallback(const std::vector<Child>& children) { | |
| auto& scope = builder.getScope(); | |
| std::optional<size_t> unreachableFallbackSize; | |
| // Two-part indices into the stack of available expressions and the vector | |
| // of requirements, allowing them to move independently with the granularity | |
| // of a single tuple element. | |
| size_t stackIndex = scope.exprStack.size(); | |
| size_t stackTupleIndex = 0; | |
| size_t childIndex = children.size(); | |
| size_t childTupleIndex = 0; | |
| // Whether we are deeper than some concrete expression. | |
| bool seenConcrete = false; | |
| // Check whether the values on the stack will be able to meet the given | |
| // requirements. | |
| while (true) { | |
| // Advance to the next requirement. | |
| if (childTupleIndex > 0) { | |
| --childTupleIndex; | |
| } else { | |
| if (childIndex == 0) { | |
| // We have examined all the requirements. | |
| break; | |
| } | |
| --childIndex; | |
| childTupleIndex = children[childIndex].constraint.size() - 1; | |
| } | |
| // Advance to the next available value on the stack. | |
| while (true) { | |
| if (stackTupleIndex > 0) { | |
| --stackTupleIndex; | |
| } else { | |
| if (stackIndex == 0) { | |
| // No more available values. This is valid iff we are reaching past | |
| // an unreachable, but we still need the fallback behavior to ensure | |
| // the input unreachable instruction is executed first. If we are | |
| // not reaching past an unreachable, the error will be caught when | |
| // we pop. | |
| return unreachableFallbackSize; | |
| } | |
| --stackIndex; | |
| stackTupleIndex = scope.exprStack[stackIndex]->type.size() - 1; | |
| } | |
| // Skip expressions that don't produce values. | |
| if (scope.exprStack[stackIndex]->type == Type::none) { | |
| stackTupleIndex = 0; | |
| continue; | |
| } | |
| break; | |
| } | |
| // We have an available type and a constraint. Only check constraints if | |
| // we are deeper than an unreachable, since otherwise we can leave | |
| // problems to be caught by the validator later. | |
| auto type = scope.exprStack[stackIndex]->type[stackTupleIndex]; | |
| if (unreachableFallbackSize) { | |
| auto constraint = children[childIndex].constraint[childTupleIndex]; | |
| if (!PrincipalType::matches(type, constraint)) { | |
| return unreachableFallbackSize; | |
| } | |
| } | |
| // We may need an unreachable fallback if we find violated constraints in | |
| // expressions deeper than this unreachable. Calculate the stack size at | |
| // which this unreachable will be the next thing popped. This size is | |
| // usually one more than the current index, but if there are no shallower | |
| // concrete expressions, then everything down to this unreachable will be | |
| // popped at once immediately at the current stack size. | |
| if (type == Type::unreachable) { | |
| unreachableFallbackSize = | |
| seenConcrete ? stackIndex + 1 : scope.exprStack.size(); | |
| } else { | |
| // We skipped none-typed expressions and this isn't unreachable, so it | |
| // must be concrete. | |
| assert(type.isConcrete()); | |
| seenConcrete = true; | |
| } | |
| } | |
| // No unreachable fallback necessary. | |
| return std::nullopt; | |
| } | |
| // If `greedy`, then we will pop additional none-typed expressions that come | |
| // before the value-producing expression. The additional expressions will be | |
| // packaged into a block with the value-producing expression. This is better | |
| // than leaving them on top of the stack, where they will force the use of a | |
| // scratch local when the next operand is popped. `greedy` should be used when | |
| // popping all children of an expression except the first (i.e. the | |
| // last child to be popped). Not being greedy for the last popped child defers | |
| // the creation of a block to hold its none-typed predecessors. It may turn | |
| // out that such a block is not necessary, for example when the none-typed | |
| // expressions can be included directly into a parent block scope. | |
| Result<Expression*> pop(size_t size, bool greedy = false) { | |
| assert(size >= 1); | |
| auto& scope = builder.getScope(); | |
| // Find the suffix of expressions that do not produce values. | |
| auto hoisted = builder.hoistLastValue(greedy); | |
| CHECK_ERR(hoisted); | |
| if (!hoisted) { | |
| // There are no expressions that produce values. | |
| if (scope.unreachable) { | |
| return builder.builder.makeUnreachable(); | |
| } | |
| return Err{"popping from empty stack"}; | |
| } | |
| CHECK_ERR(builder.packageHoistedValue(*hoisted, size)); | |
| auto* ret = scope.exprStack.back(); | |
| // If the top value has the correct size, we can pop it and be done. | |
| // Unreachable values satisfy any size. | |
| if (ret->type.size() == size || ret->type == Type::unreachable) { | |
| scope.exprStack.pop_back(); | |
| return ret; | |
| } | |
| // The last value-producing expression did not produce exactly the right | |
| // number of values, so we need to construct a tuple piecewise instead. | |
| assert(size > 1); | |
| std::vector<Expression*> elems; | |
| elems.resize(size); | |
| for (int i = size - 1; i >= 0; --i) { | |
| auto elem = pop(1, greedy || i > 0); | |
| CHECK_ERR(elem); | |
| elems[i] = *elem; | |
| } | |
| return builder.builder.makeTupleMake(elems); | |
| } | |
| public: | |
| Result<> visitExpression(Expression* expr) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visit(expr); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitAtomicCmpxchg(AtomicCmpxchg* curr, | |
| std::optional<Type> type = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitAtomicCmpxchg(curr, type); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStructGet(StructGet* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStructGet(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStructSet(StructSet* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStructSet(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStructRMW(StructRMW* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStructRMW(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStructCmpxchg(StructCmpxchg* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStructCmpxchg(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStructWait(StructWait* curr, | |
| std::optional<HeapType> structType = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStructWait(curr, structType); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayGet(ArrayGet* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayGet(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArraySet(ArraySet* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArraySet(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayLoad(ArrayLoad* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayLoad(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayStore(ArrayStore* curr, | |
| std::optional<HeapType> ht = std::nullopt, | |
| std::optional<Type> valueType = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayStore(curr, ht, valueType); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayCopy(ArrayCopy* curr, | |
| std::optional<HeapType> dest = std::nullopt, | |
| std::optional<HeapType> src = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayCopy(curr, dest, src); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayFill(ArrayFill* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayFill(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayInitData(ArrayInitData* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayInitData(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayInitElem(ArrayInitElem* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayInitElem(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayRMW(ArrayRMW* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayRMW(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitArrayCmpxchg(ArrayCmpxchg* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitArrayCmpxchg(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitCallRef(CallRef* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitCallRef(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitRefGetDesc(RefGetDesc* curr, | |
| std::optional<HeapType> ht = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitRefGetDesc(curr, ht); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitBreak(Break* curr, | |
| std::optional<Type> labelType = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitBreak(curr, labelType); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitBrOn(BrOn* curr, | |
| std::optional<Type> in = std::nullopt, | |
| std::optional<Type> out = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitBrOn(curr, in, out); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitSwitch(Switch* curr, | |
| std::optional<Type> labelType = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitSwitch(curr, labelType); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitDrop(Drop* curr, std::optional<Index> arity = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitDrop(curr, arity); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitTupleExtract(TupleExtract* curr, | |
| std::optional<Index> arity = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitTupleExtract(curr, arity); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitContBind(ContBind* curr, | |
| std::optional<HeapType> src = std::nullopt, | |
| std::optional<HeapType> dest = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitContBind(curr, src, dest); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitResume(Resume* curr, | |
| std::optional<HeapType> ct = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitResume(curr, ct); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitResumeThrow(ResumeThrow* curr, | |
| std::optional<HeapType> ct = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitResumeThrow(curr, ct); | |
| return popConstrainedChildren(children); | |
| } | |
| Result<> visitStackSwitch(StackSwitch* curr, | |
| std::optional<HeapType> ct = std::nullopt) { | |
| std::vector<Child> children; | |
| ConstraintCollector{builder, children}.visitStackSwitch(curr, ct); | |
| return popConstrainedChildren(children); | |
| } | |
| }; | |
| Result<> IRBuilder::visit(Expression* curr) { | |
| // Call either `visitExpression` or an expression-specific override. | |
| auto val = UnifiedExpressionVisitor<IRBuilder, Result<>>::visit(curr); | |
| CHECK_ERR(val); | |
| if (auto* block = curr->dynCast<Block>()) { | |
| block->finalize(block->type); | |
| } else { | |
| // TODO: Call more efficient versions of finalize() that take the known type | |
| // for other kinds of nodes as well, as done above. | |
| ReFinalizeNode{}.visit(curr); | |
| } | |
| push(curr); | |
| return Ok{}; | |
| } | |
| // Handle the common case of instructions with a constant number of children | |
| // uniformly. | |
| Result<> IRBuilder::visitExpression(Expression* curr) { | |
| if (Properties::isControlFlowStructure(curr) && !curr->is<If>()) { | |
| // Control flow structures (besides `if`, handled separately) do not consume | |
| // stack values. | |
| return Ok{}; | |
| } | |
| return ChildPopper{*this}.visit(curr); | |
| } | |
| Result<Type> IRBuilder::getLabelType(Index label) { | |
| auto scope = getScope(label); | |
| CHECK_ERR(scope); | |
| return (*scope)->getLabelType(); | |
| } | |
| Result<Type> IRBuilder::getLabelType(Name labelName) { | |
| auto label = getLabelIndex(labelName); | |
| CHECK_ERR(label); | |
| return getLabelType(*label); | |
| } | |
| Result<> IRBuilder::visitBreakWithType(Break* curr, Type type) { | |
| CHECK_ERR(ChildPopper{*this}.visitBreak(curr, type)); | |
| curr->finalize(); | |
| push(curr); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::visitSwitchWithType(Switch* curr, Type type) { | |
| CHECK_ERR(ChildPopper{*this}.visitSwitch(curr, type)); | |
| curr->finalize(); | |
| push(curr); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::visitFunctionStart(Function* func) { | |
| if (!scopeStack.empty()) { | |
| return Err{"unexpected start of function"}; | |
| } | |
| if (auto* loc = std::get_if<Function::DebugLocation>(&debugLoc)) { | |
| func->prologLocation = *loc; | |
| } | |
| debugLoc = CanReceiveDebug(); | |
| scopeStack.push_back(ScopeCtx::makeFunc(func)); | |
| this->func = func; | |
| if (binaryPos) { | |
| lastBinaryPos = *binaryPos; | |
| } | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::visitBlockStart(Block* curr, Type inputType) { | |
| applyDebugLoc(curr); | |
| return pushScope(ScopeCtx::makeBlock(curr, inputType)); | |
| } | |
| Result<> IRBuilder::visitIfStart(If* iff, Name label, Type inputType) { | |
| applyDebugLoc(iff); | |
| CHECK_ERR(visitIf(iff)); | |
| return pushScope(ScopeCtx::makeIf(iff, label, inputType)); | |
| } | |
| Result<> IRBuilder::visitLoopStart(Loop* loop, Type inputType) { | |
| applyDebugLoc(loop); | |
| return pushScope(ScopeCtx::makeLoop(loop, inputType)); | |
| } | |
| Result<> IRBuilder::visitTryStart(Try* tryy, Name label, Type inputType) { | |
| applyDebugLoc(tryy); | |
| return pushScope(ScopeCtx::makeTry(tryy, label, inputType)); | |
| } | |
| Result<> | |
| IRBuilder::visitTryTableStart(TryTable* trytable, Name label, Type inputType) { | |
| applyDebugLoc(trytable); | |
| return pushScope(ScopeCtx::makeTryTable(trytable, label, inputType)); | |
| } | |
| Result<Expression*> IRBuilder::finishScope(Block* block) { | |
| #if IR_BUILDER_DEBUG | |
| if (auto* loc = std::get_if<Function::DebugLocation>(&debugLoc)) { | |
| std::cerr << "discarding debugloc " << loc->fileIndex << ":" | |
| << loc->lineNumber << ":" << loc->columnNumber << "\n"; | |
| } | |
| #endif | |
| debugLoc = CanReceiveDebug(); | |
| if (scopeStack.empty() || scopeStack.back().isNone()) { | |
| return Err{"unexpected end of scope"}; | |
| } | |
| auto& scope = scopeStack.back(); | |
| auto type = scope.getResultType(); | |
| if (scope.unreachable) { | |
| // Drop everything before the last unreachable. | |
| bool sawUnreachable = false; | |
| for (int i = scope.exprStack.size() - 1; i >= 0; --i) { | |
| if (sawUnreachable) { | |
| scope.exprStack[i] = builder.dropIfConcretelyTyped(scope.exprStack[i]); | |
| } else if (scope.exprStack[i]->type == Type::unreachable) { | |
| sawUnreachable = true; | |
| } | |
| } | |
| } | |
| if (type.isConcrete()) { | |
| auto hoisted = hoistLastValue(); | |
| CHECK_ERR(hoisted); | |
| if (!hoisted) { | |
| return Err{"popping from empty stack"}; | |
| } | |
| if (scope.exprStack.back()->type == Type::none) { | |
| // Nothing was hoisted, which means there must have been an unreachable | |
| // buried under none-type expressions. It is not valid to end a concretely | |
| // typed block with none-typed expressions, so add an extra unreachable. | |
| pushSynthetic(builder.makeUnreachable()); | |
| } | |
| if (type.isTuple()) { | |
| auto hoistedType = scope.exprStack.back()->type; | |
| if (hoistedType != Type::unreachable && | |
| hoistedType.size() != type.size()) { | |
| // We cannot propagate the hoisted value directly because it does not | |
| // have the correct number of elements. Repackage it. | |
| CHECK_ERR(packageHoistedValue(*hoisted, hoistedType.size())); | |
| CHECK_ERR(makeTupleMake(type.size())); | |
| } | |
| } | |
| } | |
| Expression* ret = nullptr; | |
| if (scope.exprStack.size() == 0) { | |
| // No expressions for this scope, but we need something. If we were given a | |
| // block, we can empty it out and return it, but otherwise create a new | |
| // empty block. | |
| if (block) { | |
| block->list.clear(); | |
| ret = block; | |
| } else { | |
| ret = builder.makeBlock(); | |
| } | |
| } else if (scope.exprStack.size() == 1) { | |
| // We can put our single expression directly into the surrounding scope. | |
| if (block) { | |
| block->list.resize(1); | |
| block->list[0] = scope.exprStack.back(); | |
| ret = block; | |
| } else { | |
| ret = scope.exprStack.back(); | |
| } | |
| } else { | |
| // More than one expression, so we need a block. Allocate one if we weren't | |
| // already given one. | |
| if (block) { | |
| block->list.set(scope.exprStack); | |
| } else { | |
| block = builder.makeBlock(scope.exprStack, type); | |
| } | |
| ret = block; | |
| } | |
| // If this scope had a label, remove it from the context. | |
| if (auto label = scope.getOriginalLabel()) { | |
| labelDepths.at(label).pop_back(); | |
| } | |
| scopeStack.pop_back(); | |
| return ret; | |
| } | |
| Result<> IRBuilder::visitElse() { | |
| auto scope = getScope(); | |
| auto* iff = scope.getIf(); | |
| if (!iff) { | |
| return Err{"unexpected else"}; | |
| } | |
| auto expr = finishScope(); | |
| CHECK_ERR(expr); | |
| iff->ifTrue = *expr; | |
| if (binaryPos && func) { | |
| func->delimiterLocations[iff][BinaryLocations::Else] = | |
| lastBinaryPos - codeSectionOffset; | |
| // Note the start of the if (which will be lost as the If is closed and the | |
| // Else begins, but the if spans them both). | |
| func->expressionLocations[iff].start = scope.startPos - codeSectionOffset; | |
| } | |
| return pushScope(ScopeCtx::makeElse(std::move(scope))); | |
| } | |
| void setCatchBody(Try* tryy, Expression* expr, Index index) { | |
| // Indexes are managed manually to support Outlining. | |
| // Its prepopulated try catchBodies and catchTags vectors | |
| // cannot be appended to, as in the case of the empty try | |
| // used during parsing. | |
| if (tryy->catchBodies.size() < index) { | |
| tryy->catchBodies.resize(tryy->catchBodies.size() + 1); | |
| } | |
| // The first time visitCatch is called: the body of the | |
| // try is set and catchBodies is not appended to, but the tag | |
| // for the following catch is appended. So, catchTags uses | |
| // index as-is, but catchBodies uses index-1. | |
| tryy->catchBodies[index - 1] = expr; | |
| } | |
| Result<> IRBuilder::visitCatch(Name tag) { | |
| auto scope = getScope(); | |
| bool wasTry = true; | |
| auto* tryy = scope.getTry(); | |
| if (!tryy) { | |
| wasTry = false; | |
| tryy = scope.getCatch(); | |
| } | |
| if (!tryy) { | |
| return Err{"unexpected catch"}; | |
| } | |
| auto index = scope.getIndex(); | |
| auto expr = finishScope(); | |
| CHECK_ERR(expr); | |
| if (wasTry) { | |
| tryy->body = *expr; | |
| } else { | |
| setCatchBody(tryy, *expr, index); | |
| } | |
| if (tryy->catchTags.size() == index) { | |
| tryy->catchTags.resize(tryy->catchTags.size() + 1); | |
| } | |
| tryy->catchTags[index] = tag; | |
| if (binaryPos && func) { | |
| auto& delimiterLocs = func->delimiterLocations[tryy]; | |
| delimiterLocs[delimiterLocs.size()] = lastBinaryPos - codeSectionOffset; | |
| if (wasTry) { | |
| func->expressionLocations[tryy].start = | |
| scope.startPos - codeSectionOffset; | |
| } | |
| } | |
| CHECK_ERR(pushScope(ScopeCtx::makeCatch(std::move(scope), tryy))); | |
| // Push a pop for the exception payload if necessary. | |
| auto params = wasm.getTag(tag)->params(); | |
| if (params != Type::none) { | |
| // Note that we have a pop to help determine later whether we need to run | |
| // the fixup for pops within blocks. | |
| scopeStack[0].notePop(); | |
| pushSynthetic(builder.makePop(params)); | |
| } | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::visitCatchAll() { | |
| auto scope = getScope(); | |
| bool wasTry = true; | |
| auto* tryy = scope.getTry(); | |
| if (!tryy) { | |
| wasTry = false; | |
| tryy = scope.getCatch(); | |
| } | |
| if (!tryy) { | |
| return Err{"unexpected catch"}; | |
| } | |
| auto index = scope.getIndex(); | |
| auto expr = finishScope(); | |
| CHECK_ERR(expr); | |
| if (wasTry) { | |
| tryy->body = *expr; | |
| } else { | |
| setCatchBody(tryy, *expr, index); | |
| } | |
| if (binaryPos && func) { | |
| auto& delimiterLocs = func->delimiterLocations[tryy]; | |
| delimiterLocs[delimiterLocs.size()] = lastBinaryPos - codeSectionOffset; | |
| if (wasTry) { | |
| func->expressionLocations[tryy].start = | |
| scope.startPos - codeSectionOffset; | |
| } | |
| } | |
| return pushScope(ScopeCtx::makeCatchAll(std::move(scope), tryy)); | |
| } | |
| Result<> IRBuilder::visitDelegate(Index label) { | |
| auto& scope = getScope(); | |
| auto* tryy = scope.getTry(); | |
| if (!tryy) { | |
| return Err{"unexpected delegate"}; | |
| } | |
| // In Binaryen IR, delegates can only target try or function scopes directly. | |
| // Search upward to find the nearest enclosing try or function scope. Since | |
| // the given label is relative the parent scope of the try, start by adjusting | |
| // it to be relative to the try scope. | |
| ++label; | |
| for (size_t size = scopeStack.size(); label < size; ++label) { | |
| auto& delegateScope = scopeStack[size - label - 1]; | |
| if (delegateScope.getTry()) { | |
| auto delegateName = getDelegateLabelName(label); | |
| CHECK_ERR(delegateName); | |
| tryy->delegateTarget = *delegateName; | |
| break; | |
| } else if (delegateScope.getFunction()) { | |
| tryy->delegateTarget = DELEGATE_CALLER_TARGET; | |
| break; | |
| } | |
| } | |
| if (label == scopeStack.size()) { | |
| return Err{"unexpected delegate"}; | |
| } | |
| // Delegate ends the try. | |
| return visitEnd(); | |
| } | |
| Result<> IRBuilder::visitEnd() { | |
| auto scope = getScope(); | |
| if (scope.isNone()) { | |
| return Err{"unexpected end"}; | |
| } | |
| if (auto* func = scope.getFunction()) { | |
| if (auto* loc = std::get_if<Function::DebugLocation>(&debugLoc)) { | |
| func->epilogLocation = *loc; | |
| } | |
| } | |
| debugLoc = CanReceiveDebug(); | |
| auto expr = finishScope(scope.getBlock()); | |
| CHECK_ERR(expr); | |
| bool isTry = scope.getTry() || scope.getCatch() || scope.getCatchAll(); | |
| auto& label = isTry ? scope.branchLabel : scope.label; | |
| auto blockType = scope.getResultType(); | |
| // When we wrap an expression then push the wrapper, we would end up with the | |
| // binary start location attached to the original expression and the end | |
| // location attached to the wrapper. Move the binary start location to the | |
| // wrapper to avoid splitting the span information. | |
| auto moveBinaryPosToWrapper = [&](Expression* original, Expression* wrapper) { | |
| if (!func) { | |
| return; | |
| } | |
| if (auto it = func->expressionLocations.find(original); | |
| it != func->expressionLocations.end()) { | |
| auto span = it->second; | |
| assert(span.end == 0); | |
| func->expressionLocations.erase(it); | |
| [[maybe_unused]] auto [_, inserted] = | |
| func->expressionLocations.insert({wrapper, span}); | |
| assert(inserted); | |
| } | |
| }; | |
| // If the scope expression cannot be directly labeled, we may need to wrap it | |
| // in a block. | |
| auto maybeWrapForLabel = [&](Expression* curr) -> Expression* { | |
| if (!label) { | |
| return curr; | |
| } | |
| auto* fixed = fixExtraOutput(scope, label, curr); | |
| // We can reuse unnamed blocks instead of wrapping them. | |
| if (auto* block = fixed->dynCast<Block>(); block && !block->name) { | |
| block->name = label; | |
| block->type = blockType; | |
| moveBinaryPosToWrapper(curr, block); | |
| return block; | |
| } | |
| auto* block = builder.makeBlock(); | |
| block->name = label; | |
| block->list.push_back(fixed); | |
| block->finalize(blockType, | |
| scope.labelUsed ? Block::HasBreak : Block::NoBreak); | |
| moveBinaryPosToWrapper(curr, block); | |
| return block; | |
| }; | |
| // The binary position we record for the block instruction should start at the | |
| // beginning of the block, not at the beginning of the `end`. | |
| lastBinaryPos = scope.startPos; | |
| if (auto* func = scope.getFunction()) { | |
| func->body = maybeWrapForLabel(*expr); | |
| labelDepths.clear(); | |
| if (scope.needsPopFixup()) { | |
| // We may be in the binary parser, where pops need to be fixed up before | |
| // we know that EH will be enabled. | |
| EHUtils::handleBlockNestedPops( | |
| func, wasm, EHUtils::FeaturePolicy::RunIfNoEH); | |
| } | |
| this->func = nullptr; | |
| blockHint = 0; | |
| labelHint = 0; | |
| } else if (auto* block = scope.getBlock()) { | |
| assert(*expr == block); | |
| block->name = Name(); | |
| block = fixExtraOutput(scope, label, block)->cast<Block>(); | |
| block->name = label; | |
| block->finalize(block->type, | |
| scope.labelUsed ? Block::HasBreak : Block::NoBreak); | |
| push(block); | |
| } else if (auto* loop = scope.getLoop()) { | |
| loop->body = fixExtraOutput(scope, label, *expr); | |
| loop->name = scope.label; | |
| if (scope.inputType != Type::none && scope.labelUsed) { | |
| // Branches to this loop carry values, but Binaryen IR does not support | |
| // that. Fix this by trampolining the branches through new code that sets | |
| // the branch value to the appropriate scratch local. | |
| fixLoopWithInput(loop, scope.inputType, scope.inputLocal); | |
| } | |
| loop->finalize(loop->type); | |
| push(loop); | |
| } else if (auto* iff = scope.getIf()) { | |
| iff->ifTrue = *expr; | |
| if (scope.inputType != Type::none) { | |
| // Normally an if without an else must have type none, but if there is an | |
| // input parameter, the empty else arm must propagate its value. | |
| // Synthesize an else arm that loads the value from the scratch local. | |
| iff->ifFalse = builder.makeLocalGet(scope.inputLocal, scope.inputType); | |
| } else { | |
| iff->ifFalse = nullptr; | |
| } | |
| iff->finalize(iff->type); | |
| push(maybeWrapForLabel(iff)); | |
| } else if (auto* iff = scope.getElse()) { | |
| iff->ifFalse = *expr; | |
| iff->finalize(iff->type); | |
| push(maybeWrapForLabel(iff)); | |
| } else if (auto* tryy = scope.getTry()) { | |
| tryy->body = *expr; | |
| tryy->name = scope.label; | |
| tryy->finalize(tryy->type); | |
| push(maybeWrapForLabel(tryy)); | |
| } else if (Try* tryy; | |
| (tryy = scope.getCatch()) || (tryy = scope.getCatchAll())) { | |
| auto index = scope.getIndex(); | |
| setCatchBody(tryy, *expr, index); | |
| tryy->name = scope.label; | |
| tryy->finalize(tryy->type); | |
| push(maybeWrapForLabel(tryy)); | |
| } else if (auto* trytable = scope.getTryTable()) { | |
| trytable->body = *expr; | |
| trytable->finalize(trytable->type, &wasm); | |
| push(maybeWrapForLabel(trytable)); | |
| } else { | |
| WASM_UNREACHABLE("unexpected scope kind"); | |
| } | |
| return Ok{}; | |
| } | |
| Result<std::pair<Index, Name>> | |
| IRBuilder::getExtraOutputLocalAndLabel(Index label, size_t extraArity) { | |
| auto scope = getScope(label); | |
| CHECK_ERR(scope); | |
| auto& s = **scope; | |
| auto i = extraArity - 1; | |
| if (s.outputLabels.size() < extraArity) { | |
| s.outputLocals.resize(extraArity); | |
| s.outputLabels.resize(extraArity); | |
| } | |
| if (!s.outputLabels[i]) { | |
| auto labelType = s.getLabelType(); | |
| auto it = labelType.begin(); | |
| Type extraType = Tuple(it, it + extraArity); | |
| auto local = addScratchLocal(extraType); | |
| CHECK_ERR(local); | |
| auto name = getLabelName(label); | |
| s.outputLocals[i] = *local; | |
| s.outputLabels[i] = makeFresh(*name); | |
| } | |
| return std::make_pair(s.outputLocals[i], s.outputLabels[i]); | |
| } | |
| // If there are branches to this scope that carried extra values in scratch | |
| // locals, we need to set up the trampolines those branches will go to. The | |
| // trampolines get the values out of the scratch locals and onto the stack in | |
| // the correct order. | |
| Expression* | |
| IRBuilder::fixExtraOutput(ScopeCtx& scope, Name label, Expression* curr) { | |
| // Add a trampoline branch target. Reuse unnamed blocks. | |
| auto addTrampoline = | |
| [&](Type receivedType, Name trampolineLabel, Name skipLabel) { | |
| if (auto* block = curr->dynCast<Block>(); block && !block->name) { | |
| block->name = trampolineLabel; | |
| if (block->list.back()->type == Type::none) { | |
| block->list.push_back(builder.makeBreak(skipLabel)); | |
| } else { | |
| block->list.back() = builder.makeBreak(skipLabel, block->list.back()); | |
| } | |
| block->type = receivedType; | |
| } else { | |
| assert(curr->type != Type::none); | |
| curr = builder.makeBlock( | |
| trampolineLabel, {builder.makeBreak(skipLabel, curr)}, receivedType); | |
| } | |
| }; | |
| auto labelType = scope.getLabelType(); | |
| Name fallthroughLabel; | |
| for (Index i = 0; i < scope.outputLabels.size(); ++i) { | |
| auto extraLabel = scope.outputLabels[i]; | |
| if (!extraLabel) { | |
| continue; | |
| } | |
| auto extraLocal = scope.outputLocals[i]; | |
| auto extraType = func->getLocalType(extraLocal); | |
| Type receivedType = | |
| Tuple(labelType.begin() + extraType.size(), labelType.end()); | |
| // For normal blocks, the original fallthrough values can be sent to the | |
| // scope label and they will end up in the right place, but for loops, the | |
| // fallthrough values should _not_ go to the scope label. We will add a new | |
| // branch target at the end to send the fallthrough values to. | |
| if (scope.getLoop() && !fallthroughLabel) { | |
| fallthroughLabel = makeFresh(label); | |
| addTrampoline(receivedType, extraLabel, fallthroughLabel); | |
| } else { | |
| addTrampoline(receivedType, extraLabel, label); | |
| } | |
| // If all the received values are in the scratch local, just fetch them out. | |
| if (receivedType == Type::none) { | |
| curr = builder.makeSequence( | |
| curr, builder.makeLocalGet(extraLocal, extraType), extraType); | |
| continue; | |
| } | |
| // Otherwise, we have to reorder the received values past the extra values | |
| // from the scratch local using an additional scratch local. | |
| auto receivedLocal = *addScratchLocal(receivedType); | |
| curr = builder.makeLocalSet(receivedLocal, curr); | |
| // Concatenate the extra values and received values into a tuple. | |
| std::vector<Expression*> elems; | |
| if (extraType.isSingle()) { | |
| elems.push_back(builder.makeLocalGet(extraLocal, extraType)); | |
| } else { | |
| for (Index j = 0; j < extraType.size(); ++j) { | |
| elems.push_back(builder.makeTupleExtract( | |
| builder.makeLocalGet(extraLocal, extraType), j)); | |
| } | |
| } | |
| if (receivedType.isSingle()) { | |
| elems.push_back(builder.makeLocalGet(receivedLocal, receivedType)); | |
| } else { | |
| for (Index j = 0; j < receivedType.size(); ++j) { | |
| elems.push_back(builder.makeTupleExtract( | |
| builder.makeLocalGet(receivedLocal, receivedType), j)); | |
| } | |
| } | |
| curr = builder.makeSequence(curr, builder.makeTupleMake(elems), labelType); | |
| } | |
| if (fallthroughLabel) { | |
| // The loop fallthrough values need to propagate to one final trampoline. | |
| addTrampoline(scope.getResultType(), fallthroughLabel, label); | |
| } | |
| return curr; | |
| } | |
| // Branches to this loop need to be trampolined through code that sets the value | |
| // carried by the branch to the appropriate scratch local before branching to | |
| // the loop. Transform this: | |
| // | |
| // (loop $l (param t1) (result t2) ...) | |
| // | |
| // to this: | |
| // | |
| // (loop $l0 (result t2) | |
| // (block $l1 (result t2) | |
| // (local.set $scratch ;; set the branch values to the scratch local | |
| // (block $l (result t1) | |
| // (br $l1 ;; exit the loop with the fallthrough value, if any. | |
| // ... ;; contains branches to $l | |
| // ) | |
| // ) | |
| // ) | |
| // (br $l0) ;; continue the loop | |
| // ) | |
| // ) | |
| void IRBuilder::fixLoopWithInput(Loop* loop, Type inputType, Index scratch) { | |
| auto l = loop->name; | |
| auto l0 = makeFresh(l, 0); | |
| auto l1 = makeFresh(l, 1); | |
| Block* inner = | |
| loop->type == Type::none | |
| ? builder.blockifyWithName( | |
| loop->body, l, builder.makeBreak(l1), inputType) | |
| : builder.makeBlock(l, {builder.makeBreak(l1, loop->body)}, inputType); | |
| Block* outer = builder.makeBlock( | |
| l1, | |
| {builder.makeLocalSet(scratch, inner), builder.makeBreak(l0)}, | |
| loop->type); | |
| loop->body = outer; | |
| loop->name = l0; | |
| } | |
| Result<Index> IRBuilder::getLabelIndex(Name label, bool inDelegate) { | |
| auto it = labelDepths.find(label); | |
| if (it == labelDepths.end() || it->second.empty()) { | |
| return Err{"unexpected label '"s + label.toString() + "'"}; | |
| } | |
| auto index = scopeStack.size() - it->second.back(); | |
| if (inDelegate) { | |
| if (index == 0) { | |
| // The real label we're referencing, if it exists, has been shadowed by | |
| // the `try`. Get the previous label with this name instead. For example: | |
| // | |
| // block $l | |
| // try $l | |
| // delegate $l | |
| // end | |
| // | |
| // The `delegate $l` should target the block, not the try, even though a | |
| // normal branch to $l in the try's scope would target the try. | |
| if (it->second.size() <= 1) { | |
| return Err{"unexpected self-referencing label '"s + label.toString() + | |
| "'"}; | |
| } | |
| index = scopeStack.size() - it->second[it->second.size() - 2]; | |
| assert(index != 0); | |
| } | |
| // Adjust the index to be relative to the try. | |
| --index; | |
| } | |
| return index; | |
| } | |
| Result<Name> IRBuilder::getLabelName(Index label, bool forDelegate) { | |
| auto scope = getScope(label); | |
| CHECK_ERR(scope); | |
| // For normal branches to try blocks, we need to use the secondary label. | |
| bool useTryBranchLabel = | |
| !forDelegate && | |
| ((*scope)->getTry() || (*scope)->getCatch() || (*scope)->getCatchAll()); | |
| auto& scopeLabel = | |
| useTryBranchLabel ? (*scope)->branchLabel : (*scope)->label; | |
| if (!scopeLabel) { | |
| // The scope does not already have a name, so we need to create one. | |
| if ((*scope)->getBlock()) { | |
| scopeLabel = makeFresh("block", blockHint++); | |
| } else { | |
| scopeLabel = makeFresh("label", labelHint++); | |
| } | |
| } | |
| if (!forDelegate) { | |
| (*scope)->labelUsed = true; | |
| } | |
| return scopeLabel; | |
| } | |
| Result<> IRBuilder::makeNop() { | |
| push(builder.makeNop()); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeBlock(Name label, Signature sig) { | |
| auto* block = wasm.allocator.alloc<Block>(); | |
| block->name = label; | |
| block->type = sig.results; | |
| return visitBlockStart(block, sig.params); | |
| } | |
| Result<> IRBuilder::makeIf(Name label, | |
| Signature sig, | |
| const CodeAnnotation& annotations) { | |
| auto* iff = wasm.allocator.alloc<If>(); | |
| iff->type = sig.results; | |
| applyAnnotations(iff, annotations); | |
| return visitIfStart(iff, label, sig.params); | |
| } | |
| Result<> IRBuilder::makeLoop(Name label, Signature sig) { | |
| auto* loop = wasm.allocator.alloc<Loop>(); | |
| loop->name = label; | |
| loop->type = sig.results; | |
| return visitLoopStart(loop, sig.params); | |
| } | |
| Result<> IRBuilder::makeBreak(Index label, | |
| bool isConditional, | |
| const CodeAnnotation& annotations) { | |
| auto name = getLabelName(label); | |
| CHECK_ERR(name); | |
| auto labelType = getLabelType(label); | |
| CHECK_ERR(labelType); | |
| Break curr; | |
| curr.name = *name; | |
| // Use a dummy condition value if we need to pop a condition. | |
| curr.condition = isConditional ? &curr : nullptr; | |
| CHECK_ERR(ChildPopper{*this}.visitBreak(&curr, *labelType)); | |
| auto* br = builder.makeBreak(curr.name, curr.value, curr.condition); | |
| applyAnnotations(br, annotations); | |
| push(br); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSwitch(const std::vector<Index>& labels, | |
| Index defaultLabel) { | |
| auto defaultType = getLabelType(defaultLabel); | |
| CHECK_ERR(defaultType); | |
| std::vector<Name> names; | |
| names.reserve(labels.size()); | |
| Type glbLabelType = *defaultType; | |
| for (auto label : labels) { | |
| auto name = getLabelName(label); | |
| CHECK_ERR(name); | |
| names.push_back(*name); | |
| auto type = getLabelType(label); | |
| CHECK_ERR(type); | |
| glbLabelType = Type::getGreatestLowerBound(glbLabelType, *type); | |
| } | |
| auto defaultName = getLabelName(defaultLabel); | |
| CHECK_ERR(defaultName); | |
| Switch curr(wasm.allocator); | |
| CHECK_ERR(ChildPopper{*this}.visitSwitch(&curr, glbLabelType)); | |
| push(builder.makeSwitch(names, *defaultName, curr.condition, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeCall(Name func, | |
| bool isReturn, | |
| const CodeAnnotation& annotations) { | |
| auto sig = wasm.getFunction(func)->getSig(); | |
| Call curr(wasm.allocator); | |
| curr.target = func; | |
| curr.operands.resize(sig.params.size()); | |
| CHECK_ERR(visitCall(&curr)); | |
| auto* call = | |
| builder.makeCall(curr.target, curr.operands, sig.results, isReturn); | |
| push(call); | |
| applyAnnotations(call, annotations); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeCallIndirect(Name table, | |
| HeapType type, | |
| bool isReturn, | |
| const CodeAnnotation& annotations) { | |
| if (!type.isSignature()) { | |
| return Err{"expected function type annotation on call_indirect"}; | |
| } | |
| CallIndirect curr(wasm.allocator); | |
| curr.heapType = type; | |
| curr.operands.resize(type.getSignature().params.size()); | |
| CHECK_ERR(visitCallIndirect(&curr)); | |
| auto* call = | |
| builder.makeCallIndirect(table, curr.target, curr.operands, type, isReturn); | |
| push(call); | |
| applyAnnotations(call, annotations); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeLocalGet(Index local) { | |
| if (!func) { | |
| return Err{"local.get is only valid in a function context"}; | |
| } | |
| if (local >= func->getNumLocals()) { | |
| return Err{"invalid local.get index"}; | |
| } | |
| push(builder.makeLocalGet(local, func->getLocalType(local))); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeLocalSet(Index local) { | |
| if (!func) { | |
| return Err{"local.set is only valid in a function context"}; | |
| } | |
| if (local >= func->getNumLocals()) { | |
| return Err{"invalid local.set index"}; | |
| } | |
| LocalSet curr; | |
| curr.index = local; | |
| CHECK_ERR(visitLocalSet(&curr)); | |
| push(builder.makeLocalSet(local, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeLocalTee(Index local) { | |
| if (!func) { | |
| return Err{"local.tee is only valid in a function context"}; | |
| } | |
| if (local >= func->getNumLocals()) { | |
| return Err{"invalid local.tee index"}; | |
| } | |
| LocalSet curr; | |
| curr.index = local; | |
| CHECK_ERR(visitLocalSet(&curr)); | |
| push(builder.makeLocalTee(local, curr.value, func->getLocalType(local))); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeGlobalGet(Name global) { | |
| push(builder.makeGlobalGet(global, wasm.getGlobal(global)->type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeGlobalSet(Name global) { | |
| GlobalSet curr; | |
| curr.name = global; | |
| CHECK_ERR(visitGlobalSet(&curr)); | |
| push(builder.makeGlobalSet(global, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeLoad(unsigned bytes, | |
| bool signed_, | |
| Address offset, | |
| unsigned align, | |
| Type type, | |
| Name mem) { | |
| Load curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitLoad(&curr)); | |
| push(builder.makeLoad(bytes, signed_, offset, align, curr.ptr, type, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStore( | |
| unsigned bytes, Address offset, unsigned align, Type type, Name mem) { | |
| Store curr; | |
| curr.memory = mem; | |
| curr.valueType = type; | |
| CHECK_ERR(visitStore(&curr)); | |
| push( | |
| builder.makeStore(bytes, offset, align, curr.ptr, curr.value, type, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicLoad(unsigned bytes, | |
| Address offset, | |
| Address align, | |
| Type type, | |
| Name mem, | |
| MemoryOrder order) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, bytes)); | |
| Load curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitLoad(&curr)); | |
| push(builder.makeAtomicLoad(bytes, offset, curr.ptr, type, mem, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicStore(unsigned bytes, | |
| Address offset, | |
| Address align, | |
| Type type, | |
| Name mem, | |
| MemoryOrder order) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, bytes)); | |
| Store curr; | |
| curr.memory = mem; | |
| curr.valueType = type; | |
| CHECK_ERR(visitStore(&curr)); | |
| push(builder.makeAtomicStore( | |
| bytes, offset, curr.ptr, curr.value, type, mem, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicRMW(AtomicRMWOp op, | |
| unsigned bytes, | |
| Address offset, | |
| Address align, | |
| Type type, | |
| Name mem, | |
| MemoryOrder order) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, bytes)); | |
| AtomicRMW curr; | |
| curr.memory = mem; | |
| curr.type = type; | |
| CHECK_ERR(visitAtomicRMW(&curr)); | |
| push(builder.makeAtomicRMW( | |
| op, bytes, offset, curr.ptr, curr.value, type, mem, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicCmpxchg(unsigned bytes, | |
| Address offset, | |
| Address align, | |
| Type type, | |
| Name mem, | |
| MemoryOrder order) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, bytes)); | |
| AtomicCmpxchg curr; | |
| curr.memory = mem; | |
| CHECK_ERR(ChildPopper{*this}.visitAtomicCmpxchg(&curr, type)); | |
| push(builder.makeAtomicCmpxchg(bytes, | |
| offset, | |
| curr.ptr, | |
| curr.expected, | |
| curr.replacement, | |
| type, | |
| mem, | |
| order)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeAtomicWait(Type type, Address offset, Address align, Name mem) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, type == Type::i32 ? 4 : 8)); | |
| AtomicWait curr; | |
| curr.memory = mem; | |
| curr.expectedType = type; | |
| CHECK_ERR(visitAtomicWait(&curr)); | |
| push(builder.makeAtomicWait( | |
| curr.ptr, curr.expected, curr.timeout, type, offset, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicNotify(Address offset, Address align, Name mem) { | |
| CHECK_ERR(requireNaturalAtomicAlign(align, 4)); | |
| AtomicNotify curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitAtomicNotify(&curr)); | |
| push(builder.makeAtomicNotify(curr.ptr, curr.notifyCount, offset, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeAtomicFence(MemoryOrder order) { | |
| push(builder.makeAtomicFence(order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makePause() { | |
| push(builder.makePause()); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDExtract(SIMDExtractOp op, uint8_t lane) { | |
| SIMDExtract curr; | |
| CHECK_ERR(visitSIMDExtract(&curr)); | |
| push(builder.makeSIMDExtract(op, curr.vec, lane)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDReplace(SIMDReplaceOp op, uint8_t lane) { | |
| SIMDReplace curr; | |
| curr.op = op; | |
| CHECK_ERR(visitSIMDReplace(&curr)); | |
| push(builder.makeSIMDReplace(op, curr.vec, lane, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDShuffle(const std::array<uint8_t, 16>& lanes) { | |
| SIMDShuffle curr; | |
| CHECK_ERR(visitSIMDShuffle(&curr)); | |
| push(builder.makeSIMDShuffle(curr.left, curr.right, lanes)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDTernary(SIMDTernaryOp op) { | |
| SIMDTernary curr; | |
| CHECK_ERR(visitSIMDTernary(&curr)); | |
| push(builder.makeSIMDTernary(op, curr.a, curr.b, curr.c)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDShift(SIMDShiftOp op) { | |
| SIMDShift curr; | |
| CHECK_ERR(visitSIMDShift(&curr)); | |
| push(builder.makeSIMDShift(op, curr.vec, curr.shift)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDLoad(SIMDLoadOp op, | |
| Address offset, | |
| unsigned align, | |
| Name mem) { | |
| SIMDLoad curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitSIMDLoad(&curr)); | |
| push(builder.makeSIMDLoad(op, offset, align, curr.ptr, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSIMDLoadStoreLane(SIMDLoadStoreLaneOp op, | |
| Address offset, | |
| unsigned align, | |
| uint8_t lane, | |
| Name mem) { | |
| SIMDLoadStoreLane curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitSIMDLoadStoreLane(&curr)); | |
| push(builder.makeSIMDLoadStoreLane( | |
| op, offset, align, lane, curr.ptr, curr.vec, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeMemoryInit(Name data, Name mem) { | |
| MemoryInit curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitMemoryInit(&curr)); | |
| push(builder.makeMemoryInit(data, curr.dest, curr.offset, curr.size, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeDataDrop(Name data) { | |
| push(builder.makeDataDrop(data)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeMemoryCopy(Name destMem, Name srcMem) { | |
| MemoryCopy curr; | |
| curr.destMemory = destMem; | |
| curr.sourceMemory = srcMem; | |
| CHECK_ERR(visitMemoryCopy(&curr)); | |
| push( | |
| builder.makeMemoryCopy(curr.dest, curr.source, curr.size, destMem, srcMem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeMemoryFill(Name mem) { | |
| MemoryFill curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitMemoryFill(&curr)); | |
| push(builder.makeMemoryFill(curr.dest, curr.value, curr.size, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeConst(Literal val) { | |
| push(builder.makeConst(val)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeUnary(UnaryOp op) { | |
| Unary curr; | |
| curr.op = op; | |
| CHECK_ERR(visitUnary(&curr)); | |
| push(builder.makeUnary(op, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeBinary(BinaryOp op) { | |
| Binary curr; | |
| curr.op = op; | |
| CHECK_ERR(visitBinary(&curr)); | |
| push(builder.makeBinary(op, curr.left, curr.right)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeWideIntAddSub(WideIntAddSubOp op) { | |
| WideIntAddSub curr; | |
| curr.op = op; | |
| CHECK_ERR(visitWideIntAddSub(&curr)); | |
| push(builder.makeWideIntAddSub( | |
| op, curr.leftLow, curr.leftHigh, curr.rightLow, curr.rightHigh)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeWideIntMul(WideIntMulOp op) { | |
| WideIntMul curr; | |
| curr.op = op; | |
| CHECK_ERR(visitWideIntMul(&curr)); | |
| push(builder.makeWideIntMul(op, curr.left, curr.right)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSelect(std::optional<Type> type) { | |
| Select curr; | |
| CHECK_ERR(visitSelect(&curr)); | |
| auto* built = builder.makeSelect(curr.condition, curr.ifTrue, curr.ifFalse); | |
| if (type && !Type::isSubType(built->type, *type)) { | |
| return Err{"select type does not match expected type"}; | |
| } | |
| push(built); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeDrop() { | |
| Drop curr; | |
| CHECK_ERR(ChildPopper{*this}.visitDrop(&curr, 1)); | |
| push(builder.makeDrop(curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeReturn() { | |
| if (!func) { | |
| return Err{"return is only valid in a function context"}; | |
| } | |
| Return curr; | |
| CHECK_ERR(visitReturn(&curr)); | |
| push(builder.makeReturn(curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeMemorySize(Name mem) { | |
| push(builder.makeMemorySize(mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeMemoryGrow(Name mem) { | |
| MemoryGrow curr; | |
| curr.memory = mem; | |
| CHECK_ERR(visitMemoryGrow(&curr)); | |
| push(builder.makeMemoryGrow(curr.delta, mem)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeUnreachable() { | |
| push(builder.makeUnreachable()); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makePop(Type type) { | |
| // We don't actually want to create a new Pop expression here because we | |
| // already create them automatically when starting a legacy catch block that | |
| // needs one. Just verify that the Pop we are being asked to make is the same | |
| // type as the Pop we have already made. | |
| auto& scope = getScope(); | |
| if (!scope.getCatch() || scope.exprStack.size() != 1 || | |
| !scope.exprStack[0]->is<Pop>()) { | |
| return Err{ | |
| "pop instructions may only appear at the beginning of catch blocks"}; | |
| } | |
| auto expectedType = scope.exprStack[0]->type; | |
| if (!Type::isSubType(expectedType, type)) { | |
| return Err{std::string("Expected pop of type ") + expectedType.toString()}; | |
| } | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefNull(HeapType type) { | |
| push(builder.makeRefNull(type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefIsNull() { | |
| RefIsNull curr; | |
| CHECK_ERR(visitRefIsNull(&curr)); | |
| push(builder.makeRefIsNull(curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefFunc(Name func) { | |
| push(builder.makeRefFunc(func)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefEq() { | |
| RefEq curr; | |
| CHECK_ERR(visitRefEq(&curr)); | |
| push(builder.makeRefEq(curr.left, curr.right)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableGet(Name table) { | |
| TableGet curr; | |
| curr.table = table; | |
| CHECK_ERR(visitTableGet(&curr)); | |
| auto type = wasm.getTable(table)->type; | |
| push(builder.makeTableGet(table, curr.index, type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableSet(Name table) { | |
| TableSet curr; | |
| curr.table = table; | |
| CHECK_ERR(visitTableSet(&curr)); | |
| push(builder.makeTableSet(table, curr.index, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableSize(Name table) { | |
| push(builder.makeTableSize(table)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableGrow(Name table) { | |
| TableGrow curr; | |
| curr.table = table; | |
| CHECK_ERR(visitTableGrow(&curr)); | |
| push(builder.makeTableGrow(table, curr.value, curr.delta)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableFill(Name table) { | |
| TableFill curr; | |
| curr.table = table; | |
| CHECK_ERR(visitTableFill(&curr)); | |
| push(builder.makeTableFill(table, curr.dest, curr.value, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableCopy(Name destTable, Name srcTable) { | |
| TableCopy curr; | |
| curr.destTable = destTable; | |
| curr.sourceTable = srcTable; | |
| CHECK_ERR(visitTableCopy(&curr)); | |
| push(builder.makeTableCopy( | |
| curr.dest, curr.source, curr.size, destTable, srcTable)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTableInit(Name elem, Name table) { | |
| TableInit curr; | |
| curr.table = table; | |
| CHECK_ERR(visitTableInit(&curr)); | |
| push(builder.makeTableInit(elem, curr.dest, curr.offset, curr.size, table)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeElemDrop(Name segment) { | |
| push(builder.makeElemDrop(segment)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTry(Name label, Signature sig) { | |
| auto* tryy = wasm.allocator.alloc<Try>(); | |
| tryy->type = sig.results; | |
| return visitTryStart(tryy, label, sig.params); | |
| } | |
| Result<> IRBuilder::makeTryTable(Name label, | |
| Signature sig, | |
| const std::vector<Name>& tags, | |
| const std::vector<Index>& labels, | |
| const std::vector<bool>& isRefs) { | |
| auto* trytable = wasm.allocator.alloc<TryTable>(); | |
| trytable->type = sig.results; | |
| trytable->catchTags.set(tags); | |
| trytable->catchRefs.set(isRefs); | |
| trytable->catchDests.reserve(labels.size()); | |
| for (auto label : labels) { | |
| auto name = getLabelName(label); | |
| CHECK_ERR(name); | |
| trytable->catchDests.push_back(*name); | |
| } | |
| return visitTryTableStart(trytable, label, sig.params); | |
| } | |
| Result<> IRBuilder::makeThrow(Name tag) { | |
| Throw curr(wasm.allocator); | |
| curr.tag = tag; | |
| curr.operands.resize(wasm.getTag(tag)->params().size()); | |
| CHECK_ERR(visitThrow(&curr)); | |
| push(builder.makeThrow(tag, curr.operands)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRethrow(Index label) { | |
| // Rethrow references `Try` labels directly, just like `delegate`. | |
| auto name = getDelegateLabelName(label); | |
| CHECK_ERR(name); | |
| push(builder.makeRethrow(*name)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeThrowRef() { | |
| ThrowRef curr; | |
| CHECK_ERR(visitThrowRef(&curr)); | |
| push(builder.makeThrowRef(curr.exnref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTupleMake(uint32_t arity) { | |
| if (arity < 2) { | |
| return Err{"tuple arity must be at least 2"}; | |
| } | |
| TupleMake curr(wasm.allocator); | |
| curr.operands.resize(arity); | |
| CHECK_ERR(visitTupleMake(&curr)); | |
| push(builder.makeTupleMake(curr.operands)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTupleExtract(uint32_t arity, uint32_t index) { | |
| if (index >= arity) { | |
| return Err{"tuple index out of bounds"}; | |
| } | |
| if (arity < 2) { | |
| return Err{"tuple arity must be at least 2"}; | |
| } | |
| TupleExtract curr; | |
| CHECK_ERR(ChildPopper{*this}.visitTupleExtract(&curr, arity)); | |
| push(builder.makeTupleExtract(curr.tuple, index)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeTupleDrop(uint32_t arity) { | |
| if (arity < 2) { | |
| return Err{"tuple arity must be at least 2"}; | |
| } | |
| Drop curr; | |
| CHECK_ERR(ChildPopper{*this}.visitDrop(&curr, arity)); | |
| push(builder.makeDrop(curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefI31(Shareability share) { | |
| RefI31 curr; | |
| CHECK_ERR(visitRefI31(&curr)); | |
| push(builder.makeRefI31(curr.value, share)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeI31Get(bool signed_) { | |
| I31Get curr; | |
| CHECK_ERR(visitI31Get(&curr)); | |
| push(builder.makeI31Get(curr.i31, signed_)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeCallRef(HeapType type, | |
| bool isReturn, | |
| const CodeAnnotation& annotations) { | |
| if (!type.isSignature()) { | |
| return Err{"expected function type annotation on call_ref"}; | |
| } | |
| CallRef curr(wasm.allocator); | |
| if (!type.isSignature()) { | |
| return Err{"expected function type"}; | |
| } | |
| auto sig = type.getSignature(); | |
| curr.operands.resize(type.getSignature().params.size()); | |
| CHECK_ERR(ChildPopper{*this}.visitCallRef(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.target)); | |
| auto* call = | |
| builder.makeCallRef(curr.target, curr.operands, sig.results, isReturn); | |
| push(call); | |
| applyAnnotations(call, annotations); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefTest(Type type) { | |
| RefTest curr; | |
| curr.castType = type; | |
| CHECK_ERR(visitRefTest(&curr)); | |
| push(builder.makeRefTest(curr.ref, type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefCast(Type type, bool isDesc) { | |
| if (!type.isCastable()) { | |
| return Err{"ref.cast cannot cast to invalid type"}; | |
| } | |
| std::optional<HeapType> descriptor; | |
| if (isDesc) { | |
| assert(type.isRef()); | |
| descriptor = type.getHeapType().getDescriptorType(); | |
| if (!descriptor) { | |
| return Err{"cast target must have descriptor"}; | |
| } | |
| } | |
| RefCast curr; | |
| curr.type = type; | |
| // Placeholder value to differentiate ref.cast_desc_eq. | |
| curr.desc = isDesc ? &curr : nullptr; | |
| CHECK_ERR(visitRefCast(&curr)); | |
| if (isDesc) { | |
| CHECK_ERR( | |
| validateTypeAnnotation(type.with(*descriptor).with(Nullable), curr.desc)); | |
| } | |
| push(builder.makeRefCast(curr.ref, curr.desc, type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefGetDesc(HeapType type) { | |
| RefGetDesc curr; | |
| if (!type.getDescriptorType()) { | |
| return Err{"expected type with descriptor"}; | |
| } | |
| CHECK_ERR(ChildPopper{*this}.visitRefGetDesc(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeRefGetDesc(curr.ref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeBrOn(Index label, | |
| BrOnOp op, | |
| std::optional<Type> in, | |
| std::optional<Type> out, | |
| const CodeAnnotation& annotations) { | |
| bool isCast = op != BrOnNull && op != BrOnNonNull; | |
| bool isDescCast = op == BrOnCastDescEq || op == BrOnCastDescEqFail; | |
| assert(bool(in) == bool(out) && bool(in) == isCast); | |
| std::optional<HeapType> descriptor; | |
| if (isDescCast) { | |
| assert(out && out->isRef()); | |
| descriptor = out->getHeapType().getDescriptorType(); | |
| if (!descriptor) { | |
| return Err{"cast target must have descriptor"}; | |
| } | |
| } | |
| BrOn curr; | |
| curr.op = op; | |
| curr.castType = isCast ? *out : Type::none; | |
| curr.desc = nullptr; | |
| if (isCast && !out->isCastable()) { | |
| return Err{"br_on cannot cast to invalid type"}; | |
| } | |
| CHECK_ERR(ChildPopper{*this}.visitBrOn(&curr, in, out)); | |
| // Validate things that would cause errors later. | |
| if (curr.ref->type != Type::unreachable && !curr.ref->type.isRef()) { | |
| return Err{"br_on* ref must be a ref"}; | |
| } | |
| if (curr.desc && curr.desc->type != Type::unreachable && | |
| !curr.desc->type.isRef()) { | |
| return Err{"br_on_cast_desc* must be a ref"}; | |
| } | |
| // Validate type immediates before we forget them. | |
| switch (op) { | |
| case BrOnNull: | |
| case BrOnNonNull: | |
| break; | |
| case BrOnCastDescEq: | |
| case BrOnCastDescEqFail: { | |
| CHECK_ERR(validateTypeAnnotation(out->with(*descriptor).with(Nullable), | |
| curr.desc)); | |
| } | |
| [[fallthrough]]; | |
| case BrOnCast: | |
| case BrOnCastFail: | |
| assert(in->isRef()); | |
| CHECK_ERR(validateTypeAnnotation(*in, curr.ref)); | |
| } | |
| // Extra values need to be sent in a scratch local. | |
| auto labelType = getLabelType(label); | |
| CHECK_ERR(labelType); | |
| auto extraArity = labelType->size(); | |
| switch (op) { | |
| case BrOnNull: | |
| // Modeled as sending no values. | |
| break; | |
| case BrOnNonNull: | |
| case BrOnCast: | |
| case BrOnCastFail: | |
| case BrOnCastDescEq: | |
| case BrOnCastDescEqFail: | |
| // Modeled as sending one value. | |
| if (extraArity == 0) { | |
| return Err{"br_on target does not expect a value"}; | |
| } | |
| extraArity -= 1; | |
| break; | |
| } | |
| // Before we can put the extra values into the output scratch local, we need | |
| // to stash the value under test in another scratch local. Find the type of | |
| // that local. | |
| Type testType; | |
| switch (op) { | |
| case BrOnNull: | |
| case BrOnNonNull: | |
| testType = curr.ref->type; | |
| break; | |
| case BrOnCast: | |
| case BrOnCastFail: | |
| case BrOnCastDescEq: | |
| case BrOnCastDescEqFail: | |
| testType = *in; | |
| break; | |
| } | |
| // If the value under test is unreachable, then we can proceed without putting | |
| // anything in locals since the branch will never be taken. The extra values | |
| // will just be dropped. We can't leave this optimization to DCE because we | |
| // wouldn't know what type to use for the scratch local if we tried to | |
| // continue. | |
| if (!extraArity || testType == Type::unreachable) { | |
| auto name = getLabelName(label); | |
| CHECK_ERR(name); | |
| auto* br = builder.makeBrOn(op, *name, curr.ref, curr.castType, curr.desc); | |
| applyAnnotations(br, annotations); | |
| push(br); | |
| return Ok{}; | |
| } | |
| auto testLocal = addScratchLocal(testType); | |
| CHECK_ERR(testLocal); | |
| // Put the value under test back on the stack and stash it. | |
| getScope().exprStack.push_back(curr.ref); | |
| CHECK_ERR(makeLocalSet(*testLocal)); | |
| // Now we can stash the extra values. | |
| auto info = getExtraOutputLocalAndLabel(label, extraArity); | |
| CHECK_ERR(info); | |
| auto [extraLocal, extraLabel] = *info; | |
| CHECK_ERR(makeLocalSet(extraLocal)); | |
| // Restore the test value. | |
| CHECK_ERR(makeLocalGet(*testLocal)); | |
| // Perform the branch. | |
| CHECK_ERR(visitBrOn(&curr)); | |
| auto* br = | |
| builder.makeBrOn(op, extraLabel, curr.ref, curr.castType, curr.desc); | |
| applyAnnotations(br, annotations); | |
| push(br); | |
| // If the branch wasn't taken, we need to leave the extra values on the | |
| // stack. For all instructions except br_on_non_null the extra values need | |
| // to be under the result value. br_on_non_null does not have a result | |
| // value, so it is simpler. | |
| if (op == BrOnNonNull) { | |
| CHECK_ERR(makeLocalGet(extraLocal)); | |
| return Ok{}; | |
| } | |
| Type resultType; | |
| switch (op) { | |
| case BrOnNull: | |
| resultType = Type(testType.getHeapType(), NonNullable); | |
| break; | |
| case BrOnNonNull: | |
| WASM_UNREACHABLE("unexpected op"); | |
| case BrOnCast: | |
| case BrOnCastDescEq: | |
| if (out->isNullable()) { | |
| resultType = Type(in->getHeapType(), NonNullable); | |
| } else { | |
| resultType = *in; | |
| } | |
| break; | |
| case BrOnCastFail: | |
| case BrOnCastDescEqFail: | |
| if (in->isNonNullable()) { | |
| resultType = Type(out->getHeapType(), NonNullable); | |
| } else { | |
| resultType = *out; | |
| } | |
| break; | |
| } | |
| auto resultLocal = addScratchLocal(resultType); | |
| CHECK_ERR(resultLocal); | |
| CHECK_ERR(makeLocalSet(*resultLocal)); | |
| CHECK_ERR(makeLocalGet(extraLocal)); | |
| CHECK_ERR(makeLocalGet(*resultLocal)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStructNew(HeapType type, bool isDesc) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.new"}; | |
| } | |
| if (isDesc && !type.getDescriptorType()) { | |
| return Err{"struct.new_desc of type without descriptor"}; | |
| } | |
| if (!isDesc && type.getDescriptorType()) { | |
| return Err{"type with descriptor requires struct.new_desc"}; | |
| } | |
| StructNew curr(wasm.allocator); | |
| curr.type = Type(type, NonNullable, Exact); | |
| curr.operands.resize(type.getStruct().fields.size()); | |
| CHECK_ERR(visitStructNew(&curr)); | |
| push(builder.makeStructNew(type, std::move(curr.operands), curr.desc)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStructNewDefault(HeapType type, bool isDesc) { | |
| if (isDesc && !type.getDescriptorType()) { | |
| return Err{"struct.new_default_desc of type without descriptor"}; | |
| } | |
| if (!isDesc && type.getDescriptorType()) { | |
| return Err{"type with descriptor requires struct.new_default_desc"}; | |
| } | |
| StructNew curr(wasm.allocator); | |
| curr.type = Type(type, NonNullable, Exact); | |
| CHECK_ERR(visitStructNew(&curr)); | |
| push(builder.makeStructNew(type, {}, curr.desc)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStructGet(HeapType type, | |
| Index field, | |
| bool signed_, | |
| MemoryOrder order) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.get"}; | |
| } | |
| const auto& fields = type.getStruct().fields; | |
| StructGet curr; | |
| CHECK_ERR(ChildPopper{*this}.visitStructGet(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push( | |
| builder.makeStructGet(field, curr.ref, order, fields[field].type, signed_)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeStructSet(HeapType type, Index field, MemoryOrder order) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.set"}; | |
| } | |
| StructSet curr; | |
| curr.index = field; | |
| CHECK_ERR(ChildPopper{*this}.visitStructSet(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeStructSet(field, curr.ref, curr.value, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStructRMW(AtomicRMWOp op, | |
| HeapType type, | |
| Index field, | |
| MemoryOrder order) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.atomic.rmw"}; | |
| } | |
| StructRMW curr; | |
| curr.index = field; | |
| CHECK_ERR(ChildPopper{*this}.visitStructRMW(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeStructRMW(op, field, curr.ref, curr.value, order)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeStructCmpxchg(HeapType type, Index field, MemoryOrder order) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.atomic.rmw"}; | |
| } | |
| StructCmpxchg curr; | |
| curr.index = field; | |
| CHECK_ERR(ChildPopper{*this}.visitStructCmpxchg(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeStructCmpxchg( | |
| field, curr.ref, curr.expected, curr.replacement, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStructWait(HeapType type, Index index) { | |
| if (!type.isStruct()) { | |
| return Err{"expected struct type annotation on struct.wait"}; | |
| } | |
| // This is likely checked in the caller by the `fieldidx` parser. | |
| if (index >= type.getStruct().fields.size()) { | |
| return Err{"struct.wait field index out of bounds"}; | |
| } | |
| StructWait curr(wasm.allocator); | |
| curr.index = index; | |
| CHECK_ERR(ChildPopper{*this}.visitStructWait(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeStructWait( | |
| index, curr.ref, curr.waitqueue, curr.expected, curr.timeout)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeWaitqueueNew() { | |
| WaitqueueNew curr(wasm.allocator); | |
| push(builder.makeWaitqueueNew()); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeWaitqueueNotify() { | |
| WaitqueueNotify curr(wasm.allocator); | |
| CHECK_ERR(ChildPopper{*this}.visitWaitqueueNotify(&curr)); | |
| push(builder.makeWaitqueueNotify(curr.waitqueue, curr.count)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makePublish() { | |
| Publish curr(wasm.allocator); | |
| CHECK_ERR(ChildPopper{*this}.visitPublish(&curr)); | |
| push(builder.makePublish(curr.ref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayNew(HeapType type) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.new"}; | |
| } | |
| ArrayNew curr; | |
| curr.type = Type(type, NonNullable, Exact); | |
| // Differentiate from array.new_default with dummy initializer. | |
| curr.init = (Expression*)0x01; | |
| CHECK_ERR(visitArrayNew(&curr)); | |
| push(builder.makeArrayNew(type, curr.size, curr.init)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayNewDefault(HeapType type) { | |
| ArrayNew curr; | |
| curr.init = nullptr; | |
| CHECK_ERR(visitArrayNew(&curr)); | |
| push(builder.makeArrayNew(type, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayNewData(HeapType type, Name data) { | |
| ArrayNewData curr; | |
| CHECK_ERR(visitArrayNewData(&curr)); | |
| push(builder.makeArrayNewData(type, data, curr.offset, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayNewElem(HeapType type, Name elem) { | |
| ArrayNewElem curr; | |
| CHECK_ERR(visitArrayNewElem(&curr)); | |
| push(builder.makeArrayNewElem(type, elem, curr.offset, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayNewFixed(HeapType type, uint32_t arity) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.new_fixed"}; | |
| } | |
| ArrayNewFixed curr(wasm.allocator); | |
| curr.type = Type(type, NonNullable); | |
| curr.values.resize(arity); | |
| CHECK_ERR(visitArrayNewFixed(&curr)); | |
| push(builder.makeArrayNewFixed(type, curr.values)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeArrayGet(HeapType type, bool signed_, MemoryOrder order) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.get"}; | |
| } | |
| ArrayGet curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayGet(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayGet( | |
| curr.ref, curr.index, order, type.getArray().element.type, signed_)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArraySet(HeapType type, MemoryOrder order) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.set"}; | |
| } | |
| ArraySet curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArraySet(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArraySet(curr.ref, curr.index, curr.value, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayStore(HeapType arrayType, | |
| unsigned bytes, | |
| Address offset, | |
| Address align, | |
| Type type) { | |
| if (!arrayType.isArray()) { | |
| return Err{"expected array type annotation on array store"}; | |
| } | |
| ArrayStore curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayStore(&curr, arrayType, type)); | |
| CHECK_ERR(validateTypeAnnotation(arrayType, curr.ref)); | |
| push(builder.makeArrayStore( | |
| bytes, offset, align, curr.ref, curr.index, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayLoad(HeapType arrayType, | |
| unsigned bytes, | |
| bool signed_, | |
| Address offset, | |
| Address align, | |
| Type type) { | |
| if (!arrayType.isArray()) { | |
| return Err{"expected array type annotation on array load"}; | |
| } | |
| ArrayLoad curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayLoad(&curr, arrayType)); | |
| CHECK_ERR(validateTypeAnnotation(arrayType, curr.ref)); | |
| push(builder.makeArrayLoad( | |
| bytes, signed_, offset, align, curr.ref, curr.index, type)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayLen() { | |
| ArrayLen curr; | |
| CHECK_ERR(visitArrayLen(&curr)); | |
| push(builder.makeArrayLen(curr.ref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayCopy(HeapType destType, HeapType srcType) { | |
| ArrayCopy curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayCopy(&curr, destType, srcType)); | |
| CHECK_ERR(validateTypeAnnotation(destType, curr.destRef)); | |
| CHECK_ERR(validateTypeAnnotation(srcType, curr.srcRef)); | |
| push(builder.makeArrayCopy( | |
| curr.destRef, curr.destIndex, curr.srcRef, curr.srcIndex, curr.length)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayFill(HeapType type) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.fill"}; | |
| } | |
| ArrayFill curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayFill(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayFill(curr.ref, curr.index, curr.value, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayInitData(HeapType type, Name data) { | |
| ArrayInitData curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayInitData(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayInitData( | |
| data, curr.ref, curr.index, curr.offset, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayInitElem(HeapType type, Name elem) { | |
| // Validate the elem type, too, before we potentially forget the type | |
| // annotation. | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.init_elem"}; | |
| } | |
| if (!Type::isSubType(wasm.getElementSegment(elem)->type, | |
| type.getArray().element.type)) { | |
| return Err{"element segment type must be a subtype of array element type " | |
| "on array.init_elem"}; | |
| } | |
| ArrayInitElem curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayInitElem(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayInitElem( | |
| elem, curr.ref, curr.index, curr.offset, curr.size)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeArrayRMW(AtomicRMWOp op, HeapType type, MemoryOrder order) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.atomic.rmw"}; | |
| } | |
| ArrayRMW curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayRMW(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayRMW(op, curr.ref, curr.index, curr.value, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeArrayCmpxchg(HeapType type, MemoryOrder order) { | |
| if (!type.isArray()) { | |
| return Err{"expected array type annotation on array.atomic.rmw"}; | |
| } | |
| ArrayCmpxchg curr; | |
| CHECK_ERR(ChildPopper{*this}.visitArrayCmpxchg(&curr, type)); | |
| CHECK_ERR(validateTypeAnnotation(type, curr.ref)); | |
| push(builder.makeArrayCmpxchg( | |
| curr.ref, curr.index, curr.expected, curr.replacement, order)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeRefAs(RefAsOp op) { | |
| RefAs curr; | |
| curr.op = op; | |
| CHECK_ERR(visitRefAs(&curr)); | |
| push(builder.makeRefAs(op, curr.value)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringNew(StringNewOp op) { | |
| StringNew curr; | |
| curr.op = op; | |
| if (op == StringNewFromCodePoint) { | |
| CHECK_ERR(visitStringNew(&curr)); | |
| push(builder.makeStringNew(op, curr.ref)); | |
| return Ok{}; | |
| } | |
| CHECK_ERR(visitStringNew(&curr)); | |
| push(builder.makeStringNew(op, curr.ref, curr.start, curr.end)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringConst(Name string) { | |
| push(builder.makeStringConst(string)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringMeasure(StringMeasureOp op) { | |
| StringMeasure curr; | |
| curr.op = op; | |
| CHECK_ERR(visitStringMeasure(&curr)); | |
| push(builder.makeStringMeasure(op, curr.ref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringEncode(StringEncodeOp op) { | |
| StringEncode curr; | |
| curr.op = op; | |
| CHECK_ERR(visitStringEncode(&curr)); | |
| push(builder.makeStringEncode(op, curr.str, curr.array, curr.start)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringConcat() { | |
| StringConcat curr; | |
| CHECK_ERR(visitStringConcat(&curr)); | |
| push(builder.makeStringConcat(curr.left, curr.right)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringEq(StringEqOp op) { | |
| StringEq curr; | |
| CHECK_ERR(visitStringEq(&curr)); | |
| push(builder.makeStringEq(op, curr.left, curr.right)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringTest() { | |
| StringTest curr; | |
| CHECK_ERR(visitStringTest(&curr)); | |
| push(builder.makeStringTest(curr.ref)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringWTF16Get() { | |
| StringWTF16Get curr; | |
| CHECK_ERR(visitStringWTF16Get(&curr)); | |
| push(builder.makeStringWTF16Get(curr.ref, curr.pos)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStringSliceWTF() { | |
| StringSliceWTF curr; | |
| CHECK_ERR(visitStringSliceWTF(&curr)); | |
| push(builder.makeStringSliceWTF(curr.ref, curr.start, curr.end)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeContNew(HeapType type) { | |
| if (!type.isContinuation()) { | |
| return Err{"expected continuation type"}; | |
| } | |
| ContNew curr; | |
| curr.type = Type(type, NonNullable); | |
| CHECK_ERR(visitContNew(&curr)); | |
| push(builder.makeContNew(type, curr.func)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeContBind(HeapType sourceType, HeapType targetType) { | |
| if (!sourceType.isContinuation() || !targetType.isContinuation()) { | |
| return Err{"expected continuation type annotations on cont.bind"}; | |
| } | |
| ContBind curr(wasm.allocator); | |
| curr.type = Type(targetType, NonNullable); | |
| size_t sourceParams = | |
| sourceType.getContinuation().type.getSignature().params.size(); | |
| size_t targetParams = | |
| targetType.getContinuation().type.getSignature().params.size(); | |
| if (sourceParams < targetParams) { | |
| return Err{"incompatible continuation types in cont.bind: source type " + | |
| sourceType.toString() + " has fewer parameters than target " + | |
| targetType.toString()}; | |
| } | |
| curr.operands.resize(sourceParams - targetParams); | |
| CHECK_ERR(ChildPopper{*this}.visitContBind(&curr, sourceType, targetType)); | |
| CHECK_ERR(validateTypeAnnotation(sourceType, curr.cont)); | |
| CHECK_ERR(validateTypeAnnotation(targetType, &curr)); | |
| push(builder.makeContBind(targetType, std::move(curr.operands), curr.cont)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeSuspend(Name tag) { | |
| Suspend curr(wasm.allocator); | |
| curr.tag = tag; | |
| curr.operands.resize(wasm.getTag(tag)->params().size()); | |
| CHECK_ERR(visitSuspend(&curr)); | |
| std::vector<Expression*> operands(curr.operands.begin(), curr.operands.end()); | |
| push(builder.makeSuspend(tag, operands)); | |
| return Ok{}; | |
| } | |
| struct ResumeTable { | |
| std::vector<Name> targets; | |
| std::vector<Type> sentTypes; | |
| }; | |
| static Result<ResumeTable> | |
| makeResumeTable(const std::vector<std::optional<Index>>& labels, | |
| std::function<Result<Name>(Index)> getLabelName, | |
| std::function<Result<Type>(Index)> getLabelType) { | |
| std::vector<Name> targets; | |
| targets.reserve(labels.size()); | |
| std::vector<Type> sentTypes; | |
| sentTypes.reserve(sentTypes.size()); | |
| for (Index i = 0; i < labels.size(); i++) { | |
| Name target; | |
| Type sentType; | |
| if (labels[i].has_value()) { | |
| // (on $tag $label) clause | |
| Index labelIndex = labels[i].value(); | |
| Result<Name> name = getLabelName(labelIndex); | |
| CHECK_ERR(name); | |
| target = *name; | |
| Result<Type> targetType = getLabelType(labelIndex); | |
| CHECK_ERR(targetType); | |
| if (targetType->isContinuation()) { | |
| sentType = *targetType; | |
| } else if (targetType->isTuple() && | |
| targetType->getTuple().back().isContinuation()) { | |
| // The continuation type is expected to be the last element of | |
| // a multi-valued block. | |
| sentType = *targetType; | |
| } else { | |
| return Err{"expected continuation type"}; | |
| } | |
| } else { | |
| // (on $tag switch) clause | |
| target = Name(); | |
| sentType = Type::none; | |
| } | |
| targets.push_back(target); | |
| sentTypes.push_back(sentType); | |
| } | |
| return ResumeTable{std::move(targets), std::move(sentTypes)}; | |
| } | |
| Result<> | |
| IRBuilder::makeResume(HeapType ct, | |
| const std::vector<Name>& tags, | |
| const std::vector<std::optional<Index>>& labels) { | |
| if (tags.size() != labels.size()) { | |
| return Err{"the sizes of tags and labels must be equal"}; | |
| } | |
| if (!ct.isContinuation()) { | |
| return Err{"expected continuation type annotation on resume"}; | |
| } | |
| Resume curr(wasm.allocator); | |
| auto contSig = ct.getContinuation().type.getSignature(); | |
| curr.operands.resize(contSig.params.size()); | |
| Result<ResumeTable> resumetable = makeResumeTable( | |
| labels, | |
| [this](Index i) { return this->getLabelName(i); }, | |
| [this](Index i) { return this->getLabelType(i); }); | |
| CHECK_ERR(resumetable); | |
| CHECK_ERR(ChildPopper{*this}.visitResume(&curr, ct)); | |
| CHECK_ERR(validateTypeAnnotation(ct, curr.cont)); | |
| push(builder.makeResume(tags, | |
| resumetable->targets, | |
| resumetable->sentTypes, | |
| std::move(curr.operands), | |
| curr.cont, | |
| ct)); | |
| return Ok{}; | |
| } | |
| Result<> | |
| IRBuilder::makeResumeThrow(HeapType ct, | |
| Name tag, | |
| const std::vector<Name>& tags, | |
| const std::vector<std::optional<Index>>& labels) { | |
| if (tags.size() != labels.size()) { | |
| return Err{"the sizes of tags and labels must be equal"}; | |
| } | |
| if (!ct.isContinuation()) { | |
| return Err{"expected continuation type annotation on resume_throw"}; | |
| } | |
| ResumeThrow curr(wasm.allocator); | |
| curr.tag = tag; | |
| if (tag) { | |
| // This is a normal resume_throw. | |
| curr.operands.resize(wasm.getTag(tag)->params().size()); | |
| } else { | |
| // This is a resume_throw_ref. | |
| curr.operands.resize(1); | |
| } | |
| Result<ResumeTable> resumetable = makeResumeTable( | |
| labels, | |
| [this](Index i) { return this->getLabelName(i); }, | |
| [this](Index i) { return this->getLabelType(i); }); | |
| CHECK_ERR(resumetable); | |
| CHECK_ERR(ChildPopper{*this}.visitResumeThrow(&curr, ct)); | |
| CHECK_ERR(validateTypeAnnotation(ct, curr.cont)); | |
| push(builder.makeResumeThrow(tag, | |
| tags, | |
| resumetable->targets, | |
| resumetable->sentTypes, | |
| std::move(curr.operands), | |
| curr.cont, | |
| ct)); | |
| return Ok{}; | |
| } | |
| Result<> IRBuilder::makeStackSwitch(HeapType ct, Name tag) { | |
| if (!ct.isContinuation()) { | |
| return Err{"expected continuation type annotation on switch"}; | |
| } | |
| StackSwitch curr(wasm.allocator); | |
| curr.tag = tag; | |
| auto nparams = ct.getContinuation().type.getSignature().params.size(); | |
| if (nparams < 1) { | |
| return Err{"arity mismatch: the continuation argument must have, at least, " | |
| "unary arity"}; | |
| } | |
| // The continuation argument of the continuation is synthetic, | |
| // i.e. it is provided by the runtime. | |
| curr.operands.resize(nparams - 1); | |
| CHECK_ERR(ChildPopper{*this}.visitStackSwitch(&curr, ct)); | |
| CHECK_ERR(validateTypeAnnotation(ct, curr.cont)); | |
| push(builder.makeStackSwitch(tag, std::move(curr.operands), curr.cont)); | |
| return Ok{}; | |
| } | |
| void IRBuilder::applyAnnotations(Expression* expr, | |
| const CodeAnnotation& annotation) { | |
| if (annotation.branchLikely) { | |
| // Branches are only possible inside functions. | |
| assert(func); | |
| func->codeAnnotations[expr].branchLikely = annotation.branchLikely; | |
| } | |
| if (annotation.inline_) { | |
| assert(func); | |
| func->codeAnnotations[expr].inline_ = annotation.inline_; | |
| } | |
| if (annotation.removableIfUnused) { | |
| assert(func); | |
| func->codeAnnotations[expr].removableIfUnused = true; | |
| } | |
| if (annotation.jsCalled) { | |
| assert(func); | |
| func->codeAnnotations[expr].jsCalled = true; | |
| } | |
| if (annotation.idempotent) { | |
| assert(func); | |
| func->codeAnnotations[expr].idempotent = true; | |
| } | |
| if (annotation.toolchainInline) { | |
| assert(func); | |
| func->codeAnnotations[expr].toolchainInline = annotation.toolchainInline; | |
| } | |
| } | |
| } // namespace wasm |