| /* |
| * Copyright 2018 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. |
| */ |
| |
| // |
| // Souperify - convert to Souper IR in text form. |
| // |
| // This needs 'flatten' to be run before it, as it assumes the IR is in |
| // flat form. You may also want to optimize a little, e.g. |
| // --flatten --simplify-locals-nonesting --reorder-locals |
| // (as otherwise flattening introduces many copies; we do ignore boring |
| // copies here, but they end up as identical LHSes). |
| // |
| // See https://github.com/google/souper/issues/323 |
| // |
| // TODO: |
| // * pcs and blockpcs for things other than ifs |
| // * Investigate 'inlining', adding in nodes through calls |
| // * Consider generalizing DataFlow IR for internal Binaryen use. |
| // * Automatic conversion of Binaryen IR opts to run on the DataFlow IR. |
| // This would subsume precompute-propagate, for example. Using DFIR we |
| // can "expand" the BIR into expressions that BIR opts can handle |
| // directly, without the need for *-propagate techniques. |
| // |
| |
| #include "dataflow/graph.h" |
| #include "dataflow/node.h" |
| #include "dataflow/utils.h" |
| #include "ir/flat.h" |
| #include "ir/local-graph.h" |
| #include "ir/utils.h" |
| #include "pass.h" |
| #include "wasm-builder.h" |
| #include "wasm.h" |
| |
| namespace wasm { |
| |
| static int debug() { |
| static char* str = getenv("BINARYEN_DEBUG_SOUPERIFY"); |
| static int ret = str ? atoi(str) : 0; |
| return ret; |
| } |
| |
| namespace DataFlow { |
| |
| // Internal helper to find all the uses of a set. |
| struct UseFinder { |
| // Gets a list of all the uses of an expression. As we are in flat IR, |
| // the expression must be the value of a set, and we seek the other sets |
| // (or rather, their values) that contain a get that uses that value. |
| // There may also be non-set uses of the value, for example in a drop |
| // or a return. We represent those with a nullptr, meaning "other". |
| std::vector<Expression*> |
| getUses(Expression* origin, Graph& graph, LocalGraph& localGraph) { |
| if (debug() >= 2) { |
| std::cout << "getUses\n" << origin << '\n'; |
| } |
| std::vector<Expression*> ret; |
| auto* set = graph.getSet(origin); |
| if (!set) { |
| // If the parent is not a set (a drop, call, return, etc.) then |
| // it is not something we need to track. |
| return ret; |
| } |
| addSetUses(set, graph, localGraph, ret); |
| return ret; |
| } |
| |
| // There may be loops of sets with copies between them. |
| std::unordered_set<LocalSet*> seenSets; |
| |
| void addSetUses(LocalSet* set, |
| Graph& graph, |
| LocalGraph& localGraph, |
| std::vector<Expression*>& ret) { |
| // If already handled, nothing to do here. |
| if (!seenSets.emplace(set).second) { |
| return; |
| } |
| // Find all the uses of that set. |
| auto& gets = localGraph.getSetInfluences(set); |
| if (debug() >= 2) { |
| std::cout << "addSetUses for " << set << ", " << gets.size() << " gets\n"; |
| } |
| for (auto* get : gets) { |
| // Each of these relevant gets is either |
| // (1) a child of a set, which we can track, or |
| // (2) not a child of a set, e.g., a call argument or such |
| auto& sets = localGraph.getGetInfluences(get); // TODO: iterator |
| // In flat IR, each get can influence at most 1 set. |
| assert(sets.size() <= 1); |
| if (sets.size() == 0) { |
| // This get is not the child of a set. Check if it is a drop, |
| // otherwise it is an actual use, and so an external use. |
| auto* parent = graph.getParent(get); |
| if (parent && parent->is<Drop>()) { |
| // Just ignore it. |
| } else { |
| ret.push_back(nullptr); |
| if (debug() >= 2) { |
| std::cout << "add nullptr\n"; |
| } |
| } |
| } else { |
| // This get is the child of a set. |
| auto* subSet = *sets.begin(); |
| // If this is a copy, we need to look through it: data-flow IR |
| // counts actual values, not copies, and in particular we need |
| // to look through the copies that implement a phi. |
| if (subSet->value == get) { |
| // Indeed a copy. |
| // TODO: this could be optimized and done all at once beforehand. |
| addSetUses(subSet, graph, localGraph, ret); |
| } else { |
| // Not a copy. |
| auto* value = subSet->value; |
| ret.push_back(value); |
| if (debug() >= 2) { |
| std::cout << "add a value\n" << value << '\n'; |
| } |
| } |
| } |
| } |
| } |
| }; |
| |
| // Generates a trace: all the information to generate a Souper LHS |
| // for a specific local.set whose value we want to infer. |
| struct Trace { |
| Graph& graph; |
| Node* toInfer; |
| // Nodes we should exclude from being children of traces (but they |
| // may be the root we try to infer. |
| std::unordered_set<Node*>& excludeAsChildren; |
| |
| // A limit on how deep we go - we don't want to create arbitrarily |
| // large traces. |
| size_t depthLimit = 10; |
| size_t totalLimit = 30; |
| |
| bool bad = false; |
| std::vector<Node*> nodes; |
| std::unordered_set<Node*> addedNodes; |
| std::vector<Node*> pathConditions; |
| // When we need to (like when the depth is too deep), we replace |
| // expressions with other expressions, and track them here. |
| std::unordered_map<Node*, std::unique_ptr<Node>> replacements; |
| // The nodes that have additional external uses (only computed |
| // for the "work" nodes, not the descriptive nodes arriving for |
| // path conditions). |
| std::unordered_set<Node*> hasExternalUses; |
| // We add path conditions after the "work". We collect them here |
| // and then go through them at the proper time. |
| std::vector<Node*> conditionsToAdd; |
| // Whether we are at the adding-conditions stage (i.e., post |
| // adding the "work"). |
| bool addingConditions = false; |
| // The local information graph. Used to check if a node has external uses. |
| LocalGraph& localGraph; |
| |
| Trace(Graph& graph, |
| Node* toInfer, |
| std::unordered_set<Node*>& excludeAsChildren, |
| LocalGraph& localGraph) |
| : graph(graph), toInfer(toInfer), excludeAsChildren(excludeAsChildren), |
| localGraph(localGraph) { |
| if (debug() >= 2) { |
| std::cout << "\nstart a trace (in " << graph.func->name << ")\n"; |
| } |
| // Check if there is a depth limit override |
| auto* depthLimitStr = getenv("BINARYEN_SOUPERIFY_DEPTH_LIMIT"); |
| if (depthLimitStr) { |
| depthLimit = atoi(depthLimitStr); |
| } |
| auto* totalLimitStr = getenv("BINARYEN_SOUPERIFY_TOTAL_LIMIT"); |
| if (totalLimitStr) { |
| totalLimit = atoi(totalLimitStr); |
| } |
| // Pull in all the dependencies, starting from the value itself. |
| add(toInfer, 0); |
| if (bad) { |
| return; |
| } |
| // If we are trivial before adding pcs, we are still trivial, and |
| // can ignore this. |
| auto sizeBeforePathConditions = nodes.size(); |
| // No input is uninteresting |
| if (sizeBeforePathConditions == 0) { |
| bad = true; |
| return; |
| } |
| // Just a var is uninteresting. TODO: others too? |
| if (sizeBeforePathConditions == 1 && nodes[0]->isVar()) { |
| bad = true; |
| return; |
| } |
| // Before adding the path conditions, we can now compute the |
| // actual number of uses of "work" nodes, the real computation done |
| // here and that we hope to replace, as opposed to path condition |
| // computation which is only descriptive and helps optimization of |
| // the work. |
| findExternalUses(); |
| // We can now add conditions. |
| addingConditions = true; |
| for (auto* condition : conditionsToAdd) { |
| add(condition, 0); |
| } |
| // Add in path conditions based on the location of this node: e.g. |
| // if it is inside an if's true branch, we can add a path-condition |
| // for that. |
| auto iter = graph.nodeParentMap.find(toInfer); |
| if (iter != graph.nodeParentMap.end()) { |
| addPath(toInfer, iter->second); |
| } |
| } |
| |
| Node* add(Node* node, size_t depth) { |
| depth++; |
| // If replaced, return the replacement. |
| auto iter = replacements.find(node); |
| if (iter != replacements.end()) { |
| return iter->second.get(); |
| } |
| // If already added, nothing more to do. |
| if (addedNodes.find(node) != addedNodes.end()) { |
| return node; |
| } |
| switch (node->type) { |
| case Node::Type::Var: { |
| break; // nothing more to add |
| } |
| case Node::Type::Expr: { |
| // If this is a Const, it's not an instruction - nothing to add, |
| // it's just a value. |
| if (node->expr->is<Const>()) { |
| return node; |
| } |
| // If we've gone too deep, emit a var instead. |
| // Do the same if this is a node we should exclude from traces. |
| if (depth >= depthLimit || nodes.size() >= totalLimit || |
| (node != toInfer && |
| excludeAsChildren.find(node) != excludeAsChildren.end())) { |
| auto type = node->getWasmType(); |
| assert(type.isConcrete()); |
| auto* var = Node::makeVar(type); |
| replacements[node] = std::unique_ptr<Node>(var); |
| node = var; |
| break; |
| } |
| // Add the dependencies. |
| assert(!node->expr->is<LocalGet>()); |
| for (Index i = 0; i < node->values.size(); i++) { |
| add(node->getValue(i), depth); |
| } |
| break; |
| } |
| case Node::Type::Phi: { |
| auto* block = add(node->getValue(0), depth); |
| assert(block); |
| auto size = block->values.size(); |
| // First, add the conditions for the block |
| for (Index i = 0; i < size; i++) { |
| // a condition may be bad, but conditions are not necessary - |
| // we can proceed without the extra condition information |
| auto* condition = block->getValue(i); |
| if (!condition->isBad()) { |
| if (!addingConditions) { |
| // Too early, queue it for later. |
| conditionsToAdd.push_back(condition); |
| } else { |
| add(condition, depth); |
| } |
| } |
| } |
| // Then, add the phi values |
| for (Index i = 1; i < size + 1; i++) { |
| add(node->getValue(i), depth); |
| } |
| break; |
| } |
| case Node::Type::Cond: { |
| add(node->getValue(0), depth); // add the block |
| add(node->getValue(1), depth); // add the node |
| break; |
| } |
| case Node::Type::Block: { |
| break; // nothing more to add |
| } |
| case Node::Type::Zext: { |
| add(node->getValue(0), depth); |
| break; |
| } |
| case Node::Type::Bad: { |
| bad = true; |
| return nullptr; |
| } |
| default: |
| WASM_UNREACHABLE("unexpected node type"); |
| } |
| // Assert on no cycles |
| assert(addedNodes.find(node) == addedNodes.end()); |
| nodes.push_back(node); |
| addedNodes.insert(node); |
| return node; |
| } |
| |
| void addPath(Node* node, Expression* curr) { |
| // We track curr and parent, which are always in the state of parent |
| // being the parent of curr. |
| auto* parent = graph.expressionParentMap.at(curr); |
| while (parent) { |
| auto iter = graph.expressionConditionMap.find(parent); |
| if (iter != graph.expressionConditionMap.end()) { |
| // Given the block, add a proper path-condition |
| addPathTo(parent, curr, iter->second); |
| } |
| curr = parent; |
| parent = graph.expressionParentMap.at(parent); |
| } |
| } |
| |
| // curr is a child of parent, and parent has a Block which we are |
| // give as 'node'. Add a path condition for reaching the child. |
| void addPathTo(Expression* parent, |
| Expression* curr, |
| std::vector<Node*> conditions) { |
| if (auto* iff = parent->dynCast<If>()) { |
| Index index; |
| if (curr == iff->ifTrue) { |
| index = 0; |
| } else if (curr == iff->ifFalse) { |
| index = 1; |
| } else { |
| WASM_UNREACHABLE("invalid expr"); |
| } |
| auto* condition = conditions[index]; |
| // Add the condition itself as an instruction in the trace - |
| // the pc uses it as its input. |
| add(condition, 0); |
| // Add it as a pc, which we will emit directly. |
| pathConditions.push_back(condition); |
| } else { |
| WASM_UNREACHABLE("invalid expr"); |
| } |
| } |
| |
| bool isBad() { return bad; } |
| |
| static bool isTraceable(Node* node) { |
| if (!node->origin) { |
| // Ignore artificial etc. nodes. |
| // TODO: perhaps require all the nodes for an origin appear, so we |
| // don't try to compute an internal part of one, like the |
| // extra artificial != 0 of a select? |
| return false; |
| } |
| if (node->isExpr()) { |
| // Consider only the simple computational nodes. |
| auto* expr = node->expr; |
| return expr->is<Unary>() || expr->is<Binary>() || expr->is<Select>(); |
| } |
| return false; |
| } |
| |
| void findExternalUses() { |
| // Find all the wasm code represented in this trace. |
| std::unordered_set<Expression*> origins; |
| for (auto& node : nodes) { |
| if (auto* origin = node->origin) { |
| if (debug() >= 2) { |
| std::cout << "note origin " << origin << '\n'; |
| } |
| origins.insert(origin); |
| } |
| } |
| for (auto& node : nodes) { |
| if (node == toInfer) { |
| continue; |
| } |
| if (auto* origin = node->origin) { |
| auto uses = UseFinder().getUses(origin, graph, localGraph); |
| for (auto* use : uses) { |
| // A non-set use (a drop or return etc.) is definitely external. |
| // Otherwise, check if internal or external. |
| if (use == nullptr || origins.count(use) == 0) { |
| if (debug() >= 2) { |
| std::cout << "found external use for\n"; |
| dump(node, std::cout); |
| std::cout << " due to " << use << '\n'; |
| } |
| hasExternalUses.insert(node); |
| break; |
| } |
| } |
| } |
| } |
| } |
| }; |
| |
| // Emits a trace, which is basically a Souper LHS. |
| struct Printer { |
| Graph& graph; |
| Trace& trace; |
| |
| // Each Node in a trace has an index, from 0. |
| std::unordered_map<Node*, Index> indexing; |
| |
| bool printedHasExternalUses = false; |
| |
| Printer(Graph& graph, Trace& trace) : graph(graph), trace(trace) { |
| std::cout << "\n; start LHS (in " << graph.func->name << ")\n"; |
| // Index the nodes. |
| for (auto* node : trace.nodes) { |
| // pcs and blockpcs are not instructions and do not need to be indexed |
| if (!node->isCond()) { |
| auto index = indexing.size(); |
| indexing[node] = index; |
| } |
| } |
| // Print them out. |
| for (auto* node : trace.nodes) { |
| print(node); |
| } |
| // Print out pcs. |
| for (auto* condition : trace.pathConditions) { |
| printPathCondition(condition); |
| } |
| |
| // Finish up |
| std::cout << "infer %" << indexing[trace.toInfer] << "\n\n"; |
| } |
| |
| Node* getMaybeReplaced(Node* node) { |
| auto iter = trace.replacements.find(node); |
| if (iter != trace.replacements.end()) { |
| return iter->second.get(); |
| } |
| return node; |
| } |
| |
| void print(Node* node) { |
| // The node may have been replaced during trace building, if so then |
| // print the proper replacement. |
| node = getMaybeReplaced(node); |
| assert(node); |
| switch (node->type) { |
| case Node::Type::Var: { |
| std::cout << "%" << indexing[node] << ":" << node->wasmType << " = var"; |
| break; // nothing more to add |
| } |
| case Node::Type::Expr: { |
| if (debug()) { |
| std::cout << "; "; |
| std::cout << *node->expr << '\n'; |
| } |
| std::cout << "%" << indexing[node] << " = "; |
| printExpression(node); |
| break; |
| } |
| case Node::Type::Phi: { |
| auto* block = node->getValue(0); |
| auto size = block->values.size(); |
| std::cout << "%" << indexing[node] << " = phi %" << indexing[block]; |
| for (Index i = 1; i < size + 1; i++) { |
| std::cout << ", "; |
| printInternal(node->getValue(i)); |
| } |
| break; |
| } |
| case Node::Type::Cond: { |
| std::cout << "blockpc %" << indexing[node->getValue(0)] << ' ' |
| << node->index << ' '; |
| printInternal(node->getValue(1)); |
| std::cout << " 1:i1"; |
| break; |
| } |
| case Node::Type::Block: { |
| std::cout << "%" << indexing[node] << " = block " |
| << node->values.size(); |
| break; |
| } |
| case Node::Type::Zext: { |
| auto* child = node->getValue(0); |
| std::cout << "%" << indexing[node] << ':' << child->getWasmType(); |
| std::cout << " = zext "; |
| printInternal(child); |
| break; |
| } |
| case Node::Type::Bad: { |
| WASM_UNREACHABLE("!!!BAD!!!"); |
| } |
| default: |
| WASM_UNREACHABLE("unexpted type"); |
| } |
| if (node->isExpr() || node->isPhi()) { |
| if (node->origin != trace.toInfer->origin && |
| trace.hasExternalUses.count(node) > 0) { |
| std::cout << " (hasExternalUses)"; |
| printedHasExternalUses = true; |
| } |
| } |
| std::cout << '\n'; |
| if (debug() && (node->isExpr() || node->isPhi())) { |
| warnOnSuspiciousValues(node); |
| } |
| } |
| |
| void print(Literal value) { |
| std::cout << value.getInteger() << ':' << value.type; |
| } |
| |
| void printInternal(Node* node) { |
| node = getMaybeReplaced(node); |
| assert(node); |
| if (node->isConst()) { |
| print(node->expr->cast<Const>()->value); |
| } else { |
| std::cout << "%" << indexing[node]; |
| } |
| } |
| |
| // Emit an expression |
| |
| void printExpression(Node* node) { |
| assert(node->isExpr()); |
| // TODO use a Visitor here? |
| auto* curr = node->expr; |
| if (auto* c = curr->dynCast<Const>()) { |
| print(c->value); |
| } else if (auto* unary = curr->dynCast<Unary>()) { |
| switch (unary->op) { |
| case ClzInt32: |
| case ClzInt64: |
| std::cout << "ctlz"; |
| break; |
| case CtzInt32: |
| case CtzInt64: |
| std::cout << "cttz"; |
| break; |
| case PopcntInt32: |
| case PopcntInt64: |
| std::cout << "ctpop"; |
| break; |
| default: |
| WASM_UNREACHABLE("invalid op"); |
| } |
| std::cout << ' '; |
| auto* value = node->getValue(0); |
| printInternal(value); |
| } else if (auto* binary = curr->dynCast<Binary>()) { |
| switch (binary->op) { |
| case AddInt32: |
| case AddInt64: |
| std::cout << "add"; |
| break; |
| case SubInt32: |
| case SubInt64: |
| std::cout << "sub"; |
| break; |
| case MulInt32: |
| case MulInt64: |
| std::cout << "mul"; |
| break; |
| case DivSInt32: |
| case DivSInt64: |
| std::cout << "sdiv"; |
| break; |
| case DivUInt32: |
| case DivUInt64: |
| std::cout << "udiv"; |
| break; |
| case RemSInt32: |
| case RemSInt64: |
| std::cout << "srem"; |
| break; |
| case RemUInt32: |
| case RemUInt64: |
| std::cout << "urem"; |
| break; |
| case AndInt32: |
| case AndInt64: |
| std::cout << "and"; |
| break; |
| case OrInt32: |
| case OrInt64: |
| std::cout << "or"; |
| break; |
| case XorInt32: |
| case XorInt64: |
| std::cout << "xor"; |
| break; |
| case ShlInt32: |
| case ShlInt64: |
| std::cout << "shl"; |
| break; |
| case ShrUInt32: |
| case ShrUInt64: |
| std::cout << "lshr"; |
| break; |
| case ShrSInt32: |
| case ShrSInt64: |
| std::cout << "ashr"; |
| break; |
| case RotLInt32: |
| case RotLInt64: |
| std::cout << "rotl"; |
| break; |
| case RotRInt32: |
| case RotRInt64: |
| std::cout << "rotr"; |
| break; |
| case EqInt32: |
| case EqInt64: |
| std::cout << "eq"; |
| break; |
| case NeInt32: |
| case NeInt64: |
| std::cout << "ne"; |
| break; |
| case LtSInt32: |
| case LtSInt64: |
| std::cout << "slt"; |
| break; |
| case LtUInt32: |
| case LtUInt64: |
| std::cout << "ult"; |
| break; |
| case LeSInt32: |
| case LeSInt64: |
| std::cout << "sle"; |
| break; |
| case LeUInt32: |
| case LeUInt64: |
| std::cout << "ule"; |
| break; |
| default: |
| WASM_UNREACHABLE("invalid op"); |
| } |
| std::cout << ' '; |
| auto* left = node->getValue(0); |
| printInternal(left); |
| std::cout << ", "; |
| auto* right = node->getValue(1); |
| printInternal(right); |
| } else if (curr->is<Select>()) { |
| std::cout << "select "; |
| printInternal(node->getValue(0)); |
| std::cout << ", "; |
| printInternal(node->getValue(1)); |
| std::cout << ", "; |
| printInternal(node->getValue(2)); |
| } else { |
| WASM_UNREACHABLE("unexecpted node type"); |
| } |
| } |
| |
| void printPathCondition(Node* condition) { |
| std::cout << "pc "; |
| printInternal(condition); |
| std::cout << " 1:i1\n"; |
| } |
| |
| // Checks if a value looks suspiciously optimizable. |
| void warnOnSuspiciousValues(Node* node) { |
| assert(debug()); |
| // If the node has no uses, it's not interesting enough to be |
| // suspicious. TODO |
| // If an input was replaced with a var, then we should not |
| // look into it, it's not suspiciously trivial. |
| for (auto* value : node->values) { |
| if (value != getMaybeReplaced(value)) { |
| return; |
| } |
| } |
| if (allInputsIdentical(node)) { |
| std::cout << "^^ suspicious identical inputs! missing optimization in " |
| << graph.func->name << "? ^^\n"; |
| return; |
| } |
| if (!node->isPhi() && allInputsConstant(node)) { |
| std::cout << "^^ suspicious constant inputs! missing optimization in " |
| << graph.func->name << "? ^^\n"; |
| return; |
| } |
| } |
| }; |
| |
| } // namespace DataFlow |
| |
| struct Souperify : public WalkerPass<PostWalker<Souperify>> { |
| // Not parallel, for now - could parallelize and combine outputs at the end. |
| // If Souper is thread-safe, we could also run it in parallel. |
| |
| bool singleUseOnly; |
| |
| Souperify(bool singleUseOnly) : singleUseOnly(singleUseOnly) {} |
| |
| void doWalkFunction(Function* func) { |
| std::cout << "\n; function: " << func->name << '\n'; |
| Flat::verifyFlatness(func); |
| // Build the data-flow IR. |
| DataFlow::Graph graph; |
| graph.build(func, getModule()); |
| if (debug() >= 2) { |
| dump(graph, std::cout); |
| } |
| // Build the local graph data structure. |
| LocalGraph localGraph(func); |
| localGraph.computeInfluences(); |
| // If we only want single-use nodes, exclude all the others. |
| std::unordered_set<DataFlow::Node*> excludeAsChildren; |
| if (singleUseOnly) { |
| for (auto& nodePtr : graph.nodes) { |
| auto* node = nodePtr.get(); |
| if (node->origin) { |
| // TODO: work for identical origins could be saved |
| auto uses = |
| DataFlow::UseFinder().getUses(node->origin, graph, localGraph); |
| if (debug() >= 2) { |
| std::cout << "following node has " << uses.size() << " uses\n"; |
| dump(node, std::cout); |
| } |
| if (uses.size() > 1) { |
| excludeAsChildren.insert(node); |
| } |
| } |
| } |
| } |
| // Emit possible traces. |
| for (auto& nodePtr : graph.nodes) { |
| auto* node = nodePtr.get(); |
| // Trace |
| if (DataFlow::Trace::isTraceable(node)) { |
| DataFlow::Trace trace(graph, node, excludeAsChildren, localGraph); |
| if (!trace.isBad()) { |
| DataFlow::Printer printer(graph, trace); |
| if (singleUseOnly) { |
| assert(!printer.printedHasExternalUses); |
| } |
| } |
| } |
| } |
| } |
| }; |
| |
| Pass* createSouperifyPass() { return new Souperify(false); } |
| |
| Pass* createSouperifySingleUsePass() { return new Souperify(true); } |
| |
| } // namespace wasm |