diff options
-rw-r--r-- | src/ir/effects.h | 113 | ||||
-rw-r--r-- | src/ir/localize.h | 15 | ||||
-rw-r--r-- | src/passes/Inlining.cpp | 210 | ||||
-rw-r--r-- | src/tools/wasm-ctor-eval.cpp | 194 | ||||
-rw-r--r-- | src/wasm-builder.h | 1 | ||||
-rw-r--r-- | src/wasm-interpreter.h | 168 | ||||
-rw-r--r-- | src/wasm/wasm.cpp | 1 | ||||
-rw-r--r-- | test/lit/ctor-eval/return_call.wast | 529 | ||||
-rw-r--r-- | test/lit/passes/global-effects.wast | 222 | ||||
-rw-r--r-- | test/lit/passes/inlining-unreachable.wast | 19 | ||||
-rw-r--r-- | test/lit/passes/inlining_all-features.wast | 7 | ||||
-rw-r--r-- | test/lit/passes/inlining_enable-tail-call.wast | 519 | ||||
-rw-r--r-- | test/lit/passes/simplify-locals-eh-old.wast | 51 | ||||
-rw-r--r-- | test/lit/passes/vacuum-eh-old.wast | 79 | ||||
-rw-r--r-- | test/spec/return_call.wast | 202 | ||||
-rw-r--r-- | test/spec/return_call_eh.wast | 35 | ||||
-rw-r--r-- | test/spec/return_call_indirect.wast | 536 | ||||
-rw-r--r-- | test/spec/return_call_ref.wast | 377 |
18 files changed, 2925 insertions, 353 deletions
diff --git a/src/ir/effects.h b/src/ir/effects.h index 6901f99de..3ecb54641 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -67,9 +67,23 @@ public: // noticeable from the perspective of the caller, that is, effects that are // only noticeable during the call, but "vanish" when the call stack is // unwound. + // + // Unlike walking just the body, walking the function will also + // include the effects of any return calls the function makes. For that + // reason, it is a bug if a user of this code calls walk(Expression*) and not + // walk(Function*) if their intention is to scan an entire function body. + // Putting it another way, a return_call is syntax sugar for a return and a + // call, where the call executes at the function scope, so there is a + // meaningful difference between scanning an expression and scanning + // the entire function body. void walk(Function* func) { walk(func->body); + // Effects of return-called functions will be visible to the caller. + if (hasReturnCallThrow) { + throws_ = true; + } + // We can ignore branching out of the function body - this can only be // a return, and that is only noticeable in the function, not outside. branchesOut = false; @@ -143,6 +157,22 @@ public: // or a continuation that is never continued, are examples of that. bool mayNotReturn = false; + // Since return calls return out of the body of the function before performing + // their call, they are indistinguishable from normal returns from the + // perspective of their surrounding code, and the return-callee's effects only + // become visible when considering the effects of the whole function + // containing the return call. To model this correctly, stash the callee's + // effects on the side and only merge them in after walking a full function + // body. + // + // We currently do this stashing only for the throw effect, but in principle + // we could do it for all effects if it made a difference. (Only throw is + // noticeable now because the only thing that can change between doing the + // call here and doing it outside at the function exit is the scoping of + // try-catch blocks. If future wasm scoping additions are added, we may need + // more here.) + bool hasReturnCallThrow = false; + // Helper functions to check for various effect types bool accessesLocal() const { @@ -466,43 +496,63 @@ private: return; } + const EffectAnalyzer* targetEffects = nullptr; + if (parent.funcEffectsMap) { + auto iter = parent.funcEffectsMap->find(curr->target); + if (iter != parent.funcEffectsMap->end()) { + targetEffects = &iter->second; + } + } + if (curr->isReturn) { parent.branchesOut = true; + // When EH is enabled, any call can throw. + if (parent.features.hasExceptionHandling() && + (!targetEffects || targetEffects->throws())) { + parent.hasReturnCallThrow = true; + } } - if (parent.funcEffectsMap) { - auto iter = parent.funcEffectsMap->find(curr->target); - if (iter != parent.funcEffectsMap->end()) { - // We have effect information for this call target, and can just use - // that. The one change we may want to make is to remove throws_, if - // the target function throws and we know that will be caught anyhow, - // the same as the code below for the general path. - const auto& targetEffects = iter->second; - if (targetEffects.throws_ && parent.tryDepth > 0) { - auto filteredEffects = targetEffects; - filteredEffects.throws_ = false; - parent.mergeIn(filteredEffects); - } else { - // Just merge in all the effects. - parent.mergeIn(targetEffects); - } - return; + if (targetEffects) { + // We have effect information for this call target, and can just use + // that. The one change we may want to make is to remove throws_, if the + // target function throws and we know that will be caught anyhow, the + // same as the code below for the general path. We can always filter out + // throws for return calls because they are already more precisely + // captured by `branchesOut`, which models the return, and + // `hasReturnCallThrow`, which models the throw that will happen after + // the return. + if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) { + auto filteredEffects = *targetEffects; + filteredEffects.throws_ = false; + parent.mergeIn(filteredEffects); + } else { + // Just merge in all the effects. + parent.mergeIn(*targetEffects); } + return; } parent.calls = true; - // When EH is enabled, any call can throw. - if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { + // When EH is enabled, any call can throw. Skip this for return calls + // because the throw is already more precisely captured by the combination + // of `hasReturnCallThrow` and `branchesOut`. + if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 && + !curr->isReturn) { parent.throws_ = true; } } void visitCallIndirect(CallIndirect* curr) { parent.calls = true; - if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { - parent.throws_ = true; - } if (curr->isReturn) { parent.branchesOut = true; + if (parent.features.hasExceptionHandling()) { + parent.hasReturnCallThrow = true; + } + } + if (parent.features.hasExceptionHandling() && + (parent.tryDepth == 0 && !curr->isReturn)) { + parent.throws_ = true; } } void visitLocalGet(LocalGet* curr) { @@ -745,21 +795,26 @@ private: } } void visitCallRef(CallRef* curr) { + if (curr->isReturn) { + parent.branchesOut = true; + if (parent.features.hasExceptionHandling()) { + parent.hasReturnCallThrow = true; + } + } if (curr->target->type.isNull()) { parent.trap = true; return; } - parent.calls = true; - if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { - parent.throws_ = true; - } - if (curr->isReturn) { - parent.branchesOut = true; - } // traps when the call target is null if (curr->target->type.isNullable()) { parent.implicitTrap = true; } + + parent.calls = true; + if (parent.features.hasExceptionHandling() && + (parent.tryDepth == 0 && !curr->isReturn)) { + parent.throws_ = true; + } } void visitRefTest(RefTest* curr) {} void visitRefCast(RefCast* curr) { diff --git a/src/ir/localize.h b/src/ir/localize.h index d44fb9be5..85e4415f5 100644 --- a/src/ir/localize.h +++ b/src/ir/localize.h @@ -153,17 +153,20 @@ struct ChildLocalizer { // Nothing to add. return parent; } + auto* block = getChildrenReplacement(); + if (!hasUnreachableChild) { + block->list.push_back(parent); + block->finalize(); + } + return block; + } + // Like `getReplacement`, but the result never contains the parent. + Block* getChildrenReplacement() { auto* block = Builder(wasm).makeBlock(); block->list.set(sets); if (hasUnreachableChild) { - // If there is an unreachable child then we do not need the parent at all, - // and we know the type is unreachable. block->type = Type::unreachable; - } else { - // Otherwise, add the parent and finalize. - block->list.push_back(parent); - block->finalize(); } return block; } diff --git a/src/passes/Inlining.cpp b/src/passes/Inlining.cpp index 51dddaaa9..0643389c2 100644 --- a/src/passes/Inlining.cpp +++ b/src/passes/Inlining.cpp @@ -37,6 +37,7 @@ #include "ir/element-utils.h" #include "ir/find_all.h" #include "ir/literal-utils.h" +#include "ir/localize.h" #include "ir/module-utils.h" #include "ir/names.h" #include "ir/type-updating.h" @@ -298,22 +299,34 @@ struct Updater : public PostWalker<Updater> { Module* module; std::map<Index, Index> localMapping; Name returnName; + Type resultType; bool isReturn; Builder* builder; PassOptions& options; + struct ReturnCallInfo { + // The original `return_call` or `return_call_indirect` or `return_call_ref` + // with its operands replaced with `local.get`s. + Expression* call; + // The branch that is serving as the "return" part of the original + // `return_call`. + Break* branch; + }; + + // Collect information on return_calls in the inlined body. Each will be + // turned into branches out of the original inlined body followed by + // non-return version of the original `return_call`, followed by a branch out + // to the caller. The branch labels will be filled in at the end of the walk. + std::vector<ReturnCallInfo> returnCallInfos; + Updater(PassOptions& options) : options(options) {} void visitReturn(Return* curr) { replaceCurrent(builder->makeBreak(returnName, curr->value)); } - // Return calls in inlined functions should only break out of the scope of - // the inlined code, not the entire function they are being inlined into. To - // achieve this, make the call a non-return call and add a break. This does - // not cause unbounded stack growth because inlining and return calling both - // avoid creating a new stack frame. - template<typename T> void handleReturnCall(T* curr, Type results) { - if (isReturn) { + + template<typename T> void handleReturnCall(T* curr, Signature sig) { + if (isReturn || !curr->isReturn) { // If the inlined callsite was already a return_call, then we can keep // return_calls in the inlined function rather than downgrading them. // That is, if A->B and B->C and both those calls are return_calls @@ -321,45 +334,85 @@ struct Updater : public PostWalker<Updater> { // return_call. return; } + + // Set the children to locals as necessary, then add a branch out of the + // inlined body. The branch label will be set later when we create branch + // targets for the calls. + Block* childBlock = ChildLocalizer(curr, getFunction(), *module, options) + .getChildrenReplacement(); + Break* branch = builder->makeBreak(Name()); + childBlock->list.push_back(branch); + childBlock->type = Type::unreachable; + replaceCurrent(childBlock); + curr->isReturn = false; - curr->type = results; - // There might still be unreachable children causing this to be unreachable. - curr->finalize(); - if (results.isConcrete()) { - replaceCurrent(builder->makeBreak(returnName, curr)); - } else { - replaceCurrent(builder->blockify(curr, builder->makeBreak(returnName))); - } + curr->type = sig.results; + returnCallInfos.push_back({curr, branch}); } + void visitCall(Call* curr) { - if (curr->isReturn) { - handleReturnCall(curr, module->getFunction(curr->target)->getResults()); - } + handleReturnCall(curr, module->getFunction(curr->target)->getSig()); } + void visitCallIndirect(CallIndirect* curr) { - if (curr->isReturn) { - handleReturnCall(curr, curr->heapType.getSignature().results); - } + handleReturnCall(curr, curr->heapType.getSignature()); } + void visitCallRef(CallRef* curr) { Type targetType = curr->target->type; - if (targetType.isNull()) { - // We don't know what type the call should return, but we can't leave it - // as a potentially-invalid return_call_ref, either. - replaceCurrent(getDroppedChildrenAndAppend( - curr, *module, options, Builder(*module).makeUnreachable())); + if (!targetType.isSignature()) { + // We don't know what type the call should return, but it will also never + // be reached, so we don't need to do anything here. return; } - if (curr->isReturn) { - handleReturnCall(curr, targetType.getHeapType().getSignature().results); - } + handleReturnCall(curr, targetType.getHeapType().getSignature()); } + void visitLocalGet(LocalGet* curr) { curr->index = localMapping[curr->index]; } + void visitLocalSet(LocalSet* curr) { curr->index = localMapping[curr->index]; } + + void walk(Expression*& curr) { + PostWalker<Updater>::walk(curr); + if (returnCallInfos.empty()) { + return; + } + + Block* body = builder->blockify(curr); + curr = body; + auto blockNames = BranchUtils::BranchAccumulator::get(body); + + for (Index i = 0; i < returnCallInfos.size(); ++i) { + auto& info = returnCallInfos[i]; + + // Add a block containing the previous body and a branch up to the caller. + // Give the block a name that will allow this return_call's original + // callsite to branch out of it then execute the call before returning to + // the caller. + auto name = Names::getValidName( + "__return_call", [&](Name test) { return !blockNames.count(test); }, i); + blockNames.insert(name); + info.branch->name = name; + Block* oldBody = builder->makeBlock(body->list, body->type); + body->list.clear(); + + if (resultType.isConcrete()) { + body->list.push_back(builder->makeBlock( + name, {builder->makeBreak(returnName, oldBody)}, Type::none)); + } else { + oldBody->list.push_back(builder->makeBreak(returnName)); + oldBody->name = name; + oldBody->type = Type::none; + body->list.push_back(oldBody); + } + body->list.push_back(info.call); + body->finalize(resultType); + } + } }; // Core inlining logic. Modifies the outside function (adding locals as @@ -376,8 +429,11 @@ static Expression* doInlining(Module* module, Index nameHint = 0) { Function* from = action.contents; auto* call = (*action.callSite)->cast<Call>(); + // Works for return_call, too Type retType = module->getFunction(call->target)->getResults(); + + // Build the block that will contain the inlined contents. Builder builder(*module); auto* block = builder.makeBlock(); auto name = std::string("__inlined_func$") + from->name.toString(); @@ -385,6 +441,7 @@ static Expression* doInlining(Module* module, name += '$' + std::to_string(nameHint); } block->name = Name(name); + // In the unlikely event that the function already has a branch target with // this name, fix that up, as otherwise we can get unexpected capture of our // branches, that is, we could end up with this: @@ -407,27 +464,24 @@ static Expression* doInlining(Module* module, // // (In this case we could use a second block and define the named block $X // after the call's parameters, but that adds work for an extremely rare - // situation.) + // situation.) The latter case does not apply if the call is a return_call, + // because in that case the call's children do not appear inside the same + // block as the inlined body. if (BranchUtils::hasBranchTarget(from->body, block->name) || - BranchUtils::BranchSeeker::has(call, block->name)) { + (!call->isReturn && BranchUtils::BranchSeeker::has(call, block->name))) { auto fromNames = BranchUtils::getBranchTargets(from->body); - auto callNames = BranchUtils::BranchAccumulator::get(call); + auto callNames = call->isReturn ? BranchUtils::NameSet{} + : BranchUtils::BranchAccumulator::get(call); block->name = Names::getValidName(block->name, [&](Name test) { return !fromNames.count(test) && !callNames.count(test); }); } - if (call->isReturn) { - if (retType.isConcrete()) { - *action.callSite = builder.makeReturn(block); - } else { - *action.callSite = builder.makeSequence(block, builder.makeReturn()); - } - } else { - *action.callSite = block; - } + // Prepare to update the inlined code's locals and other things. Updater updater(options); + updater.setFunction(into); updater.module = module; + updater.resultType = from->getResults(); updater.returnName = block->name; updater.isReturn = call->isReturn; updater.builder = &builder; @@ -435,31 +489,71 @@ static Expression* doInlining(Module* module, for (Index i = 0; i < from->getNumLocals(); i++) { updater.localMapping[i] = builder.addVar(into, from->getLocalType(i)); } - // Assign the operands into the params - for (Index i = 0; i < from->getParams().size(); i++) { - block->list.push_back( - builder.makeLocalSet(updater.localMapping[i], call->operands[i])); - } - // Zero out the vars (as we may be in a loop, and may depend on their - // zero-init value - for (Index i = 0; i < from->vars.size(); i++) { - auto type = from->vars[i]; - if (!LiteralUtils::canMakeZero(type)) { - // Non-zeroable locals do not need to be zeroed out. As they have no zero - // value they by definition should not be used before being written to, so - // any value we set here would not be observed anyhow. - continue; + + if (call->isReturn) { + // Wrap the existing function body in a block we can branch out of before + // entering the inlined function body. This block must have a name that is + // different from any other block name above the branch. + auto intoNames = BranchUtils::BranchAccumulator::get(into->body); + auto bodyName = + Names::getValidName(Name("__original_body"), + [&](Name test) { return !intoNames.count(test); }); + if (retType.isConcrete()) { + into->body = builder.makeBlock( + bodyName, {builder.makeReturn(into->body)}, Type::none); + } else { + into->body = builder.makeBlock( + bodyName, {into->body, builder.makeReturn()}, Type::none); + } + + // Sequence the inlined function body after the original caller body. + into->body = builder.makeSequence(into->body, block, retType); + + // Replace the original callsite with an expression that assigns the + // operands into the params and branches out of the original body. + auto numParams = from->getParams().size(); + if (numParams) { + auto* branchBlock = builder.makeBlock(); + for (Index i = 0; i < numParams; i++) { + branchBlock->list.push_back( + builder.makeLocalSet(updater.localMapping[i], call->operands[i])); + } + branchBlock->list.push_back(builder.makeBreak(bodyName)); + branchBlock->finalize(Type::unreachable); + *action.callSite = branchBlock; + } else { + *action.callSite = builder.makeBreak(bodyName); + } + } else { + // Assign the operands into the params + for (Index i = 0; i < from->getParams().size(); i++) { + block->list.push_back( + builder.makeLocalSet(updater.localMapping[i], call->operands[i])); } - block->list.push_back( - builder.makeLocalSet(updater.localMapping[from->getVarIndexBase() + i], - LiteralUtils::makeZero(type, *module))); + // Zero out the vars (as we may be in a loop, and may depend on their + // zero-init value + for (Index i = 0; i < from->vars.size(); i++) { + auto type = from->vars[i]; + if (!LiteralUtils::canMakeZero(type)) { + // Non-zeroable locals do not need to be zeroed out. As they have no + // zero value they by definition should not be used before being written + // to, so any value we set here would not be observed anyhow. + continue; + } + block->list.push_back( + builder.makeLocalSet(updater.localMapping[from->getVarIndexBase() + i], + LiteralUtils::makeZero(type, *module))); + } + *action.callSite = block; } + // Generate and update the inlined contents auto* contents = ExpressionManipulator::copy(from->body, *module); debug::copyDebugInfo(from->body, contents, from, into); updater.walk(contents); block->list.push_back(contents); block->type = retType; + // The ReFinalize below will handle propagating unreachability if we need to // do so, that is, if the call was reachable but now the inlined content we // replaced it with was unreachable. The opposite case requires special diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index d476bb2cc..fe3d42d09 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -25,6 +25,7 @@ #include <memory> #include "asmjs/shared-constants.h" +#include "ir/find_all.h" #include "ir/gc-type-utils.h" #include "ir/global-utils.h" #include "ir/import-utils.h" @@ -1061,40 +1062,45 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, params.push_back(Literal::makeZero(type)); } - // We want to handle the form of the global constructor function in LLVM. That - // looks like this: - // - // (func $__wasm_call_ctors - // (call $ctor.1) - // (call $ctor.2) - // (call $ctor.3) - // ) - // - // Some of those ctors may be inlined, however, which would mean that the - // function could have locals, control flow, etc. However, we assume for now - // that it does not have parameters at least (whose values we can't tell). - // And for now we look for a toplevel block and process its children one at a - // time. This allows us to eval some of the $ctor.* functions (or their - // inlined contents) even if not all. - // - // TODO: Support complete partial evalling, that is, evaluate parts of an - // arbitrary function, and not just a sequence in a single toplevel - // block. + // After we successfully eval a line we will store the operations to set up + // the locals here. That is, we need to save the local state in the function, + // which we do by setting up at the entry. We update this list of expressions + // at the same time as applyToModule() - we must only do it after an entire + // atomic "chunk" has been processed succesfully, we do not want partial + // updates from an item in the block that we only partially evalled. When we + // construct the (partially) evalled function, we will create local.sets of + // these expressions at the beginning. + std::vector<Expression*> localExprs; + + // We might have to evaluate multiple functions due to return calls. +start_eval: + while (true) { + // We want to handle the form of the global constructor function in LLVM. + // That looks like this: + // + // (func $__wasm_call_ctors + // (call $ctor.1) + // (call $ctor.2) + // (call $ctor.3) + // ) + // + // Some of those ctors may be inlined, however, which would mean that the + // function could have locals, control flow, etc. However, we assume for now + // that it does not have parameters at least (whose values we can't tell). + // And for now we look for a toplevel block and process its children one at + // a time. This allows us to eval some of the $ctor.* functions (or their + // inlined contents) even if not all. + // + // TODO: Support complete partial evalling, that is, evaluate parts of an + // arbitrary function, and not just a sequence in a single toplevel + // block. + Builder builder(wasm); + auto* block = builder.blockify(func->body); - if (auto* block = func->body->dynCast<Block>()) { // Go through the items in the block and try to execute them. We do all this // in a single function scope for all the executions. EvallingModuleRunner::FunctionScope scope(func, params, instance); - // After we successfully eval a line we will store the operations to set up - // the locals here. That is, we need to save the local state in the - // function, which we do by setting up at the entry. We update this list of - // local.sets at the same time as applyToModule() - we must only do it after - // an entire atomic "chunk" has been processed succesfully, we do not want - // partial updates from an item in the block that we only partially evalled. - std::vector<Expression*> localSets; - - Builder builder(wasm); Literals results; Index successes = 0; @@ -1116,6 +1122,22 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, break; } + if (flow.breakTo == RETURN_CALL_FLOW) { + // The return-called function is stored in the last value. + func = wasm.getFunction(flow.values.back().getFunc()); + flow.values.pop_back(); + params = std::move(flow.values); + + // Serialize the arguments for the new function and save the module + // state in case we fail to eval the new function. + localExprs.clear(); + for (auto& param : params) { + localExprs.push_back(interface.getSerialization(param)); + } + interface.applyToModule(); + goto start_eval; + } + // So far so good! Serialize the values of locals, and apply to the // module. Note that we must serialize the locals now as doing so may // cause changes that must be applied to the module (e.g. GC data may @@ -1128,11 +1150,9 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // of them, and leave it to the optimizer to remove redundant or // unnecessary operations. We just recompute the entire local // serialization sets from scratch each time here, for all locals. - localSets.clear(); + localExprs.clear(); for (Index i = 0; i < func->getNumLocals(); i++) { - auto value = scope.locals[i]; - localSets.push_back( - builder.makeLocalSet(i, interface.getSerialization(value))); + localExprs.push_back(interface.getSerialization(scope.locals[i])); } interface.applyToModule(); successes++; @@ -1144,41 +1164,97 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, if (flow.breaking()) { // We are returning out of the function (either via a return, or via a // break to |block|, which has the same outcome. That means we don't - // need to execute any more lines, and can consider them to be executed. + // need to execute any more lines, and can consider them to be + // executed. if (!quiet) { std::cout << " ...stopping in block due to break\n"; } // Mark us as having succeeded on the entire block, since we have: we - // are skipping the rest, which means there is no problem there. We must - // set this here so that lower down we realize that we've evalled + // are skipping the rest, which means there is no problem there. We + // must set this here so that lower down we realize that we've evalled // everything. successes = block->list.size(); break; } } - if (successes > 0 && successes < block->list.size()) { - // We managed to eval some but not all. That means we can't just remove - // the entire function, but need to keep parts of it - the parts we have - // not evalled - around. To do so, we create a copy of the function with - // the partially-evalled contents and make the export use that (as the - // function may be used in other places than the export, which we do not - // want to affect). + // If we have not fully evaluated the current function, but we have + // evaluated part of it, have return-called to a different function, or have + // precomputed values for the current return-called function, then we can + // replace the export with a new function that does less work than the + // original. + if ((func->imported() || successes < block->list.size()) && + (successes > 0 || func->name != funcName || + (localExprs.size() && func->getParams() != Type::none))) { + auto originalFuncType = wasm.getFunction(funcName)->type; auto copyName = Names::getValidFunctionName(wasm, funcName); - auto* copyFunc = ModuleUtils::copyFunction(func, wasm, copyName); wasm.getExport(exportName)->value = copyName; + if (func->imported()) { + // We must have return-called this imported function. Generate a new + // function that return-calls the import with the arguments we have + // evalled. + auto copyFunc = builder.makeFunction( + copyName, + originalFuncType, + {}, + builder.makeCall(func->name, localExprs, func->getResults(), true)); + wasm.addFunction(std::move(copyFunc)); + return EvalCtorOutcome(); + } + + // We may have managed to eval some but not all. That means we can't just + // remove the entire function, but need to keep parts of it - the parts we + // have not evalled - around. To do so, we create a copy of the function + // with the partially-evalled contents and make the export use that (as + // the function may be used in other places than the export, which we do + // not want to affect). + auto* copyBody = + builder.blockify(ExpressionManipulator::copy(func->body, wasm)); + // Remove the items we've evalled. - auto* copyBlock = copyFunc->body->cast<Block>(); for (Index i = 0; i < successes; i++) { - copyBlock->list[i] = builder.makeNop(); + copyBody->list[i] = builder.makeNop(); } - // Put the local sets at the front of the block. We know there must be a - // nop in that position (since we've evalled at least one item in the - // block, and replaced it with a nop), so we can overwrite it. - copyBlock->list[0] = builder.makeBlock(localSets); + // Put the local sets at the front of the function body. + auto* setsBlock = builder.makeBlock(); + for (Index i = 0; i < localExprs.size(); ++i) { + setsBlock->list.push_back(builder.makeLocalSet(i, localExprs[i])); + } + copyBody = builder.makeSequence(setsBlock, copyBody, copyBody->type); + + // We may have return-called into a function with different parameter + // types, but we ultimately need to export a function with the original + // signature. If there is a mismatch, shift the local indices to make room + // for the unused parameters. + std::vector<Type> localTypes; + auto originalParams = originalFuncType.getSignature().params; + if (originalParams != func->getParams()) { + // Add locals for the body to use instead of using the params. + for (auto type : func->getParams()) { + localTypes.push_back(type); + } + + // Shift indices in the body so they will refer to the new locals. + auto localShift = originalParams.size(); + if (localShift != 0) { + for (auto* get : FindAll<LocalGet>(copyBody).list) { + get->index += localShift; + } + for (auto* set : FindAll<LocalSet>(copyBody).list) { + set->index += localShift; + } + } + } + + // Add vars from current function. + localTypes.insert(localTypes.end(), func->vars.begin(), func->vars.end()); + + // Create and add the new function. + auto* copyFunc = wasm.addFunction(builder.makeFunction( + copyName, originalFuncType, std::move(localTypes), copyBody)); // Interesting optimizations may be possible both due to removing some but // not all of the code, and due to the locals we just added. @@ -1196,24 +1272,6 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, return EvalCtorOutcome(); } } - - // Otherwise, we don't recognize a pattern that allows us to do partial - // evalling. So simply call the entire function at once and see if we can - // optimize that. - - Literals results; - try { - results = instance.callFunction(funcName, params); - } catch (FailToEvalException& fail) { - if (!quiet) { - std::cout << " ...stopping since could not eval: " << fail.why << "\n"; - } - return EvalCtorOutcome(); - } - - // Success! Apply the results. - interface.applyToModule(); - return EvalCtorOutcome(results); } // Eval all ctors in a module. diff --git a/src/wasm-builder.h b/src/wasm-builder.h index cc90a8abe..463faa04f 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -269,6 +269,7 @@ public: call->target = target; call->operands.set(args); call->isReturn = isReturn; + call->finalize(); return call; } template<typename T> diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index c95f694ef..64c0bfb2d 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -51,7 +51,7 @@ std::ostream& operator<<(std::ostream& o, const WasmException& exn); // Utilities -extern Name WASM, RETURN_FLOW, NONCONSTANT_FLOW; +extern Name WASM, RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW; // Stuff that flows around during executing expressions: a literal, or a change // in control flow. @@ -63,6 +63,8 @@ public: Flow(Literals&& values) : values(std::move(values)) {} Flow(Name breakTo) : values(), breakTo(breakTo) {} Flow(Name breakTo, Literal value) : values{value}, breakTo(breakTo) {} + Flow(Name breakTo, Literals&& values) + : values(std::move(values)), breakTo(breakTo) {} Literals values; Name breakTo; // if non-null, a break is going on @@ -2965,32 +2967,33 @@ public: Flow visitCall(Call* curr) { NOTE_ENTER("Call"); NOTE_NAME(curr->target); + Name target = curr->target; Literals arguments; Flow flow = self()->generateArguments(curr->operands, arguments); if (flow.breaking()) { return flow; } auto* func = wasm.getFunction(curr->target); - Flow ret; + auto funcType = func->type; if (Intrinsics(*self()->getModule()).isCallWithoutEffects(func)) { // The call.without.effects intrinsic is a call to an import that actually // calls the given function reference that is the final argument. - auto newArguments = arguments; - auto target = newArguments.back(); - newArguments.pop_back(); - ret.values = callFunctionInternal(target.getFunc(), newArguments); - } else if (func->imported()) { - ret.values = externalInterface->callImport(func, arguments); - } else { - ret.values = callFunctionInternal(curr->target, arguments); + target = arguments.back().getFunc(); + funcType = arguments.back().type.getHeapType(); + arguments.pop_back(); + } + + if (curr->isReturn) { + // Return calls are represented by their arguments followed by a reference + // to the function to be called. + arguments.push_back(Literal::makeFunc(target, funcType)); + return Flow(RETURN_CALL_FLOW, std::move(arguments)); } + + Flow ret = callFunctionInternal(target, arguments); #ifdef WASM_INTERPRETER_DEBUG std::cout << "(returned to " << scope->function->name << ")\n"; #endif - // TODO: make this a proper tail call (return first) - if (curr->isReturn) { - ret.breakTo = RETURN_FLOW; - } return ret; } @@ -3007,18 +3010,28 @@ public: } Index index = target.getSingleValue().geti32(); - Type type = curr->isReturn ? scope->function->getResults() : curr->type; auto info = getTableInterfaceInfo(curr->table); - Flow ret = info.interface->callTable( - info.name, index, curr->heapType, arguments, type, *self()); - // TODO: make this a proper tail call (return first) if (curr->isReturn) { - ret.breakTo = RETURN_FLOW; + // Return calls are represented by their arguments followed by a reference + // to the function to be called. + auto funcref = info.interface->tableLoad(info.name, index); + if (!Type::isSubType(funcref.type, Type(curr->heapType, NonNullable))) { + trap("cast failure in call_indirect"); + } + arguments.push_back(funcref); + return Flow(RETURN_CALL_FLOW, std::move(arguments)); } + + Flow ret = info.interface->callTable( + info.name, index, curr->heapType, arguments, curr->type, *self()); +#ifdef WASM_INTERPRETER_DEBUG + std::cout << "(returned to " << scope->function->name << ")\n"; +#endif return ret; } + Flow visitCallRef(CallRef* curr) { NOTE_ENTER("CallRef"); Literals arguments; @@ -3030,24 +3043,22 @@ public: if (target.breaking()) { return target; } - if (target.getSingleValue().isNull()) { + auto targetRef = target.getSingleValue(); + if (targetRef.isNull()) { trap("null target in call_ref"); } - Name funcName = target.getSingleValue().getFunc(); - auto* func = wasm.getFunction(funcName); - Flow ret; - if (func->imported()) { - ret.values = externalInterface->callImport(func, arguments); - } else { - ret.values = callFunctionInternal(funcName, arguments); + + if (curr->isReturn) { + // Return calls are represented by their arguments followed by a reference + // to the function to be called. + arguments.push_back(targetRef); + return Flow(RETURN_CALL_FLOW, std::move(arguments)); } + + Flow ret = callFunctionInternal(targetRef.getFunc(), arguments); #ifdef WASM_INTERPRETER_DEBUG std::cout << "(returned to " << scope->function->name << ")\n"; #endif - // TODO: make this a proper tail call (return first) - if (curr->isReturn) { - ret.breakTo = RETURN_FLOW; - } return ret; } @@ -4098,12 +4109,7 @@ public: // Call a function, starting an invocation. Literals callFunction(Name name, const Literals& arguments) { - auto* func = wasm.getFunction(name); - if (func->imported()) { - return externalInterface->callImport(func, arguments); - } - - // if the last call ended in a jump up the stack, it might have left stuff + // If the last call ended in a jump up the stack, it might have left stuff // for us to clean up here callDepth = 0; functionStack.clear(); @@ -4112,47 +4118,79 @@ public: // Internal function call. Must be public so that callTable implementations // can use it (refactor?) - Literals callFunctionInternal(Name name, const Literals& arguments) { + Literals callFunctionInternal(Name name, Literals arguments) { if (callDepth > maxDepth) { externalInterface->trap("stack limit"); } - auto previousCallDepth = callDepth; - callDepth++; - auto previousFunctionStackSize = functionStack.size(); - functionStack.push_back(name); - Function* function = wasm.getFunction(name); - assert(function); - FunctionScope scope(function, arguments, *self()); + Flow flow; + std::optional<Type> resultType; + + // We may have to call multiple functions in the event of return calls. + while (true) { + Function* function = wasm.getFunction(name); + assert(function); + + // Return calls can only make the result type more precise. + if (resultType) { + assert(Type::isSubType(function->getResults(), *resultType)); + } + resultType = function->getResults(); + + if (function->imported()) { + // TODO: Allow imported functions to tail call as well. + return externalInterface->callImport(function, arguments); + } + + auto previousCallDepth = callDepth; + callDepth++; + auto previousFunctionStackSize = functionStack.size(); + functionStack.push_back(name); + + FunctionScope scope(function, arguments, *self()); #ifdef WASM_INTERPRETER_DEBUG - std::cout << "entering " << function->name << "\n with arguments:\n"; - for (unsigned i = 0; i < arguments.size(); ++i) { - std::cout << " $" << i << ": " << arguments[i] << '\n'; - } + std::cout << "entering " << function->name << "\n with arguments:\n"; + for (unsigned i = 0; i < arguments.size(); ++i) { + std::cout << " $" << i << ": " << arguments[i] << '\n'; + } +#endif + + flow = self()->visit(function->body); + + // may decrease more than one, if we jumped up the stack + callDepth = previousCallDepth; + // if we jumped up the stack, we also need to pop higher frames + // TODO can FunctionScope handle this automatically? + while (functionStack.size() > previousFunctionStackSize) { + functionStack.pop_back(); + } +#ifdef WASM_INTERPRETER_DEBUG + std::cout << "exiting " << function->name << " with " << flow.values + << '\n'; #endif - Flow flow = self()->visit(function->body); + if (flow.breakTo != RETURN_CALL_FLOW) { + break; + } + + // There was a return call, so we need to call the next function before + // returning to the caller. The flow carries the function arguments and a + // function reference. + name = flow.values.back().getFunc(); + flow.values.pop_back(); + arguments = flow.values; + } + // cannot still be breaking, it means we missed our stop assert(!flow.breaking() || flow.breakTo == RETURN_FLOW); auto type = flow.getType(); - if (!Type::isSubType(type, function->getResults())) { - std::cerr << "calling " << function->name << " resulted in " << type - << " but the function type is " << function->getResults() - << '\n'; + if (!Type::isSubType(type, *resultType)) { + std::cerr << "calling " << name << " resulted in " << type + << " but the function type is " << *resultType << '\n'; WASM_UNREACHABLE("unexpected result type"); } - // may decrease more than one, if we jumped up the stack - callDepth = previousCallDepth; - // if we jumped up the stack, we also need to pop higher frames - // TODO can FunctionScope handle this automatically? - while (functionStack.size() > previousFunctionStackSize) { - functionStack.pop_back(); - } -#ifdef WASM_INTERPRETER_DEBUG - std::cout << "exiting " << function->name << " with " << flow.values - << '\n'; -#endif + return flow.values; } diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index bc707890c..c2df2c68c 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -25,6 +25,7 @@ namespace wasm { Name WASM("wasm"); Name RETURN_FLOW("*return:)*"); +Name RETURN_CALL_FLOW("*return-call:)*"); Name NONCONSTANT_FLOW("*nonconstant:)*"); namespace BinaryConsts { diff --git a/test/lit/ctor-eval/return_call.wast b/test/lit/ctor-eval/return_call.wast new file mode 100644 index 000000000..3ff35ec42 --- /dev/null +++ b/test/lit/ctor-eval/return_call.wast @@ -0,0 +1,529 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --ignore-external-input --quiet -all -g -S -o - | filecheck %s + +(module + ;; Simplest possible return call. + + (func $test (export "test") (result i32) + (return_call $test2) + ) + + (func $test2 (result i32) + (i32.const 42) + ) +) + +;; CHECK: (type $0 (func (result i32))) + +;; CHECK: (export "test" (func $test_2)) + +;; CHECK: (func $test_2 (type $0) (result i32) +;; CHECK-NEXT: (i32.const 42) +;; CHECK-NEXT: ) +(module + ;; Basic return call (followed by unreachable import call, setting global as proof it was executed) + + (import "env" "import" (func $import)) + + ;; CHECK: (type $0 (func)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call $test2) + ;; This is never executed, so it should not impede eval. + (call $import) + ) + + (func $test2 + (global.set $g2 + (i32.const 2) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $0) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) +(module + ;; Basic return call indirect + ;; TODO: Implement `tableLoad` to make this test work. + + (import "env" "import" (func $import)) + + ;; CHECK: (type $0 (func)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 0)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $test2)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call_indirect $t + (i32.const 0) + ) + ;; This is never executed, so it should not impede eval. + (call $import) + ) + + ;; CHECK: (elem $0 (i32.const 0) $test2) + + ;; CHECK: (export "g1" (global $g1)) + + ;; CHECK: (export "g2" (global $g2)) + + ;; CHECK: (export "test" (func $test_3)) + + ;; CHECK: (func $test2 (type $0) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 + (global.set $g2 + (i32.const 2) + ) + ) +) + +;; CHECK: (func $test_3 (type $0) +;; CHECK-NEXT: (return_call_indirect $t (type $0) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + ;; Basic return call ref + ;; CHECK: (type $f (func)) + (type $f (func)) + + (import "env" "import" (func $import)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (elem declare $test2) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call_ref $f + (ref.func $test2) + ) + ;; This is never executed, so it should not impede eval. + (call $import) + ) + + (func $test2 (type $f) + (global.set $g2 + (i32.const 2) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $f) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) +(module + ;; Return call to import + + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "env" "import" (func $import (type $0))) + (import "env" "import" (func $import)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call $import) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "test" (func $test_2)) + +;; CHECK: (func $test_2 (type $0) +;; CHECK-NEXT: (return_call $import) +;; CHECK-NEXT: ) +(module + ;; Return call to import with params + + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "env" "import" (func $import (type $0) (param i32))) + (import "env" "import" (func $import (param i32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call $import + (i32.add + (i32.const 40) + (i32.const 2) + ) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "test" (func $test_2)) + +;; CHECK: (func $test_2 (type $1) +;; CHECK-NEXT: (return_call $import +;; CHECK-NEXT: (i32.const 42) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + ;; Chain of return calls ending in import + + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "env" "import" (func $import (type $0) (param i32))) + (import "env" "import" (func $import (param i32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call $test2 + (i32.const 40) + ) + ) + + (func $test2 (param i32) + (global.set $g2 + (i32.const 2) + ) + (return_call $import + (i32.add + (i32.const 2) + (local.get 0) + ) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $1) +;; CHECK-NEXT: (return_call $import +;; CHECK-NEXT: (i32.const 42) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + ;; Return call to a function that can only be partially evaluated. + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "env" "import" (func $import (type $0))) + (import "env" "import" (func $import)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (func $test (export "test") + (global.set $g1 + (i32.const 1) + ) + (return_call $test2) + ) + + (func $test2 + (global.set $g2 + (i32.const 2) + ) + (call $import) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $0) +;; CHECK-NEXT: (call $import) +;; CHECK-NEXT: ) +(module + ;; Return call with parameters to a function that can only be partially evaluated. + + ;; CHECK: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (import "env" "import" (func $import (type $0) (param i32) (result i32))) + (import "env" "import" (func $import (param i32) (result i32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (func $test (export "test") (param i32) + (global.set $g1 + (i32.const 1) + ) + (return_call $test2 + (local.get 0) + ) + ) + + (func $test2 (param i32) + (local $x i32) + (global.set $g2 + (i32.const 2) + ) + (local.set $x + (call $import + (local.get 0) + ) + ) + (drop + (call $import + (i32.const 0) + ) + ) + (drop + (call $import + (local.get $x) + ) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $1) (param $0 i32) +;; CHECK-NEXT: (local.set $0 +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + ;; Return call with parameters to a function that can only be partially + ;; evaluated that takes different parameters from the original. + ;; CHECK: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (import "env" "import" (func $import (type $0) (param i32) (result i32))) + (import "env" "import" (func $import (param i32) (result i32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 1)) + (global $g1 (export "g1") (mut i32) (i32.const 0)) + + ;; CHECK: (global $g2 (mut i32) (i32.const 2)) + (global $g2 (export "g2") (mut i32) (i32.const 0)) + + (func $test (export "test") (param i32) + (global.set $g1 + (i32.const 1) + ) + (return_call $test2 + (i64.const 1) + (i64.const 2) + (i64.const 3) + ) + ) + + (func $test2 (param i64 i64 i64) + (local $x i32) + (global.set $g2 + (i32.const 2) + ) + (local.set $x + (call $import + (i32.wrap_i64 + (local.get 2) + ) + ) + ) + (drop + (call $import + (i32.const 0) + ) + ) + (drop + (call $import + (local.get $x) + ) + ) + ) +) + +;; CHECK: (export "g1" (global $g1)) + +;; CHECK: (export "g2" (global $g2)) + +;; CHECK: (export "test" (func $test_3)) + +;; CHECK: (func $test_3 (type $1) (param $0 i32) +;; CHECK-NEXT: (local.set $0 +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (i32.const 3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (call $import +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + ;; Return call to self with different params, then stop evaluating. + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "env" "import" (func $import (type $1))) + (import "env" "import" (func $import)) + + ;; CHECK: (global $g (mut i32) (i32.const 42)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (export "test" (func $test_2)) + + ;; CHECK: (func $test (type $0) (param $0 i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (return_call $test + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (export "test") (param i32) + (global.set $g + (local.get 0) + ) + (if + (i32.eq + (local.get 0) + (i32.const 42) + ) + (then + (call $import) + ) + (else + (return_call $test + (i32.add + (local.get 0) + (i32.const 1) + ) + ) + ) + ) + ) +) + +;; CHECK: (func $test_2 (type $0) (param $0 i32) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.eq +;; CHECK-NEXT: (local.tee $0 +;; CHECK-NEXT: (i32.const 42) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 42) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (call $import) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (else +;; CHECK-NEXT: (return_call $test +;; CHECK-NEXT: (i32.add +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 3f4e58e8b..38e96c6eb 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -8,27 +8,36 @@ ;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --discard-global-effects --vacuum -S -o - | filecheck %s --check-prefix WITHOUT (module - ;; WITHOUT: (type $0 (func)) + + ;; WITHOUT: (type $void (func)) + ;; INCLUDE: (type $void (func)) + (type $void (func)) ;; WITHOUT: (type $1 (func (result i32))) ;; WITHOUT: (type $2 (func (param i32))) - ;; WITHOUT: (import "a" "b" (func $import (type $0))) - ;; INCLUDE: (type $0 (func)) - + ;; WITHOUT: (import "a" "b" (func $import (type $void))) ;; INCLUDE: (type $1 (func (result i32))) ;; INCLUDE: (type $2 (func (param i32))) - ;; INCLUDE: (import "a" "b" (func $import (type $0))) + ;; INCLUDE: (import "a" "b" (func $import (type $void))) (import "a" "b" (func $import)) + ;; WITHOUT: (table $t 0 funcref) + ;; INCLUDE: (table $t 0 funcref) + (table $t funcref 0) + + ;; WITHOUT: (elem declare func $throw) + ;; WITHOUT: (tag $tag) + ;; INCLUDE: (elem declare func $throw) + ;; INCLUDE: (tag $tag) (tag $tag) - ;; WITHOUT: (func $main (type $0) + ;; WITHOUT: (func $main (type $void) ;; WITHOUT-NEXT: (call $nop) ;; WITHOUT-NEXT: (call $unreachable) ;; WITHOUT-NEXT: (call $call-nop) @@ -39,7 +48,7 @@ ;; WITHOUT-NEXT: (call $throw) ;; WITHOUT-NEXT: (call $throw-and-import) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $main (type $0) + ;; INCLUDE: (func $main (type $void) ;; INCLUDE-NEXT: (call $unreachable) ;; INCLUDE-NEXT: (call $call-unreachable) ;; INCLUDE-NEXT: (call $throw) @@ -67,10 +76,10 @@ (call $throw-and-import) ) - ;; WITHOUT: (func $cycle (type $0) + ;; WITHOUT: (func $cycle (type $void) ;; WITHOUT-NEXT: (call $cycle) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $cycle (type $0) + ;; INCLUDE: (func $cycle (type $void) ;; INCLUDE-NEXT: (call $cycle) ;; INCLUDE-NEXT: ) (func $cycle @@ -79,10 +88,10 @@ (call $cycle) ) - ;; WITHOUT: (func $cycle-1 (type $0) + ;; WITHOUT: (func $cycle-1 (type $void) ;; WITHOUT-NEXT: (call $cycle-2) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $cycle-1 (type $0) + ;; INCLUDE: (func $cycle-1 (type $void) ;; INCLUDE-NEXT: (call $cycle-2) ;; INCLUDE-NEXT: ) (func $cycle-1 @@ -90,40 +99,40 @@ (call $cycle-2) ) - ;; WITHOUT: (func $cycle-2 (type $0) + ;; WITHOUT: (func $cycle-2 (type $void) ;; WITHOUT-NEXT: (call $cycle-1) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $cycle-2 (type $0) + ;; INCLUDE: (func $cycle-2 (type $void) ;; INCLUDE-NEXT: (call $cycle-1) ;; INCLUDE-NEXT: ) (func $cycle-2 (call $cycle-1) ) - ;; WITHOUT: (func $nop (type $0) + ;; WITHOUT: (func $nop (type $void) ;; WITHOUT-NEXT: (nop) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $nop (type $0) + ;; INCLUDE: (func $nop (type $void) ;; INCLUDE-NEXT: (nop) ;; INCLUDE-NEXT: ) (func $nop (nop) ) - ;; WITHOUT: (func $unreachable (type $0) + ;; WITHOUT: (func $unreachable (type $void) ;; WITHOUT-NEXT: (unreachable) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $unreachable (type $0) + ;; INCLUDE: (func $unreachable (type $void) ;; INCLUDE-NEXT: (unreachable) ;; INCLUDE-NEXT: ) (func $unreachable (unreachable) ) - ;; WITHOUT: (func $call-nop (type $0) + ;; WITHOUT: (func $call-nop (type $void) ;; WITHOUT-NEXT: (call $nop) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-nop (type $0) + ;; INCLUDE: (func $call-nop (type $void) ;; INCLUDE-NEXT: (nop) ;; INCLUDE-NEXT: ) (func $call-nop @@ -131,10 +140,10 @@ (call $nop) ) - ;; WITHOUT: (func $call-unreachable (type $0) + ;; WITHOUT: (func $call-unreachable (type $void) ;; WITHOUT-NEXT: (call $unreachable) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-unreachable (type $0) + ;; INCLUDE: (func $call-unreachable (type $void) ;; INCLUDE-NEXT: (call $unreachable) ;; INCLUDE-NEXT: ) (func $call-unreachable @@ -172,7 +181,7 @@ ) ) - ;; WITHOUT: (func $call-throw-and-catch (type $0) + ;; WITHOUT: (func $call-throw-and-catch (type $void) ;; WITHOUT-NEXT: (try $try ;; WITHOUT-NEXT: (do ;; WITHOUT-NEXT: (call $throw) @@ -190,7 +199,7 @@ ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-throw-and-catch (type $0) + ;; INCLUDE: (func $call-throw-and-catch (type $void) ;; INCLUDE-NEXT: (try $try0 ;; INCLUDE-NEXT: (do ;; INCLUDE-NEXT: (call $throw-and-import) @@ -219,7 +228,158 @@ ) ) - ;; WITHOUT: (func $call-unreachable-and-catch (type $0) + ;; WITHOUT: (func $return-call-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call $throw) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call $throw) + ;; INCLUDE-NEXT: ) + (func $return-call-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target throws. However, the + ;; surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call $throw) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $return-call-indirect-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call_indirect $t (type $void) + ;; WITHOUT-NEXT: (i32.const 0) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-indirect-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call_indirect $t (type $void) + ;; INCLUDE-NEXT: (i32.const 0) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $return-call-indirect-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target may throw. However, + ;; the surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call_indirect + (i32.const 0) + ) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $return-call-ref-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call_ref $void + ;; WITHOUT-NEXT: (ref.func $throw) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-ref-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call_ref $void + ;; INCLUDE-NEXT: (ref.func $throw) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $return-call-ref-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target may throw. However, + ;; the surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call_ref $void + (ref.func $throw) + ) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $call-return-call-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (try $try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (try $try1 + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (try $try2 + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) + ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) + ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-return-call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (try $try1 + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (try $try2 + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; INCLUDE-NEXT: ) + (func $call-return-call-throw-and-catch + (try + (do + ;; Even though the body of the previous function is a try-catch_all, the + ;; function still throws because of its return_call, so this cannot be + ;; optimized out, but once again the entire try-catch can be. + (call $return-call-throw-and-catch) + ) + (catch_all) + ) + (try + (do + ;; This would be the same, except since it performs an indirect call, we + ;; conservatively assume it could have any effect, so we can't optimize. + (call $return-call-indirect-throw-and-catch) + ) + (catch_all) + ) + (try + (do + ;; Same here. + (call $return-call-ref-throw-and-catch) + ) + (catch_all) + ) + + ;; These cannot be optimized out at all. + (call $return-call-throw-and-catch) + (call $return-call-indirect-throw-and-catch) + (call $return-call-ref-throw-and-catch) + ) + + ;; WITHOUT: (func $call-unreachable-and-catch (type $void) ;; WITHOUT-NEXT: (try $try ;; WITHOUT-NEXT: (do ;; WITHOUT-NEXT: (call $unreachable) @@ -229,7 +389,7 @@ ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-unreachable-and-catch (type $0) + ;; INCLUDE: (func $call-unreachable-and-catch (type $void) ;; INCLUDE-NEXT: (call $unreachable) ;; INCLUDE-NEXT: ) (func $call-unreachable-and-catch @@ -299,20 +459,20 @@ ) ) - ;; WITHOUT: (func $throw (type $0) + ;; WITHOUT: (func $throw (type $void) ;; WITHOUT-NEXT: (throw $tag) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $throw (type $0) + ;; INCLUDE: (func $throw (type $void) ;; INCLUDE-NEXT: (throw $tag) ;; INCLUDE-NEXT: ) (func $throw (throw $tag) ) - ;; WITHOUT: (func $throw-and-import (type $0) + ;; WITHOUT: (func $throw-and-import (type $void) ;; WITHOUT-NEXT: (throw $tag) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $throw-and-import (type $0) + ;; INCLUDE: (func $throw-and-import (type $void) ;; INCLUDE-NEXT: (throw $tag) ;; INCLUDE-NEXT: ) (func $throw-and-import @@ -327,11 +487,11 @@ ) ) - ;; WITHOUT: (func $cycle-with-unknown-call (type $0) + ;; WITHOUT: (func $cycle-with-unknown-call (type $void) ;; WITHOUT-NEXT: (call $cycle-with-unknown-call) ;; WITHOUT-NEXT: (call $import) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $cycle-with-unknown-call (type $0) + ;; INCLUDE: (func $cycle-with-unknown-call (type $void) ;; INCLUDE-NEXT: (call $cycle-with-unknown-call) ;; INCLUDE-NEXT: (call $import) ;; INCLUDE-NEXT: ) diff --git a/test/lit/passes/inlining-unreachable.wast b/test/lit/passes/inlining-unreachable.wast index 9ad141435..2c93359fd 100644 --- a/test/lit/passes/inlining-unreachable.wast +++ b/test/lit/passes/inlining-unreachable.wast @@ -77,8 +77,16 @@ ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; CHECK-NEXT: (block $__inlined_func$callee - ;; CHECK-NEXT: (call $imported - ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (br $__return_call) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $imported + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -114,7 +122,12 @@ ;; CHECK-NEXT: (block $__inlined_func$0 ;; CHECK-NEXT: (block ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/lit/passes/inlining_all-features.wast b/test/lit/passes/inlining_all-features.wast index 65dabdd6c..c274e9abb 100644 --- a/test/lit/passes/inlining_all-features.wast +++ b/test/lit/passes/inlining_all-features.wast @@ -100,7 +100,12 @@ ) ;; CHECK: (func $1 (type $none_=>_none) ;; CHECK-NEXT: (block $__inlined_func$0 - ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $1 diff --git a/test/lit/passes/inlining_enable-tail-call.wast b/test/lit/passes/inlining_enable-tail-call.wast index d2d7772cd..49ea1c3c4 100644 --- a/test/lit/passes/inlining_enable-tail-call.wast +++ b/test/lit/passes/inlining_enable-tail-call.wast @@ -468,74 +468,303 @@ (call $0) ) ) + (module - ;; CHECK: (type $0 (func (result i32))) + ;; No params, no results + ;; CHECK: (type $0 (func)) - ;; CHECK: (func $0 (result i32) - ;; CHECK-NEXT: (return - ;; CHECK-NEXT: (block $__inlined_func$1 (result i32) + ;; CHECK: (func $caller + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$callee + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $0 (result i32) - (return_call $1) + (func $caller + (return_call $callee) ) - (func $1 (result i32) + (func $callee + (drop + (i32.const 42) + ) + ) +) + +(module + ;; No params, one result + ;; CHECK: (type $0 (func (result i32))) + + ;; CHECK: (func $caller (result i32) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$callee (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (result i32) + (return_call $callee) + ) + (func $callee (result i32) (i32.const 42) ) ) + (module + ;; One param, no results ;; CHECK: (type $0 (func)) - ;; CHECK: (func $0 + ;; CHECK: (func $caller ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (block $__inlined_func$1 - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (return) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$callee + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $0 - (return_call $1 + (func $caller + (return_call $callee (i32.const 42) ) ) - (func $1 (param i32) + (func $callee (param i32) (drop (local.get 0) ) ) ) + (module + ;; One param, one result ;; CHECK: (type $0 (func (result i32))) - ;; CHECK: (func $0 (result i32) + ;; CHECK: (func $caller (result i32) ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (return - ;; CHECK-NEXT: (block $__inlined_func$1 (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$callee (result i32) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $0 (result i32) - (return_call $1 + (func $caller (result i32) + (return_call $callee (i32.const 42) ) ) - (func $1 (param i32) (result i32) + (func $callee (param i32) (result i32) (local.get 0) ) ) + +(module + ;; Multiple params, no result + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $caller + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$callee + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (local $x i32) + (local $y i32) + (return_call $callee + (local.get $x) + (local.get $y) + ) + ) + (func $callee (param i32 i32) + (drop + (local.get 0) + ) + (drop + (local.get 1) + ) + ) +) + +(module + ;; Chain of return_calls, no params, no results. + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $first + ;; CHECK-NEXT: (block $__original_body_0 + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$second + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__original_body_0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$third$1 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $first + (return_call $second) + ) + (func $second + (return_call $third) + ) + (func $third + (drop + (i32.const 42) + ) + ) +) + +(module + ;; Chain of return_calls with params and results. + ;; CHECK: (type $0 (func (result i32))) + + ;; CHECK: (func $first (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (block $__original_body_0 + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$second + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body_0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$third$1 (result i32) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $first (result i32) + (local $x i32) + (local $y i32) + (return_call $second + (local.get $x) + (local.get $y) + ) + ) + (func $second (param i32 i32) (result i32) + (return_call $third + (local.get 0) + (local.get 1) + ) + ) + (func $third (param i32 i32) (result i32) + (drop + (local.get 0) + ) + (drop + (local.get 1) + ) + (i32.const 42) + ) +) + (module ;; CHECK: (type $0 (func)) @@ -543,14 +772,19 @@ ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (block $__inlined_func$1 (result i32) - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (br $__inlined_func$1 - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (block $__inlined_func$2$1 (result i32) - ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__return_call) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block $__inlined_func$2$1 (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -568,6 +802,7 @@ (i32.const 42) ) ) + (module ;; CHECK: (type $0 (func)) @@ -575,19 +810,110 @@ ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (block $__inlined_func$1 ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__return_call) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__inlined_func$1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__inlined_func$2$1 + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 + (call $1) + ) + (func $1 + (return_call $2 + (i32.const 42) + ) + ) + (func $2 (param i32) + (drop + (local.get 0) + ) + ) +) + +(module + ;; Same as above, but with nontrivial effects in the children. + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (func $0 + ;; CHECK-NEXT: (block $__inlined_func$1 + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (br $__inlined_func$1) + ;; CHECK-NEXT: (br $__return_call) + ;; CHECK-NEXT: (br $__inlined_func$1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $2 + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 + (call $1) + ) + (func $1 + (return_call $2 + (return) + ) + ) + ;; CHECK: (func $2 (param $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $2 (param i32) + (drop + (local.get 0) + ) + ) +) + +(module + ;; Same as above, but this time the child is not unreachable. + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $0 + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (block $__inlined_func$1 + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__return_call ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (block $__inlined_func$2$1 - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.tee $0 + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__inlined_func$1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__return_call) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $__inlined_func$1) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__inlined_func$2$1 + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -596,7 +922,9 @@ ) (func $1 (return_call $2 - (i32.const 42) + (block (result i32) + (return) + ) ) ) (func $2 (param i32) @@ -605,6 +933,7 @@ ) ) ) + (module ;; CHECK: (type $0 (func)) @@ -617,7 +946,12 @@ ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (block $__inlined_func$1 (result i32) - ;; CHECK-NEXT: (br $__inlined_func$1 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__return_call) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (call_indirect (type $T) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: (i32.const 0) @@ -650,11 +984,14 @@ ;; CHECK: (func $0 ;; CHECK-NEXT: (block $__inlined_func$1 ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__return_call + ;; CHECK-NEXT: (br $__return_call) + ;; CHECK-NEXT: (br $__inlined_func$1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (call_indirect (type $T) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $__inlined_func$1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -703,28 +1040,31 @@ ;; CHECK-NEXT: (block ;; CHECK-NEXT: (block $__inlined_func$13$1 ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (global.get $global$0) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (global.get $global$0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__inlined_func$13$1) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $__inlined_func$2 ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (block $__inlined_func$2 - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (global.get $global$0) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $__inlined_func$2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.set $global$0 - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (global.get $global$0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__inlined_func$2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $__inlined_func$13$1) + ;; CHECK-NEXT: (global.set $global$0 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -746,37 +1086,42 @@ (export "is_even" (func $is_even)) ;; CHECK: (func $is_even (param $i i32) (result i32) ;; CHECK-NEXT: (local $1 i32) - ;; CHECK-NEXT: (if (result i32) - ;; CHECK-NEXT: (i32.eqz - ;; CHECK-NEXT: (local.get $i) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (return - ;; CHECK-NEXT: (block $__inlined_func$is_odd (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (i32.sub - ;; CHECK-NEXT: (local.get $i) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if (result i32) - ;; CHECK-NEXT: (i32.eqz - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (return_call $is_even - ;; CHECK-NEXT: (i32.sub - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__original_body + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $__original_body) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $__inlined_func$is_odd (result i32) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (return_call $is_even + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/lit/passes/simplify-locals-eh-old.wast b/test/lit/passes/simplify-locals-eh-old.wast index 5e4a395d3..0bf26f34d 100644 --- a/test/lit/passes/simplify-locals-eh-old.wast +++ b/test/lit/passes/simplify-locals-eh-old.wast @@ -152,6 +152,57 @@ ) ) + ;; CHECK: (func $return-call-can-be-sinked-into-try (type $3) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (try $try (result i32) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $return-call-can-be-sinked-into-try) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e-i32 + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-can-be-sinked-into-try (result i32) + (local $0 i32) + (drop + ;; This cannot throw either, so it can be sunk. Wrap the return_call in an + ;; if so the whole expression does not return unconditionally. + (local.tee $0 + (if (result i32) + (i32.const 0) + (then + (return_call $return-call-can-be-sinked-into-try) + ) + (else + (i32.const 1) + ) + ) + ) + ) + (try (result i32) + (do + (drop (local.get $0)) + (i32.const 0) + ) + (catch $e-i32 + (pop i32) + ) + ) + ) + ;; CHECK: (func $equivalent-set-removal-call (type $1) (param $0 i32) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (nop) diff --git a/test/lit/passes/vacuum-eh-old.wast b/test/lit/passes/vacuum-eh-old.wast index 68b5f7b1e..0215b5b25 100644 --- a/test/lit/passes/vacuum-eh-old.wast +++ b/test/lit/passes/vacuum-eh-old.wast @@ -2,12 +2,19 @@ ;; RUN: wasm-opt %s --vacuum -all -S -o - | filecheck %s (module + ;; CHECK: (type $void (func)) + (type $void (func)) + + ;; CHECK: (table $t 0 funcref) + ;; CHECK: (tag $e (param i32)) (tag $e (param i32)) ;; CHECK: (tag $e2 (param i32)) (tag $e2 (param i32)) - ;; CHECK: (func $try-test (type $0) + (table $t funcref 0) + + ;; CHECK: (func $try-test (type $void) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $try-test @@ -60,7 +67,7 @@ (i32.const 2) ) - ;; CHECK: (func $inner-try-catch-test (type $0) + ;; CHECK: (func $inner-try-catch-test (type $void) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (try $try ;; CHECK-NEXT: (do @@ -108,7 +115,7 @@ ) ) - ;; CHECK: (func $br-in-catch (type $0) + ;; CHECK: (func $br-in-catch (type $void) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) (func $br-in-catch @@ -128,7 +135,7 @@ ) ) - ;; CHECK: (func $try-delegate-outer-target (type $0) + ;; CHECK: (func $try-delegate-outer-target (type $void) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (try $label$0 ;; CHECK-NEXT: (do @@ -179,7 +186,7 @@ ) ) - ;; CHECK: (func $trivial-catch-all-of-throw (type $0) + ;; CHECK: (func $trivial-catch-all-of-throw (type $void) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (try $try3 ;; CHECK-NEXT: (do @@ -225,4 +232,66 @@ (catch_all) ) ) + + ;; CHECK: (func $throw (type $void) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $throw + ;; Helper for the tail call tests below. + (throw $e + (i32.const 0) + ) + ) + + ;; CHECK: (func $return-call-catch (type $void) + ;; CHECK-NEXT: (return_call $throw) + ;; CHECK-NEXT: ) + (func $return-call-catch + (try + (do + ;; This returns before it throws, so we can optimize out the surrounding + ;; try-catch. + (return_call $throw) + ) + (catch_all) + ) + ) + + ;; CHECK: (func $return-call-indirect-catch (type $void) + ;; CHECK-NEXT: (return_call_indirect $t (type $void) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-indirect-catch + (try + (do + ;; This returns before it throws, so we can optimize out the surrounding + ;; try-catch. + (return_call_indirect + (i32.const 0) + ) + ) + (catch_all) + ) + ) + + ;; CHECK: (func $return-call-ref-catch (type $void) + ;; CHECK-NEXT: (return_call_ref $void + ;; CHECK-NEXT: (ref.func $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-ref-catch + (try + (do + ;; This returns before it throws, so we can optimize out the surrounding + ;; try-catch. + (return_call_ref $void + (ref.func $throw) + ) + ) + (catch_all) + ) + ) ) diff --git a/test/spec/return_call.wast b/test/spec/return_call.wast new file mode 100644 index 000000000..9423159ff --- /dev/null +++ b/test/spec/return_call.wast @@ -0,0 +1,202 @@ +;; Test `return_call` operator + +(module + ;; Auxiliary definitions + (func $const-i32 (result i32) (i32.const 0x132)) + (func $const-i64 (result i64) (i64.const 0x164)) + (func $const-f32 (result f32) (f32.const 0xf32)) + (func $const-f64 (result f64) (f64.const 0xf64)) + + (func $id-i32 (param i32) (result i32) (local.get 0)) + (func $id-i64 (param i64) (result i64) (local.get 0)) + (func $id-f32 (param f32) (result f32) (local.get 0)) + (func $id-f64 (param f64) (result f64) (local.get 0)) + + (func $f32-i32 (param f32 i32) (result i32) (local.get 1)) + (func $i32-i64 (param i32 i64) (result i64) (local.get 1)) + (func $f64-f32 (param f64 f32) (result f32) (local.get 1)) + (func $i64-f64 (param i64 f64) (result f64) (local.get 1)) + + ;; Typing + + (func (export "type-i32") (result i32) (return_call $const-i32)) + (func (export "type-i64") (result i64) (return_call $const-i64)) + (func (export "type-f32") (result f32) (return_call $const-f32)) + (func (export "type-f64") (result f64) (return_call $const-f64)) + + (func (export "type-first-i32") (result i32) (return_call $id-i32 (i32.const 32))) + (func (export "type-first-i64") (result i64) (return_call $id-i64 (i64.const 64))) + (func (export "type-first-f32") (result f32) (return_call $id-f32 (f32.const 1.32))) + (func (export "type-first-f64") (result f64) (return_call $id-f64 (f64.const 1.64))) + + (func (export "type-second-i32") (result i32) + (return_call $f32-i32 (f32.const 32.1) (i32.const 32)) + ) + (func (export "type-second-i64") (result i64) + (return_call $i32-i64 (i32.const 32) (i64.const 64)) + ) + (func (export "type-second-f32") (result f32) + (return_call $f64-f32 (f64.const 64) (f32.const 32)) + ) + (func (export "type-second-f64") (result f64) + (return_call $i64-f64 (i64.const 64) (f64.const 64.1)) + ) + + ;; Recursion + + (func $fac-acc (export "fac-acc") (param i64 i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 1)) + (else + (return_call $fac-acc + (i64.sub (local.get 0) (i64.const 1)) + (i64.mul (local.get 0) (local.get 1)) + ) + ) + ) + ) + + (func $count (export "count") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 0)) + (else (return_call $count (i64.sub (local.get 0) (i64.const 1)))) + ) + ) + + (func $even (export "even") (param i64) (result i32) + (if (result i32) (i64.eqz (local.get 0)) + (then (i32.const 44)) + (else (return_call $odd (i64.sub (local.get 0) (i64.const 1)))) + ) + ) + (func $odd (export "odd") (param i64) (result i32) + (if (result i32) (i64.eqz (local.get 0)) + (then (i32.const 99)) + (else (return_call $even (i64.sub (local.get 0) (i64.const 1)))) + ) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) + +(assert_return (invoke "fac-acc" (i64.const 0) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 1) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 5) (i64.const 1)) (i64.const 120)) +(assert_return + (invoke "fac-acc" (i64.const 25) (i64.const 1)) + (i64.const 7034535277573963776) +) + +(assert_return (invoke "count" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1000)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1000000)) (i64.const 0)) + +(assert_return (invoke "even" (i64.const 0)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 1)) (i32.const 99)) +(assert_return (invoke "even" (i64.const 100)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 77)) (i32.const 99)) +(assert_return (invoke "even" (i64.const 1000000)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 1000001)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 0)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 1)) (i32.const 44)) +(assert_return (invoke "odd" (i64.const 200)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 77)) (i32.const 44)) +(assert_return (invoke "odd" (i64.const 1000000)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 999999)) (i32.const 44)) + + +;; Invalid typing + +(assert_invalid + (module + (func $type-void-vs-num (result i32) (return_call 1) (i32.const 0)) + (func) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-num-vs-num (result i32) (return_call 1) (i32.const 0)) + (func (result i64) (i64.const 1)) + ) + "type mismatch" +) + +(assert_invalid + (module + (func $arity-0-vs-1 (return_call 1)) + (func (param i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $arity-0-vs-2 (return_call 1)) + (func (param f64 i32)) + ) + "type mismatch" +) + +;; (module +;; (func $arity-1-vs-0 (i32.const 1) (return_call 1)) +;; (func) +;; ) + +;; (module +;; (func $arity-2-vs-0 (f64.const 2) (i32.const 1) (return_call 1)) +;; (func) +;; ) + +(assert_invalid + (module + (func $type-first-void-vs-num (return_call 1 (nop) (i32.const 1))) + (func (param i32 i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-second-void-vs-num (return_call 1 (i32.const 1) (nop))) + (func (param i32 i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-first-num-vs-num (return_call 1 (f64.const 1) (i32.const 1))) + (func (param i32 f64)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-second-num-vs-num (return_call 1 (i32.const 1) (f64.const 1))) + (func (param f64 i32)) + ) + "type mismatch" +) + + +;; Unbound function + +(assert_invalid + (module (func $unbound-func (return_call 1))) + "unknown function" +) +(assert_invalid + (module (func $large-func (return_call 1012321300))) + "unknown function" +) diff --git a/test/spec/return_call_eh.wast b/test/spec/return_call_eh.wast new file mode 100644 index 000000000..e85fdf7c6 --- /dev/null +++ b/test/spec/return_call_eh.wast @@ -0,0 +1,35 @@ +;; Test the combination of 'return_call' with exception handling + +(module + (tag $t) + + (func $test (export "test") (result i32) + (try (result i32) + (do + (call $return-call-in-try) + ) + (catch_all + ;; Catch the exception thrown from $return-callee + (i32.const 42) + ) + ) + + ) + + (func $return-call-in-try (result i32) + (try (result i32) + (do + (return_call $return-callee) + ) + (catch_all + (unreachable) + ) + ) + ) + + (func $return-callee (result i32) + (throw $t) + ) +) + +(assert_return (invoke "test") (i32.const 42)) diff --git a/test/spec/return_call_indirect.wast b/test/spec/return_call_indirect.wast new file mode 100644 index 000000000..27f1dcdbf --- /dev/null +++ b/test/spec/return_call_indirect.wast @@ -0,0 +1,536 @@ +;; Test `return_call_indirect` operator + +(module + ;; Auxiliary definitions + (type $proc (func)) + (type $out-i32 (func (result i32))) + (type $out-i64 (func (result i64))) + (type $out-f32 (func (result f32))) + (type $out-f64 (func (result f64))) + (type $over-i32 (func (param i32) (result i32))) + (type $over-i64 (func (param i64) (result i64))) + (type $over-f32 (func (param f32) (result f32))) + (type $over-f64 (func (param f64) (result f64))) + (type $f32-i32 (func (param f32 i32) (result i32))) + (type $i32-i64 (func (param i32 i64) (result i64))) + (type $f64-f32 (func (param f64 f32) (result f32))) + (type $i64-f64 (func (param i64 f64) (result f64))) + (type $over-i32-duplicate (func (param i32) (result i32))) + (type $over-i64-duplicate (func (param i64) (result i64))) + (type $over-f32-duplicate (func (param f32) (result f32))) + (type $over-f64-duplicate (func (param f64) (result f64))) + + (func $const-i32 (type $out-i32) (i32.const 0x132)) + (func $const-i64 (type $out-i64) (i64.const 0x164)) + (func $const-f32 (type $out-f32) (f32.const 0xf32)) + (func $const-f64 (type $out-f64) (f64.const 0xf64)) + + (func $id-i32 (type $over-i32) (local.get 0)) + (func $id-i64 (type $over-i64) (local.get 0)) + (func $id-f32 (type $over-f32) (local.get 0)) + (func $id-f64 (type $over-f64) (local.get 0)) + + (func $i32-i64 (type $i32-i64) (local.get 1)) + (func $i64-f64 (type $i64-f64) (local.get 1)) + (func $f32-i32 (type $f32-i32) (local.get 1)) + (func $f64-f32 (type $f64-f32) (local.get 1)) + + (func $over-i32-duplicate (type $over-i32-duplicate) (local.get 0)) + (func $over-i64-duplicate (type $over-i64-duplicate) (local.get 0)) + (func $over-f32-duplicate (type $over-f32-duplicate) (local.get 0)) + (func $over-f64-duplicate (type $over-f64-duplicate) (local.get 0)) + + (table funcref + (elem + $const-i32 $const-i64 $const-f32 $const-f64 + $id-i32 $id-i64 $id-f32 $id-f64 + $f32-i32 $i32-i64 $f64-f32 $i64-f64 + $fac $fac-acc $even $odd + $over-i32-duplicate $over-i64-duplicate + $over-f32-duplicate $over-f64-duplicate + ) + ) + + ;; Syntax + + (func + (return_call_indirect (i32.const 0)) + (return_call_indirect (param i64) (i64.const 0) (i32.const 0)) + (return_call_indirect (param i64) (param) (param f64 i32 i64) + (i64.const 0) (f64.const 0) (i32.const 0) (i64.const 0) (i32.const 0) + ) + (return_call_indirect (result) (i32.const 0)) + ) + + (func (result i32) + (return_call_indirect (result i32) (i32.const 0)) + (return_call_indirect (result i32) (result) (i32.const 0)) + (return_call_indirect (param i64) (result i32) (i64.const 0) (i32.const 0)) + (return_call_indirect + (param) (param i64) (param) (param f64 i32 i64) (param) (param) + (result) (result i32) (result) (result) + (i64.const 0) (f64.const 0) (i32.const 0) (i64.const 0) (i32.const 0) + ) + ) + + (func (result i64) + (return_call_indirect (type $over-i64) (param i64) (result i64) + (i64.const 0) (i32.const 0) + ) + ) + + ;; Typing + + (func (export "type-i32") (result i32) + (return_call_indirect (type $out-i32) (i32.const 0)) + ) + (func (export "type-i64") (result i64) + (return_call_indirect (type $out-i64) (i32.const 1)) + ) + (func (export "type-f32") (result f32) + (return_call_indirect (type $out-f32) (i32.const 2)) + ) + (func (export "type-f64") (result f64) + (return_call_indirect (type $out-f64) (i32.const 3)) + ) + + (func (export "type-index") (result i64) + (return_call_indirect (type $over-i64) (i64.const 100) (i32.const 5)) + ) + + (func (export "type-first-i32") (result i32) + (return_call_indirect (type $over-i32) (i32.const 32) (i32.const 4)) + ) + (func (export "type-first-i64") (result i64) + (return_call_indirect (type $over-i64) (i64.const 64) (i32.const 5)) + ) + (func (export "type-first-f32") (result f32) + (return_call_indirect (type $over-f32) (f32.const 1.32) (i32.const 6)) + ) + (func (export "type-first-f64") (result f64) + (return_call_indirect (type $over-f64) (f64.const 1.64) (i32.const 7)) + ) + + (func (export "type-second-i32") (result i32) + (return_call_indirect (type $f32-i32) + (f32.const 32.1) (i32.const 32) (i32.const 8) + ) + ) + (func (export "type-second-i64") (result i64) + (return_call_indirect (type $i32-i64) + (i32.const 32) (i64.const 64) (i32.const 9) + ) + ) + (func (export "type-second-f32") (result f32) + (return_call_indirect (type $f64-f32) + (f64.const 64) (f32.const 32) (i32.const 10) + ) + ) + (func (export "type-second-f64") (result f64) + (return_call_indirect (type $i64-f64) + (i64.const 64) (f64.const 64.1) (i32.const 11) + ) + ) + + ;; Dispatch + + (func (export "dispatch") (param i32 i64) (result i64) + (return_call_indirect (type $over-i64) (local.get 1) (local.get 0)) + ) + + (func (export "dispatch-structural") (param i32) (result i64) + (return_call_indirect (type $over-i64-duplicate) + (i64.const 9) (local.get 0) + ) + ) + + ;; Multiple tables + + (table $tab2 funcref (elem $tab-f1)) + (table $tab3 funcref (elem $tab-f2)) + + (func $tab-f1 (result i32) (i32.const 0x133)) + (func $tab-f2 (result i32) (i32.const 0x134)) + + (func (export "call-tab") (param $i i32) (result i32) + (if (i32.eq (local.get $i) (i32.const 0)) + (then (return_call_indirect (type $out-i32) (i32.const 0))) + ) + (if (i32.eq (local.get $i) (i32.const 1)) + (then (return_call_indirect $tab2 (type $out-i32) (i32.const 0))) + ) + (if (i32.eq (local.get $i) (i32.const 2)) + (then (return_call_indirect $tab3 (type $out-i32) (i32.const 0))) + ) + (i32.const 0) + ) + + ;; Recursion + + (func $fac (export "fac") (type $over-i64) + (return_call_indirect (param i64 i64) (result i64) + (local.get 0) (i64.const 1) (i32.const 13) + ) + ) + + (func $fac-acc (param i64 i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 1)) + (else + (return_call_indirect (param i64 i64) (result i64) + (i64.sub (local.get 0) (i64.const 1)) + (i64.mul (local.get 0) (local.get 1)) + (i32.const 13) + ) + ) + ) + ) + + (func $even (export "even") (param i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then (i32.const 44)) + (else + (return_call_indirect (type $over-i32) + (i32.sub (local.get 0) (i32.const 1)) + (i32.const 15) + ) + ) + ) + ) + (func $odd (export "odd") (param i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then (i32.const 99)) + (else + (return_call_indirect (type $over-i32) + (i32.sub (local.get 0) (i32.const 1)) + (i32.const 14) + ) + ) + ) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-index") (i64.const 100)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) + +(assert_return (invoke "dispatch" (i32.const 5) (i64.const 2)) (i64.const 2)) +(assert_return (invoke "dispatch" (i32.const 5) (i64.const 5)) (i64.const 5)) +(assert_return (invoke "dispatch" (i32.const 12) (i64.const 5)) (i64.const 120)) +(assert_return (invoke "dispatch" (i32.const 17) (i64.const 2)) (i64.const 2)) +(assert_trap (invoke "dispatch" (i32.const 0) (i64.const 2)) "indirect call type mismatch") +(assert_trap (invoke "dispatch" (i32.const 15) (i64.const 2)) "indirect call type mismatch") +(assert_trap (invoke "dispatch" (i32.const 20) (i64.const 2)) "undefined element") +(assert_trap (invoke "dispatch" (i32.const -1) (i64.const 2)) "undefined element") +(assert_trap (invoke "dispatch" (i32.const 1213432423) (i64.const 2)) "undefined element") + +(assert_return (invoke "dispatch-structural" (i32.const 5)) (i64.const 9)) +(assert_return (invoke "dispatch-structural" (i32.const 5)) (i64.const 9)) +(assert_return (invoke "dispatch-structural" (i32.const 12)) (i64.const 362880)) +(assert_return (invoke "dispatch-structural" (i32.const 17)) (i64.const 9)) +(assert_trap (invoke "dispatch-structural" (i32.const 11)) "indirect call type mismatch") +(assert_trap (invoke "dispatch-structural" (i32.const 16)) "indirect call type mismatch") + +(assert_return (invoke "call-tab" (i32.const 0)) (i32.const 0x132)) +(assert_return (invoke "call-tab" (i32.const 1)) (i32.const 0x133)) +(assert_return (invoke "call-tab" (i32.const 2)) (i32.const 0x134)) + +(assert_return (invoke "fac" (i64.const 0)) (i64.const 1)) +(assert_return (invoke "fac" (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac" (i64.const 5)) (i64.const 120)) +(assert_return (invoke "fac" (i64.const 25)) (i64.const 7034535277573963776)) + +(assert_return (invoke "even" (i32.const 0)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 1)) (i32.const 99)) +(assert_return (invoke "even" (i32.const 100)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 77)) (i32.const 99)) +(assert_return (invoke "even" (i32.const 100000)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 111111)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 0)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 1)) (i32.const 44)) +(assert_return (invoke "odd" (i32.const 200)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 77)) (i32.const 44)) +(assert_return (invoke "odd" (i32.const 200002)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 300003)) (i32.const 44)) + + +;; Invalid syntax + +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (param i32) (type $sig) (result i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (param i32) (result i32) (type $sig)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (type $sig) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (param i32) (type $sig)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) + +(assert_malformed + (module quote + "(table 0 funcref)" + "(func (return_call_indirect (param $x i32) (i32.const 0) (i32.const 0)))" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (i32.const 0))" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (i32.const 0))" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func" + " (return_call_indirect (type $sig) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32 i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (param i32) (result i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "inline function type" +) + +;; Invalid typing + +(assert_invalid + (module + (type (func)) + (func $no-table (return_call_indirect (type 0) (i32.const 0))) + ) + "unknown table" +) + +;; (assert_invalid +;; (module +;; (type (func)) +;; (table 0 funcref) +;; (func $type-void-vs-num (i32.eqz (return_call_indirect (type 0) (i32.const 0)))) +;; ) +;; "type mismatch" +;; ) +(assert_invalid + (module + (type (func (result i64))) + (table 0 funcref) + (func $type-num-vs-num (i32.eqz (return_call_indirect (type 0) (i32.const 0)))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $arity-0-vs-1 (return_call_indirect (type 0) (i32.const 0))) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param f64 i32))) + (table 0 funcref) + (func $arity-0-vs-2 (return_call_indirect (type 0) (i32.const 0))) + ) + "type mismatch" +) + +;; (module +;; (type (func)) +;; (table 0 funcref) +;; (func $arity-1-vs-0 (return_call_indirect (type 0) (i32.const 1) (i32.const 0))) +;; ) + +;; (module +;; (type (func)) +;; (table 0 funcref) +;; (func $arity-2-vs-0 +;; (return_call_indirect (type 0) (f64.const 2) (i32.const 1) (i32.const 0)) +;; ) +;; ) + +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $type-func-void-vs-i32 (return_call_indirect (type 0) (i32.const 1) (nop))) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $type-func-num-vs-i32 (return_call_indirect (type 0) (i32.const 0) (i64.const 1))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type (func (param i32 i32))) + (table 0 funcref) + (func $type-first-void-vs-num + (return_call_indirect (type 0) (nop) (i32.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32 i32))) + (table 0 funcref) + (func $type-second-void-vs-num + (return_call_indirect (type 0) (i32.const 1) (nop) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32 f64))) + (table 0 funcref) + (func $type-first-num-vs-num + (return_call_indirect (type 0) (f64.const 1) (i32.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param f64 i32))) + (table 0 funcref) + (func $type-second-num-vs-num + (return_call_indirect (type 0) (i32.const 1) (f64.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) + + +;; Unbound type + +(assert_invalid + (module + (table 0 funcref) + (func $unbound-type (return_call_indirect (type 1) (i32.const 0))) + ) + "unknown type" +) +(assert_invalid + (module + (table 0 funcref) + (func $large-type (return_call_indirect (type 1012321300) (i32.const 0))) + ) + "unknown type" +) + + +;; Unbound function in table + +(assert_invalid + (module (table funcref (elem 0 0))) + "unknown function 0" +) diff --git a/test/spec/return_call_ref.wast b/test/spec/return_call_ref.wast new file mode 100644 index 000000000..6bb5d2a14 --- /dev/null +++ b/test/spec/return_call_ref.wast @@ -0,0 +1,377 @@ +;; Test `return_call_ref` operator + +(module + ;; Auxiliary definitions + (type $proc (func)) + (type $-i32 (func (result i32))) + (type $-i64 (func (result i64))) + (type $-f32 (func (result f32))) + (type $-f64 (func (result f64))) + + (type $i32-i32 (func (param i32) (result i32))) + (type $i64-i64 (func (param i64) (result i64))) + (type $f32-f32 (func (param f32) (result f32))) + (type $f64-f64 (func (param f64) (result f64))) + + (type $f32-i32 (func (param f32 i32) (result i32))) + (type $i32-i64 (func (param i32 i64) (result i64))) + (type $f64-f32 (func (param f64 f32) (result f32))) + (type $i64-f64 (func (param i64 f64) (result f64))) + + (type $i64i64-i64 (func (param i64 i64) (result i64))) + + (func $const-i32 (result i32) (i32.const 0x132)) + (func $const-i64 (result i64) (i64.const 0x164)) + (func $const-f32 (result f32) (f32.const 0xf32)) + (func $const-f64 (result f64) (f64.const 0xf64)) + + (func $id-i32 (param i32) (result i32) (local.get 0)) + (func $id-i64 (param i64) (result i64) (local.get 0)) + (func $id-f32 (param f32) (result f32) (local.get 0)) + (func $id-f64 (param f64) (result f64) (local.get 0)) + + (func $f32-i32 (param f32 i32) (result i32) (local.get 1)) + (func $i32-i64 (param i32 i64) (result i64) (local.get 1)) + (func $f64-f32 (param f64 f32) (result f32) (local.get 1)) + (func $i64-f64 (param i64 f64) (result f64) (local.get 1)) + + (global $const-i32 (ref $-i32) (ref.func $const-i32)) + (global $const-i64 (ref $-i64) (ref.func $const-i64)) + (global $const-f32 (ref $-f32) (ref.func $const-f32)) + (global $const-f64 (ref $-f64) (ref.func $const-f64)) + + (global $id-i32 (ref $i32-i32) (ref.func $id-i32)) + (global $id-i64 (ref $i64-i64) (ref.func $id-i64)) + (global $id-f32 (ref $f32-f32) (ref.func $id-f32)) + (global $id-f64 (ref $f64-f64) (ref.func $id-f64)) + + (global $f32-i32 (ref $f32-i32) (ref.func $f32-i32)) + (global $i32-i64 (ref $i32-i64) (ref.func $i32-i64)) + (global $f64-f32 (ref $f64-f32) (ref.func $f64-f32)) + (global $i64-f64 (ref $i64-f64) (ref.func $i64-f64)) + + (elem declare func + $const-i32 $const-i64 $const-f32 $const-f64 + $id-i32 $id-i64 $id-f32 $id-f64 + $f32-i32 $i32-i64 $f64-f32 $i64-f64 + ) + + ;; Typing + + (func (export "type-i32") (result i32) + (return_call_ref $-i32 (global.get $const-i32)) + ) + (func (export "type-i64") (result i64) + (return_call_ref $-i64 (global.get $const-i64)) + ) + (func (export "type-f32") (result f32) + (return_call_ref $-f32 (global.get $const-f32)) + ) + (func (export "type-f64") (result f64) + (return_call_ref $-f64 (global.get $const-f64)) + ) + + (func (export "type-first-i32") (result i32) + (return_call_ref $i32-i32 (i32.const 32) (global.get $id-i32)) + ) + (func (export "type-first-i64") (result i64) + (return_call_ref $i64-i64 (i64.const 64) (global.get $id-i64)) + ) + (func (export "type-first-f32") (result f32) + (return_call_ref $f32-f32 (f32.const 1.32) (global.get $id-f32)) + ) + (func (export "type-first-f64") (result f64) + (return_call_ref $f64-f64 (f64.const 1.64) (global.get $id-f64)) + ) + + (func (export "type-second-i32") (result i32) + (return_call_ref $f32-i32 (f32.const 32.1) (i32.const 32) (global.get $f32-i32)) + ) + (func (export "type-second-i64") (result i64) + (return_call_ref $i32-i64 (i32.const 32) (i64.const 64) (global.get $i32-i64)) + ) + (func (export "type-second-f32") (result f32) + (return_call_ref $f64-f32 (f64.const 64) (f32.const 32) (global.get $f64-f32)) + ) + (func (export "type-second-f64") (result f64) + (return_call_ref $i64-f64 (i64.const 64) (f64.const 64.1) (global.get $i64-f64)) + ) + + ;; Null + + (func (export "null") + (return_call_ref $proc (ref.null $proc)) + ) + + ;; Recursion + + (global $fac-acc (ref $i64i64-i64) (ref.func $fac-acc)) + + (elem declare func $fac-acc) + (func $fac-acc (export "fac-acc") (param i64 i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 1)) + (else + (return_call_ref $i64i64-i64 + (i64.sub (local.get 0) (i64.const 1)) + (i64.mul (local.get 0) (local.get 1)) + (global.get $fac-acc) + ) + ) + ) + ) + + (global $count (ref $i64-i64) (ref.func $count)) + + (elem declare func $count) + (func $count (export "count") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 0)) + (else + (return_call_ref $i64-i64 + (i64.sub (local.get 0) (i64.const 1)) + (global.get $count) + ) + ) + ) + ) + + (global $even (ref $i64-i64) (ref.func $even)) + (global $odd (ref $i64-i64) (ref.func $odd)) + + (elem declare func $even) + (func $even (export "even") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (i64.const 44)) + (else + (return_call_ref $i64-i64 + (i64.sub (local.get 0) (i64.const 1)) + (global.get $odd) + ) + ) + ) + ) + (elem declare func $odd) + (func $odd (export "odd") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (i64.const 99)) + (else + (return_call_ref $i64-i64 + (i64.sub (local.get 0) (i64.const 1)) + (global.get $even) + ) + ) + ) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) + +(assert_trap (invoke "null") "null function reference") + +(assert_return (invoke "fac-acc" (i64.const 0) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 1) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 5) (i64.const 1)) (i64.const 120)) +(assert_return + (invoke "fac-acc" (i64.const 25) (i64.const 1)) + (i64.const 7034535277573963776) +) + +(assert_return (invoke "count" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1000)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1000000)) (i64.const 0)) + +(assert_return (invoke "even" (i64.const 0)) (i64.const 44)) +(assert_return (invoke "even" (i64.const 1)) (i64.const 99)) +(assert_return (invoke "even" (i64.const 100)) (i64.const 44)) +(assert_return (invoke "even" (i64.const 77)) (i64.const 99)) +(assert_return (invoke "even" (i64.const 1000000)) (i64.const 44)) +(assert_return (invoke "even" (i64.const 1000001)) (i64.const 99)) +(assert_return (invoke "odd" (i64.const 0)) (i64.const 99)) +(assert_return (invoke "odd" (i64.const 1)) (i64.const 44)) +(assert_return (invoke "odd" (i64.const 200)) (i64.const 99)) +(assert_return (invoke "odd" (i64.const 77)) (i64.const 44)) +(assert_return (invoke "odd" (i64.const 1000000)) (i64.const 99)) +(assert_return (invoke "odd" (i64.const 999999)) (i64.const 44)) + + +;; More typing + +(module + (type $t (func)) + (type $t1 (func (result (ref $t)))) + (type $t2 (func (result (ref null $t)))) + (type $t3 (func (result (ref func)))) + (type $t4 (func (result (ref null func)))) + (elem declare func $f11 $f22 $f33 $f44) + (func $f11 (result (ref $t)) (return_call_ref $t1 (ref.func $f11))) + (func $f21 (result (ref null $t)) (return_call_ref $t1 (ref.func $f11))) + (func $f22 (result (ref null $t)) (return_call_ref $t2 (ref.func $f22))) + (func $f31 (result (ref func)) (return_call_ref $t1 (ref.func $f11))) + (func $f33 (result (ref func)) (return_call_ref $t3 (ref.func $f33))) + (func $f41 (result (ref null func)) (return_call_ref $t1 (ref.func $f11))) + (func $f42 (result (ref null func)) (return_call_ref $t2 (ref.func $f22))) + (func $f43 (result (ref null func)) (return_call_ref $t3 (ref.func $f33))) + (func $f44 (result (ref null func)) (return_call_ref $t4 (ref.func $f44))) +) + +(assert_invalid + (module + (type $t (func)) + (type $t2 (func (result (ref null $t)))) + (elem declare func $f22) + (func $f12 (result (ref $t)) (return_call_ref $t2 (ref.func $f22))) + (func $f22 (result (ref null $t)) (return_call_ref $t2 (ref.func $f22))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t (func)) + (type $t3 (func (result (ref func)))) + (elem declare func $f33) + (func $f13 (result (ref $t)) (return_call_ref $t3 (ref.func $f33))) + (func $f33 (result (ref func)) (return_call_ref $t3 (ref.func $f33))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t (func)) + (type $t4 (func (result (ref null func)))) + (elem declare func $f44) + (func $f14 (result (ref $t)) (return_call_ref $t4 (ref.func $f44))) + (func $f44 (result (ref null func)) (return_call_ref $t4 (ref.func $f44))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t (func)) + (type $t3 (func (result (ref func)))) + (elem declare func $f33) + (func $f23 (result (ref null $t)) (return_call_ref $t3 (ref.func $f33))) + (func $f33 (result (ref func)) (return_call_ref $t3 (ref.func $f33))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t (func)) + (type $t4 (func (result (ref null func)))) + (elem declare func $f44) + (func $f24 (result (ref null $t)) (return_call_ref $t4 (ref.func $f44))) + (func $f44 (result (ref null func)) (return_call_ref $t4 (ref.func $f44))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t4 (func (result (ref null func)))) + (elem declare func $f44) + (func $f34 (result (ref func)) (return_call_ref $t4 (ref.func $f44))) + (func $f44 (result (ref null func)) (return_call_ref $t4 (ref.func $f44))) + ) + "type mismatch" +) + + +;; Unreachable typing. + +(module + (type $t (func (result i32))) + (func (export "unreachable") (result i32) + (return_call_ref $t (unreachable)) + ) +) +(assert_trap (invoke "unreachable") "unreachable") + +(module + (elem declare func $f) + (type $t (func (param i32) (result i32))) + (func $f (param i32) (result i32) (local.get 0)) + + (func (export "unreachable") (result i32) + (return_call_ref $t + (unreachable) + (ref.func $f) + ) + ) +) +(assert_trap (invoke "unreachable") "unreachable") + +(module + (elem declare func $f) + (type $t (func (param i32) (result i32))) + (func $f (param i32) (result i32) (local.get 0)) + + (func (export "unreachable") (result i32) + (unreachable) + (return_call_ref $t + (i32.const 0) + (ref.func $f) + ) + (i32.const 0) + ) +) +(assert_trap (invoke "unreachable") "unreachable") + +(assert_invalid + (module + (elem declare func $f) + (type $t (func (param i32) (result i32))) + (func $f (param i32) (result i32) (local.get 0)) + + (func (export "unreachable") (result i32) + (unreachable) + (i64.const 0) + (ref.func $f) + (return_call_ref $t) + ) + ) + "type mismatch" +) + +(assert_invalid + (module + (elem declare func $f) + (type $t (func (param i32) (result i32))) + (func $f (param i32) (result i32) (local.get 0)) + + (func (export "unreachable") (result i32) + (unreachable) + (ref.func $f) + (return_call_ref $t) + (i64.const 0) + ) + ) + "type mismatch" +) + +(assert_invalid + (module + (type $t (func)) + (func $f (param $r externref) + (return_call_ref $t (local.get $r)) + ) + ) + "type mismatch" +) |