diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index ca238d8cd90..ee8ff3dd701 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2404,6 +2404,7 @@ def write_commands(commands, filename): ("--const-hoisting",), ("--dae",), ("--dae-optimizing",), + ("--dae2",), ("--dce",), ("--directize",), ("--discard-global-effects",), diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index a763006ab66..dae36d87e89 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -28,6 +28,7 @@ set(passes_SOURCES ConstHoisting.cpp DataFlowOpts.cpp DeadArgumentElimination.cpp + DeadArgumentElimination2.cpp DeadCodeElimination.cpp DeAlign.cpp DebugLocationPropagation.cpp diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp new file mode 100644 index 00000000000..c7157b66d79 --- /dev/null +++ b/src/passes/DeadArgumentElimination2.cpp @@ -0,0 +1,1392 @@ +/* + * Copyright 2025 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Perform dead argument elimination based on a smallest fixed point analysis of +// used parameters. Traverse the module once to collect call graph information, +// used parameters, and "forwarded" parameters that are only used by being +// forwarded on to other function calls. These forwarded parameters can still be +// optimized out as long as they are unused in the callees they are forwarded +// to. Since we perform a fixed point analysis, cycles of forwarded parameters +// can still be removed. +// +// After finding used parameters, traverse the module once more to remove +// unused parameters and arguments. Finally, if we are able to optimize indirect +// calls and referenced functions, traverse the module one last time to globally +// update referenced function types. This may require first giving unreferenced +// functions replacement types to make sure they are not incorrectly updated by +// the global type rewriting. +// +// As a POC, only do the backward analysis to find unused parameters. To match +// and exceed the power of DAE, we will need to extend this backward analysis to +// find unused results as well, and also add a forward analysis that propagates +// constants and types through parameters and results. + +#include +#include +#include +#include + +#include "analysis/lattices/bool.h" +#include "ir/effects.h" +#include "ir/eh-utils.h" +#include "ir/intrinsics.h" +#include "ir/label-utils.h" +#include "ir/local-graph.h" +#include "ir/manipulation.h" +#include "ir/module-utils.h" +#include "ir/type-updating.h" +#include "pass.h" +#include "support/index.h" +#include "support/utilities.h" +#include "wasm-builder.h" +#include "wasm-traversal.h" +#include "wasm-type-shape.h" +#include "wasm-type.h" +#include "wasm.h" + +#define TIME_DAE 0 + +#if TIME_DAE + +#include + +#include "support/timing.h" + +#endif // TIME_DAE + +// TODO: Treat call_indirects more precisely than call_refs by taking the target +// table into account. + +// TODO: Analyze stack switching instructions to remove their unused parameters. + +namespace wasm { + +namespace { + +#if TIME_DAE +#define TIME(...) __VA_ARGS__ +#else +#define TIME(...) +#endif // TIME_DAE + +// Find the root of the subtyping hierarchy for a given HeapType. +HeapType getRootType(HeapType type) { + while (true) { + if (auto super = type.getDeclaredSuperType()) { + type = *super; + continue; + } + break; + } + return type; +} + +// Analysis lattice: top/true = used, bot/false = unused. +using Used = analysis::Bool; + +// Analysis results for each parameter of a function. +using Params = std::vector; + +// Function index and parameter index. +using ParamLoc = std::pair; + +// Function type and parameter index. +using TypeParamLoc = std::pair; + +// A set of (source, destination) index pairs for parameters of a caller +// function being forwarded as arguments to a callee function. +using ForwardedParamSet = std::unordered_set>; + +using TypeMap = GlobalTypeRewriter::TypeMap; + +// Analysis results and call graph information for a single function. +// This tracks how parameters are used within the function and how they +// are forwarded to other functions via direct and indirect calls. +struct FunctionInfo { + // Analysis results. For each parameter, whether it is used. + Params paramUsages; + + // Map callee function names to their forwarded params for direct calls. + std::unordered_map directForwardedParams; + + // Map the root supertypes of callee types to their forwarded params for + // indirect calls. + std::unordered_map indirectForwardedParams; + + // For each parameter of this function, the list of parameters in direct + // callers that will become used if the parameter in this function turns out + // to be used. Computed by reversing the directForwardedParams graph. + std::vector> callerParams; + + // The gets that may read from parameters. These are the gets that might be + // optimized out if their results are unused or forwarded to another function + // where they will be unused. + std::unordered_set paramGets; + + // We do not yet analyze parameter usage in stack switching instructions. + // Collect the used continuation types so we can be sure not to modify their + // associated function types. + // TODO: Analyze stack switching. + std::unordered_set contTypes; + + // Whether we need to additionally propagate param usage to indirect callers + // of this function's type. Atomic because it can be set when visiting other + // functions in parallel. + std::atomic referenced = false; + + // We cannot yet fully analyze and optimize call.without.effects, which would + // require creating new imports for new signatures, etc. Functions that are + // called via these intrinsics will not be optimized. + // TODO: Fix this. + std::atomic usedInIntrinsic = false; + + // Unreferenced functions can be optimized separately from referenced + // functions with the same type. For unreferenced functions in that situation, + // this is the new type that should be applied before global type rewriting to + // prevent the function from getting the wrong optimizations. + std::optional replacementType; +}; + +// Analysis results and call graph information for a tree of related function +// types. Every type in the tree must have matching used and unused parameters, +// so we can track information per-tree instead of per-type. +struct RootFuncTypeInfo { + // For each parameter in the type, whether it is used. + Params paramUsages; + + // The list of referenced functions with types in this tree. When a parameter + // in this type tree is used, the parameter becomes used in these functions + // and vice versa. + std::vector referencedFuncs; + + // For each parameter in this function type, the list of parameters of + // indirect callers that become used when the parameter in this function type + // becomes used. Computed by reversing indirectForwardedParams from the + // function infos. + std::vector> callerParams; + + RootFuncTypeInfo(Used& used, HeapType type) + : paramUsages(type.getSignature().params.size(), used.getBottom()), + callerParams(type.getSignature().params.size()) {} +}; + +struct DAE2 : public Pass { + // Analysis lattice. + Used used; + + Module* wasm = nullptr; + + // Map function name to index. + std::unordered_map funcIndices; + + // Intermediate and final analysis results by function index. + std::vector funcInfos; + + // Intermediate and final analysis results for each function type tree, keyed + // by root type in the tree. + std::unordered_map typeTreeInfos; + + RootFuncTypeInfo& getTypeTreeInfo(HeapType rootType) { + return typeTreeInfos.try_emplace(rootType, used, rootType).first->second; + } + + // In general referenced functions may escape and be called externally in an + // open world, so we require a closed world to optimize referenced functions. + // Further, without GC we cannot differentiate the types of unreferenced and + // referenced functions before global type rewriting, so we cannot optimize + // them separately. Do not constrain the optimization of unreferenced + // functions by optimizing referenced functions in that case. + // TODO: Find a way to optimize referenced functions without GC enabled as + // long as traps never happen so call_indirect cannot distinguish separate + // types. + bool optimizeReferencedFuncs = false; + + // Cache the public heap types to avoid gathering them more than once. + std::vector publicHeapTypes; + + void run(Module* wasm) override { + this->wasm = wasm; + for (auto& func : wasm->functions) { + funcIndices.insert({func->name, funcIndices.size()}); + } + + optimizeReferencedFuncs = + getPassOptions().closedWorld && wasm->features.hasGC(); + + TIME(Timer timer); + + analyzeModule(); + + TIME(std::cerr << "analysis: " << timer.lastElapsed() << "\n"); + + prepareReverseGraph(); + + TIME(std::cerr << "prepare: " << timer.lastElapsed() << "\n"); + + computeFixedPoint(); + + TIME(std::cerr << "fixed point: " << timer.lastElapsed() << "\n"); + + optimize(); + + TIME(auto [last, total] = timer.elapsed()); + TIME(std::cerr << "optimize: " << last << "\n"); + TIME(std::cerr << "total: " << total << "\n"); + } + + void analyzeModule(); + void prepareReverseGraph(); + void computeFixedPoint(); + void optimize(); + + void makeUnreferencedFunctionTypes(const std::vector& oldTypes, + const TypeMap& newTypes); + + void markParamsUsed(Index funcIndex) { + auto& usages = funcInfos[funcIndex].paramUsages; + std::fill(usages.begin(), usages.end(), used.getTop()); + } + + void markParamsUsed(Name func) { markParamsUsed(funcIndices.at(func)); } + + void markParamsUsed(HeapType rootType) { + auto& usages = getTypeTreeInfo(rootType).paramUsages; + std::fill(usages.begin(), usages.end(), used.getTop()); + } + + bool join(ParamLoc loc, const Used::Element& other) { + auto& elem = funcInfos[loc.first].paramUsages[loc.second]; + return used.join(elem, other); + } + + bool join(TypeParamLoc loc, const Used::Element& other) { + assert(loc.first == getRootType(loc.first)); + auto& elem = getTypeTreeInfo(loc.first).paramUsages[loc.second]; + return used.join(elem, other); + } +}; + +struct GraphBuilder : public WalkerPass> { + // Analysis lattice. + const Used& used; + + // The function info graph is stored as vectors accessed by function index. + // Map function names to their indices. + const std::unordered_map& funcIndices; + + // Vector of analysis info representing the analysis graph we are building. + // This is populated safely in parallel because the visitor for each function + // only modifies the entry for that function. + std::vector& funcInfos; + + // The index of the function we are currently walking. + Index index = -1; + + // A use of a parameter local does not necessarily imply the use of the + // parameter value. We use a local graph to check where parameter values may + // be used. + std::optional localGraph; + + bool optimizeReferencedFuncs; + + GraphBuilder(const Used& used, + const std::unordered_map& funcIndices, + std::vector& funcInfos, + bool optimizeReferencedFuncs) + : used(used), funcIndices(funcIndices), funcInfos(funcInfos), + optimizeReferencedFuncs(optimizeReferencedFuncs) {} + + bool isFunctionParallel() override { return true; } + bool modifiesBinaryenIR() override { return false; } + + std::unique_ptr create() override { + return std::make_unique( + used, funcIndices, funcInfos, optimizeReferencedFuncs); + } + + void runOnFunction(Module* wasm, Function* func) override { + assert(index == Index(-1)); + index = funcIndices.at(func->name); + localGraph.emplace(func); + WalkerPass>::runOnFunction(wasm, func); + } + + void visitRefFunc(RefFunc* curr) { + funcInfos[funcIndices.at(curr->func)].referenced = true; + } + + void noteContinuation(Type type) { + if (type.isContinuation()) { + funcInfos[index].contTypes.insert(type.getHeapType()); + } + } + + void visitResume(Resume* curr) { noteContinuation(curr->cont->type); } + void visitResumeThrow(ResumeThrow* curr) { + noteContinuation(curr->cont->type); + } + void visitStackSwitch(StackSwitch* curr) { + noteContinuation(curr->cont->type); + // Do not optimize the return continuation either because that would + // require us to update the type of the switch expression. + if (curr->cont->type.isContinuation()) { + auto retParams = curr->cont->type.getHeapType() + .getContinuation() + .type.getSignature() + .params; + noteContinuation(retParams[retParams.size() - 1]); + } + } + void visitContBind(ContBind* curr) { + noteContinuation(curr->cont->type); + noteContinuation(curr->type); + } + + void visitCall(Call* curr) { + if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) { + auto target = curr->operands.back()->cast()->func; + funcInfos[funcIndices.at(target)].usedInIntrinsic = true; + } + } + + Index getArgIndex(const ExpressionList& operands, Expression* arg) { + for (Index i = 0; i < operands.size(); ++i) { + if (operands[i] == arg) { + return i; + } + } + WASM_UNREACHABLE("expected arg"); + } + + void handleDirectForwardedParam(LocalGet* get, Expression* arg, Call* call) { + auto argIndex = getArgIndex(call->operands, arg); + auto& forwarded = funcInfos[index].directForwardedParams[call->target]; + forwarded.insert({get->index, argIndex}); + } + + void handleIndirectForwardedParam(LocalGet* get, + Expression* arg, + const ExpressionList& operands, + HeapType type) { + auto rootType = getRootType(type); + auto argIndex = getArgIndex(operands, arg); + auto& forwarded = funcInfos[index].indirectForwardedParams[rootType]; + forwarded.insert({get->index, argIndex}); + } + + void visitLocalGet(LocalGet* curr) { + if (curr->index >= getFunction()->getNumParams()) { + // Not a parameter. + return; + } + + // A use of a parameter local does not necessarily imply the use of the + // parameter value. Check where the parameter value may be used. + const auto& sets = localGraph->getSets(curr); + bool usesParam = std::any_of( + sets.begin(), sets.end(), [](LocalSet* set) { return set == nullptr; }); + + if (!usesParam) { + // The original parameter value does not reach here. + return; + } + + funcInfos[index].paramGets.insert(curr); + + // Look at the transitive users of this value (i.e. its parent and further + // ancestors) to see if it is used by a call. If it is, we say that the + // caller parameter is "forwarded" to the callee. We will create an edge in + // the analysis graph so that if the callee uses its parameter, we will mark + // the forwarded parameter used in the current function as well. We must + // make sure the current function doesn't first use the parameter via this + // local.get in other ways, though, for example by teeing it to another + // local or by performing a branching or trapping cast on it. As a + // conservative approximation, consider the parameter used if any of the + // expressions between the local.get and a function call have non-removable + // side effects (even if those side effects do not depend on the value + // flowing from the get). + for (Index i = expressionStack.size() - 1; i > 0; --i) { + auto* expr = expressionStack[i]; + auto* parent = expressionStack[i - 1]; + + if (auto* call = parent->dynCast()) { + handleDirectForwardedParam(curr, expr, call); + return; + } + if (auto* call = parent->dynCast(); + call && expr != call->target && optimizeReferencedFuncs) { + handleIndirectForwardedParam( + curr, expr, call->operands, call->heapType); + return; + } + if (auto* call = parent->dynCast(); + call && expr != call->target && optimizeReferencedFuncs) { + if (!call->target->type.isSignature()) { + // The call will never happen, so we don't need to consider it. + return; + } + auto heapType = call->target->type.getHeapType(); + handleIndirectForwardedParam(curr, expr, call->operands, heapType); + return; + } + + // If the parameter flows into an If condition, we must consider it used + // because removing it may visibly change which arm of the If gets + // executed. This is not captured by the effects analysis below. + if (auto* iff = parent->dynCast(); iff && expr == iff->condition) { + break; + } + + // If the current parent expression has unremovable side effects, we + // conservatively treat the parameter as used. + EffectAnalyzer effects(getPassOptions(), *getModule()); + effects.visit(parent); + if (effects.hasUnremovableSideEffects()) { + // Conservatively assume this expression uses the parameter value + // in some way that prevents us from removing it. + break; + } + + if (!parent->type.isConcrete()) { + // The value flows no further, so it is not used in an observable way. + return; + } + // TODO: Once we analyze return values, consider the case where this + // parameter is used only if the return value of this function is used. + } + // The parameter value is used by something other than a call. + funcInfos[index].paramUsages[curr->index] = used.getTop(); + } +}; + +void DAE2::analyzeModule() { + // Initialize the function infos. (The type infos are initialized + // on-demand instead.) + funcInfos = std::vector(wasm->functions.size()); + for (Index i = 0; i < funcInfos.size(); ++i) { + auto numParams = wasm->functions[i]->getNumParams(); + funcInfos[i].paramUsages.resize(numParams, used.getBottom()); + funcInfos[i].callerParams.resize(numParams); + } + + // Analyze functions to find forwarded and used parameters as well as + // function references and other relevant information. + GraphBuilder builder(used, funcIndices, funcInfos, optimizeReferencedFuncs); + builder.run(getPassRunner(), wasm); + + // Find additional function references at the module level. + builder.walkModuleCode(wasm); + + // Model imported and exported functions as referenced so that marking the + // parameters of their types as used will prevent optimizations of the + // functions themselves. + for (Index i = 0; i < wasm->functions.size(); ++i) { + if (wasm->functions[i]->imported()) { + funcInfos[i].referenced = true; + } + } + for (auto& export_ : wasm->exports) { + if (export_->kind == ExternalKind::Function) { + auto i = funcIndices.at(*export_->getInternalName()); + funcInfos[i].referenced = true; + } + } + + // Functions called with call.without.effects cannot yet be optimized. Mark + // their parameters as used. + for (Index i = 0; i < wasm->functions.size(); ++i) { + if (funcInfos[i].usedInIntrinsic) { + markParamsUsed(i); + } + } + + // Functions passed to configureAll will be called externally. Mark their + // parameters as used. configureAll is only available when custom + // descriptors is enabled. + if (wasm->features.hasCustomDescriptors()) { + for (auto name : Intrinsics(*wasm).getConfigureAllFunctions()) { + markParamsUsed(name); + } + } + + // If we're not optimizing referenced functions, mark all their parameters as + // used. + if (!optimizeReferencedFuncs) { + for (Index i = 0; i < wasm->functions.size(); ++i) { + if (funcInfos[i].referenced) { + markParamsUsed(i); + } + } + } + + // Additionally mark parameters of referenced functions with public types (or + // private subtypes of public types) as used because we cannot rewrite their + // types. Similarly, we do not rewrite tag types or function types used in + // continuations, so any referenced function whose type is in the same tree as + // a tag type or continuation function type will have its parameters marked as + // used. + // + // TODO: Consider analyzing whether we can rewrite the types of such + // referenced functions to new private types first. This would require + // analyzing whether they can escape the module. + // + // TODO: Analyze tags and remove their unused parameters. + std::unordered_set unrewritableRoots; + publicHeapTypes = ModuleUtils::getPublicHeapTypes(*wasm); + for (auto type : publicHeapTypes) { + if (type.isSignature()) { + unrewritableRoots.insert(getRootType(type)); + } + } + for (auto& tag : wasm->tags) { + unrewritableRoots.insert(getRootType(tag->type)); + } + for (Index i = 0; i < wasm->functions.size(); ++i) { + for (auto type : funcInfos[i].contTypes) { + unrewritableRoots.insert(getRootType(type.getContinuation().type)); + } + } + + // The types of the call.without.effects imports are excluded from the set of + // public heap types, but until we can handle analyzing and updating them in + // this pass, we must treat them the same as any other imported function + // types. + for (auto& func : wasm->functions) { + if (Intrinsics(*wasm).isCallWithoutEffects(func.get())) { + unrewritableRoots.insert(getRootType(func->type.getHeapType())); + } + } + + for (auto root : unrewritableRoots) { + markParamsUsed(root); + } +} + +void DAE2::prepareReverseGraph() { + // Compute the reverse graph used by the fixed point analysis from the + // forward graph we have built. + for (Index i = 0; i < funcInfos.size(); ++i) { + funcInfos[i].callerParams.resize(funcInfos[i].paramUsages.size()); + if (funcInfos[i].referenced) { + auto root = getRootType(wasm->functions[i]->type.getHeapType()); + getTypeTreeInfo(root).referencedFuncs.push_back(i); + } + } + for (Index callerIndex = 0; callerIndex < funcInfos.size(); ++callerIndex) { + for (auto& [callee, forwarded] : + funcInfos[callerIndex].directForwardedParams) { + auto& callerParams = funcInfos[funcIndices.at(callee)].callerParams; + for (auto& [srcParam, destParam] : forwarded) { + assert(destParam < callerParams.size()); + callerParams[destParam].push_back({callerIndex, srcParam}); + } + } + for (auto& [calleeRootType, forwarded] : + funcInfos[callerIndex].indirectForwardedParams) { + assert(getRootType(calleeRootType) == calleeRootType); + auto& callerParams = getTypeTreeInfo(calleeRootType).callerParams; + for (auto& [srcParam, destParam] : forwarded) { + assert(destParam < callerParams.size()); + callerParams[destParam].push_back({callerIndex, srcParam}); + } + } + } +} + +// Performs a smallest fixed-point analysis to propagate parameter usage +// information through the reverse call graph. If a parameter is used in a +// function, then any caller parameters that were forwarded to the parameter are +// also used. Cycles of forwarded arguments will not be marked used unless +// one of the arguments starts out as used or there is some source of usage +// outside the cycle. +void DAE2::computeFixedPoint() { + using Item = std::variant; + + // List of params, either of functions or root function types, from which we + // may need to propagate usage information. Initialized with all params we + // have observed to be used in the IR. + std::vector work; + for (Index i = 0; i < funcInfos.size(); ++i) { + for (Index j = 0; j < funcInfos[i].paramUsages.size(); ++j) { + if (funcInfos[i].paramUsages[j]) { + work.push_back(ParamLoc{i, j}); + } + } + } + for (auto& [rootType, info] : typeTreeInfos) { + for (Index i = 0; i < info.paramUsages.size(); ++i) { + if (info.paramUsages[i]) { + work.push_back(TypeParamLoc{rootType, i}); + } + } + } + while (!work.empty()) { + auto item = work.back(); + work.pop_back(); + + if (auto* loc = std::get_if(&item)) { + auto [rootType, calleeParamIndex] = *loc; + auto& typeTreeInfo = getTypeTreeInfo(rootType); + const auto& elem = typeTreeInfo.paramUsages[calleeParamIndex]; + assert(elem && "unexpected unused param"); + + // Propagate usage back to forwarded parameters of indirect callers. + for (auto param : typeTreeInfo.callerParams[calleeParamIndex]) { + if (join(param, elem)) { + work.push_back(param); + } + } + // Propagate usage to referenced functions with types in the same type + // tree to ensure their types can all be updated uniformly. + for (auto funcIndex : typeTreeInfo.referencedFuncs) { + ParamLoc param = {funcIndex, calleeParamIndex}; + if (join(param, elem)) { + work.push_back(param); + } + } + continue; + } + + if (auto* loc = std::get_if(&item)) { + auto [calleeIndex, calleeParamIndex] = *loc; + auto& calleeInfo = funcInfos[calleeIndex]; + const auto& elem = calleeInfo.paramUsages[calleeParamIndex]; + assert(elem && "unexpected unused param"); + + // Propagate usage back to forwarded params of direct callers. + for (auto param : calleeInfo.callerParams[calleeParamIndex]) { + if (join(param, elem)) { + work.push_back(param); + } + } + + if (calleeInfo.referenced) { + // Propagate the use to the function type. It will be propagated from + // there to indirect callers of this type. + auto calleeType = wasm->functions[calleeIndex]->type.getHeapType(); + TypeParamLoc param = {getRootType(calleeType), calleeParamIndex}; + if (join(param, elem)) { + work.push_back(param); + } + } + continue; + } + WASM_UNREACHABLE("unexpected item"); + } +} + +// Updates function signatures throughout the module. Ensures that all functions +// within the same subtyping tree have the same parameters removed, maintaining +// the validity of the subtyping hierarchy. +struct DAETypeUpdater : GlobalTypeRewriter { + DAE2& parent; + DAETypeUpdater(DAE2& parent) + : GlobalTypeRewriter(*parent.wasm), parent(parent) {} + + void modifySignature(HeapType oldType, Signature& sig) override { + // All signature types in a type tree will have the same parameters removed + // to keep subtyping valid. Look up which parameters to keep by the root + // type in the tree. + auto& usages = parent.getTypeTreeInfo(getRootType(oldType)).paramUsages; + bool hasRemoved = std::any_of( + usages.begin(), usages.end(), [&](auto& use) { return !use; }); + if (hasRemoved) { + std::vector keptParams; + keptParams.reserve(usages.size()); + for (Index i = 0; i < usages.size(); ++i) { + if (usages[i]) { + keptParams.push_back(sig.params[i]); + } + } + sig.params = getTempTupleType(std::move(keptParams)); + } + } + + // Return the sorted list of old types (used for deterministic ordering) and + // the unordered map from old to new types. + std::pair, TypeMap> rebuildTypes() { + auto types = getSortedTypes(getPrivatePredecessors()); + auto map = GlobalTypeRewriter::rebuildTypes(types); + return {std::move(types), std::move(map)}; + } +}; + +struct Optimizer + : public WalkerPass< + ExpressionStackWalker>> { + using Super = WalkerPass< + ExpressionStackWalker>>; + + const DAE2& parent; + + // The info for the function we are running on. + const FunctionInfo* funcInfo = nullptr; + + // Map old local indices to new local indices for the function we are + // currently optimizing. + std::vector newIndices; + + // It is not enough to simply replace removed parameters with locals. Removed + // non-nullable parameters would become non-nullable locals, and those locals + // might have gets that are dropped or forwarded to other optimized calls + // before any set is executed. We cannot in general synthesize a value to + // initialize such locals (nor would we want to), so instead we must make the + // dropped or forwarded gets disappear, as well as their parents up to the + // drop or forwarding call. These are the expressions we must remove. + std::unordered_set removedExpressions; + + // We will need to generate fresh labels for trampoline blocks. + std::optional labels; + + Optimizer(const DAE2& parent) : parent(parent) {} + + bool isFunctionParallel() override { return true; } + + // We handle non-nullable local fixups in the pass itself. If we ran the + // fixups after the pass, they could get confused and produce invalid code + // because this pass updates local indices but does not always update function + // types to match. Function types are updated after this pass runs. + bool requiresNonNullableLocalFixups() override { return false; } + + std::unique_ptr create() override { + return std::make_unique(parent); + } + + // Update the locals and local indices within the function. If the function is + // not referenced, also update its type. (Referenced functions will have their + // types updated later in a global type rewriting operation.) + void runOnFunction(Module* wasm, Function* func) override { + if (func->imported()) { + return; + } + + funcInfo = &parent.funcInfos[parent.funcIndices.at(func->name)]; + labels.emplace(func); + + auto originalType = func->type; + auto originalParams = func->getParams(); + auto numParams = originalParams.size(); + Index numLocals = func->getNumLocals(); + + assert(newIndices.empty()); + newIndices.reserve(numLocals); + + auto& paramUsages = funcInfo->paramUsages; + assert(paramUsages.size() == numParams); + bool hasRemoved = std::any_of( + paramUsages.begin(), paramUsages.end(), [&](auto& use) { return !use; }); + + bool hasNewNonNullableLocal = false; + if (hasRemoved) { + // Remove the parameters and replace them with scratch locals. + std::vector> removedParams; + std::vector keptParams; + std::unordered_map newLocalNames; + std::unordered_map newLocalIndices; + Index next = 0; + + for (Index i = 0; i < numLocals; ++i) { + if (i >= numParams || paramUsages[i]) { + // Used param or local. Keep it. + if (i < numParams) { + keptParams.push_back(originalParams[i]); + } + newIndices.push_back(next); + if (auto name = func->getLocalNameOrDefault(i)) { + newLocalNames[next] = name; + newLocalIndices[name] = next; + } + ++next; + } else { + // Skip this unused param and record it to be added as a new local + // later. + removedParams.emplace_back(originalParams[i], + func->getLocalNameOrDefault(i)); + newIndices.push_back(numLocals - removedParams.size()); + } + } + + func->localNames = std::move(newLocalNames); + func->localIndices = std::move(newLocalIndices); + + // Replace the function's type with a new type using only the kept + // parameters. This ensures that newly added locals get the right indices. + // Preserve sharedness in case this becomes the permanent new type (when + // GC is disabled, see below). + Signature sig(Type(keptParams), func->getResults()); + TypeBuilder builder(1); + builder[0] = sig; + builder[0].setShared(originalType.getHeapType().getShared()); + auto newType = (*builder.build())[0]; + func->type = Type(newType, NonNullable, Exact); + + // Add new vars to replace the removed params. + for (Index i = removedParams.size(); i > 0; --i) { + auto& [type, name] = removedParams[i - 1]; + if (type.isNonNullable()) { + hasNewNonNullableLocal = true; + } + Builder::addVar(func, name, type); + } + } else { + // Not changing any indices. + for (Index i = 0; i < numLocals; ++i) { + newIndices.push_back(i); + } + } + + Super::runOnFunction(wasm, func); + + // We may have moved pops around. Fix them. + EHUtils::handleBlockNestedPops(func, *wasm); + + // We may have introduced a new non-nullable local whose sets do not + // structurally dominate its gets. Fix it. + if (hasNewNonNullableLocal) { + TypeUpdating::handleNonDefaultableLocals(func, *wasm); + } + + // If there is a replacement type, install it now. Otherwise we know the + // global type rewriting will do the right thing with the original type, + // so restore it. Only do this if we are optimizing indirect calls because + // otherwise there will not be a global type rewriting step. + if (funcInfo->replacementType) { + func->type = Type(*funcInfo->replacementType, NonNullable, Exact); + } else if (parent.optimizeReferencedFuncs) { + func->type = originalType; + } + } + + void visitLocalSet(LocalSet* curr) { curr->index = newIndices[curr->index]; } + + void visitLocalGet(LocalGet* curr) { + // If this is a get of a removed parameter, we need to make it disappear + // along with all of its parents until we reach a call or the value is no + // longer propagated. We know removing these expressions is ok because if + // any of them had non-removable side effects, we would not be optimizing + // out the parameter. + if (curr->index < funcInfo->paramUsages.size() && + !funcInfo->paramUsages[curr->index] && + funcInfo->paramGets.count(curr)) { + for (Index i = expressionStack.size(); i > 0; --i) { + auto* expr = expressionStack[i - 1]; + if (expr->is() || expr->is() || + expr->is()) { + break; + } + if (!removedExpressions.insert(expr).second) { + // This expression, and therefore its relevant parents, have already + // been marked, so we do not need to continue. + break; + }; + if (!expr->type.isConcrete()) { + break; + } + } + } + curr->index = newIndices[curr->index]; + } + + // Given an expression, return a new expression containing all its non-removed + // parts, if any, or otherwise nullptr. The returned expression will have + // non-concrete type because its value will have been removed. + Expression* getReplacement(Expression* curr); + + void removeOperands(Expression* curr, + ExpressionList& operands, + const Params& usages) { + if (operands.empty()) { + return; + } + assert(usages.empty() || usages.size() == operands.size()); + bool hasRemoved = usages.empty() || + std::any_of(usages.begin(), usages.end(), [](auto& elem) { + return !elem; + }); + if (!hasRemoved) { + return; + } + + // In general we cannot change the order of the operands, including the kept + // parts of removed operands, because they may have side effects. Use + // scratch locals to move the kept operand values past any subsequent kept + // parts of removed operands. + Builder builder(*getModule()); + Expression* block = nullptr; + Index next = 0; + bool hasUnreachableRemovedOperand = false; + for (Index i = 0; i < operands.size(); ++i) { + auto type = operands[i]->type; + if (!usages.empty() && usages[i]) { + if (type == Type::unreachable) { + // No scratch local necessary to propagate unreachable. + block = builder.blockify(block, operands[i]); + operands[next++] = builder.makeUnreachable(); + continue; + } + Index scratch = Builder::addVar(getFunction(), type); + block = + builder.blockify(block, builder.makeLocalSet(scratch, operands[i])); + operands[next++] = builder.makeLocalGet(scratch, type); + continue; + } + // This operand is removed, but it might have children we need to keep. + if (type == Type::unreachable) { + hasUnreachableRemovedOperand = true; + } + if (auto* replacement = getReplacement(operands[i])) { + block = builder.blockify(block, replacement); + } + } + operands.resize(next); + // If we removed an unreachable operand, then the call is definitely + // unreachable but may no longer have an unreachable child. This is not + // valid, so replace it with an unreachable. + if (hasUnreachableRemovedOperand) { + block = builder.blockify(block, builder.makeUnreachable()); + } else { + block = builder.blockify(block, curr); + } + replaceCurrent(block); + } + + void visitCall(Call* curr) { + removeOperands( + curr, + curr->operands, + parent.funcInfos[parent.funcIndices.at(curr->target)].paramUsages); + } + + void handleIndirectCall(Expression* curr, + ExpressionList& operands, + HeapType type) { + if (!parent.optimizeReferencedFuncs) { + return; + } + auto it = parent.typeTreeInfos.find(getRootType(type)); + if (it == parent.typeTreeInfos.end()) { + // No analysis results for this type, so none of its parameters are used. + removeOperands(curr, operands, {}); + } else { + removeOperands(curr, operands, it->second.paramUsages); + } + } + + void visitCallRef(CallRef* curr) { + if (!curr->target->type.isSignature()) { + return; + } + handleIndirectCall(curr, curr->operands, curr->target->type.getHeapType()); + } + + void visitCallIndirect(CallIndirect* curr) { + handleIndirectCall(curr, curr->operands, curr->heapType); + } + + void visitExpression(Expression* curr) { + if (curr->type.isConcrete()) { + // If this expression should be removed, that will be handled by the call + // or non-concrete expression it ultimately flows into. + return; + } + if (!removedExpressions.count(curr)) { + // We're keeping this one. + return; + } + // This is a top-level removed expression. Replace it with its kept + // children, if any, and make sure unreachable expressions remain + // unreachable. + Builder builder(*getModule()); + if (auto* replacement = getReplacement(curr)) { + if (curr->type == Type::unreachable && + replacement->type != Type::unreachable) { + replacement = builder.blockify(replacement, builder.makeUnreachable()); + } + replaceCurrent(replacement); + return; + } + // There are no kept children. + if (curr->type == Type::unreachable) { + ExpressionManipulator::unreachable(curr); + } else { + ExpressionManipulator::nop(curr); + } + } +}; + +Expression* Optimizer::getReplacement(Expression* curr) { + Builder builder(*getModule()); + if (!removedExpressions.count(curr)) { + // This expression is not removed, so none of its children are either. (If + // they were, they would already have been removed.) + if (!curr->type.isConcrete()) { + return curr; + } + return builder.makeDrop(curr); + } + + // Traverse an expression that is being removed and collect any of its + // sub-expressions that are not being removed into a new expression. + struct Collector + : ExpressionStackWalker> { + using Super = + ExpressionStackWalker>; + + std::unordered_set& removedExpressions; + LabelUtils::LabelManager& labels; + Builder& builder; + + // A stack of expressions we are building, one for each level of control + // flow structures we are inserting them into. + std::vector collectedStack = {nullptr}; + + // Indices indicating which part of a Try we are currently constructing, + // for every Try in the current expression stack. Index 0 is the body and + // index N > 0 is catch body N - 1. + std::vector tryIndexStack; + + Collector(std::unordered_set& removedExpressions, + LabelUtils::LabelManager& labels, + Builder& builder) + : removedExpressions(removedExpressions), labels(labels), + builder(builder) {} + + void appendExpr(Expression* curr) { + if (curr->type.isConcrete()) { + curr = builder.makeDrop(curr); + } + if (collectedStack.back()) { + collectedStack.back() = builder.blockify(collectedStack.back(), curr); + } else { + collectedStack.back() = curr; + } + } + + void finishControlFlow(Expression* curr) { + collectedStack.pop_back(); + appendExpr(curr); + // Do not traverse this expression again, since it need not change after + // having been processed once. + removedExpressions.erase(curr); + } + + // Ifs, Loops, Trys, and labeled Blocks can all produce values and may + // shallowly have only removable side effects, so it is possible that they + // will need to be removed. However, they may contain expressions that + // have non-removable side effects. The execution of these side effects + // must remain under the control of the original control flow structures. + // This custom scan calls hooks that will incorporate the control flow + // structures into the collected expression. Once a control flow structure + // has been fully processed once, remove it from the set of removed + // expressions because its remaining contents are not removed and would + // not change if processed again. + static void scan(Collector* self, Expression** currp) { + Expression* curr = *currp; + + if (!self->removedExpressions.count(curr)) { + // The expressions we are removing form a sub-tree starting at the + // root expression. There is therefore never a removed expression + // inside a non-removed expression. We can just collect this + // non-removed expression without scanning it. + self->appendExpr(curr); + return; + } + + if (auto* iff = curr->dynCast()) { + assert(iff->ifFalse && "unexpected one-arm if"); + self->pushTask(doPostVisit, currp); + self->pushTask(doEndIfFalse, currp); + self->pushTask(scan, &iff->ifFalse); + self->pushTask(doEndIfTrue, currp); + self->pushTask(scan, &iff->ifTrue); + // If conditions are always kept, so we do not need to scan them. + self->pushTask(doPreVisit, currp); + self->collectedStack.push_back(nullptr); + } else if (auto* loop = curr->dynCast()) { + self->pushTask(doPostVisit, currp); + self->pushTask(doEndLoop, currp); + self->pushTask(scan, &loop->body); + self->pushTask(doPreVisit, currp); + self->collectedStack.push_back(nullptr); + } else if (auto* try_ = curr->dynCast()) { + self->pushTask(doPostVisit, currp); + for (Index i = try_->catchBodies.size(); i > 0; --i) { + self->pushTask(doEndTryBody, currp); + self->pushTask(scan, &try_->catchBodies[i - 1]); + } + self->pushTask(doEndTryBody, currp); + self->pushTask(scan, &try_->body); + self->pushTask(doPreVisit, currp); + self->tryIndexStack.push_back(0); + self->collectedStack.push_back(nullptr); + } else if (auto* block = curr->dynCast(); block && block->name) { + self->pushTask(doPostVisit, currp); + self->pushTask(doEndLabeledBlock, currp); + for (Index i = block->list.size(); i > 0; --i) { + self->pushTask(scan, &block->list[i - 1]); + } + self->pushTask(doPreVisit, currp); + self->collectedStack.push_back(nullptr); + } else { + Super::scan(self, currp); + } + } + + static void doEndIfFalse(Collector* self, Expression** currp) { + If* curr = (*currp)->cast(); + // If this is null, we will just have erased the empty ifFalse arm, + // which is fine. + curr->ifFalse = self->collectedStack.back(); + curr->finalize(); + self->finishControlFlow(curr); + } + + static void doEndIfTrue(Collector* self, Expression** currp) { + If* curr = (*currp)->cast(); + // There must be an ifFalse arm because the if must have concrete type + // and we only process each removed control flow structure once. + assert(curr->ifFalse); + curr->ifTrue = self->collectedStack.back() ? self->collectedStack.back() + : self->builder.makeNop(); + self->collectedStack.back() = nullptr; + } + + static void doEndLoop(Collector* self, Expression** currp) { + Loop* curr = (*currp)->cast(); + curr->body = self->collectedStack.back() ? self->collectedStack.back() + : self->builder.makeNop(); + curr->finalize(); + self->finishControlFlow(curr); + } + + static void doEndTryBody(Collector* self, Expression** currp) { + Try* curr = (*currp)->cast(); + Index index = self->tryIndexStack.back()++; + auto* collected = self->collectedStack.back() + ? self->collectedStack.back() + : self->builder.makeNop(); + if (index == 0) { + curr->body = collected; + } else { + curr->catchBodies[index - 1] = collected; + } + self->collectedStack.back() = nullptr; + if (index == curr->catchBodies.size()) { + self->tryIndexStack.pop_back(); + curr->finalize(); + self->finishControlFlow(curr); + } + } + + static void doEndLabeledBlock(Collector* self, Expression** currp) { + Block* curr = (*currp)->cast(); + assert(curr->name); + assert(curr->type.isConcrete()); + + if (!self->collectedStack.back()) { + // This is the happy case where there cannot possibly be branches to + // the block because we have not collected any expressions at all. + // Since an empty block isn't useful, we don't need to collect + // anything new. + self->collectedStack.pop_back(); + return; + } + + // The kept expressions might have arbitrary value-carrying branches to + // this block, but we are trying to remove the block's value. In general + // we cannot remove the values from the branches, so we must instead add + // a trampoline that will let us drop the branch values: + // + // (block $fresh ;; A new outer block that returns nothing. + // (drop ;; Drop the branch values. + // (block $l (result t) ;; An inner block for the branches to target. + // ... ;; The kept expressions, if any. + // (br $fresh) ;; If no branches are taken, skip the drop. + // ) + // ) + // ) + Name fresh = self->labels.getUnique("trampoline"); + auto* inner = self->builder.makeSequence(self->collectedStack.back(), + self->builder.makeBreak(fresh)); + inner->name = curr->name; + inner->type = curr->type; + auto* drop = self->builder.makeDrop(inner); + self->finishControlFlow(self->builder.makeBlock(fresh, drop)); + } + }; + + Collector collector(removedExpressions, *labels, builder); + collector.walk(curr); + assert(collector.collectedStack.size() == 1); + return collector.collectedStack.back(); +} + +void DAE2::optimize() { + Optimizer optimizer(*this); + if (!optimizeReferencedFuncs) { + // Cannot globally rewrite types or optimize referenced functions, so just + // run the optimizer to optimize unreferenced functions. + optimizer.run(getPassRunner(), wasm); + return; + } + + TIME(Timer timer); + + // Global type rewriting + DAETypeUpdater typeUpdater(*this); + auto [oldTypes, newTypes] = typeUpdater.rebuildTypes(); + + TIME(std::cerr << " rebuild types: " << timer.lastElapsed() << "\n"); + + makeUnreferencedFunctionTypes(oldTypes, newTypes); + + TIME(std::cerr << " make replacements: " << timer.lastElapsed() << "\n"); + + optimizer.run(getPassRunner(), wasm); + + TIME(std::cerr << " optimize: " << timer.lastElapsed() << "\n"); + + // Update the types for referenced functions and all the locations that might + // hold them. The types of non-referenced functions have already been updated + // separately. + typeUpdater.mapTypes(newTypes); + + TIME(std::cerr << " update types: " << timer.lastElapsed() << "\n"); +} + +// Generate new (possibly optimized) types for each unreferenced function so +// the later global type update will leave them with the desired optimized +// signature. This may involve giving them a different function type that will +// be rewritten to the desired signature, or alternatively we might give the +// function the optimized signature directly and ensure the global type update +// will not change it further. +void DAE2::makeUnreferencedFunctionTypes(const std::vector& oldTypes, + const TypeMap& newTypes) { + + auto updateSingleType = [&](Type type) -> Type { + if (!type.isRef()) { + return type; + } + if (auto it = newTypes.find(type.getHeapType()); it != newTypes.end()) { + return type.with(it->second); + } + return type; + }; + + auto updateType = [&](Type type) -> Type { + if (type.isTuple()) { + std::vector elems; + elems.reserve(type.getTuple().size()); + for (auto t : type.getTuple()) { + elems.push_back(updateSingleType(t)); + } + return Type(std::move(elems)); + } + return updateSingleType(type); + }; + + auto updateSignature = [&](Signature sig) -> Signature { + sig.params = updateType(sig.params); + sig.results = updateType(sig.results); + return sig; + }; + + // Map possibly-optimized signatures with updated types to the pre-rewrite + // heap type that will be rewritten to have those signatures. These are the + // types we will give the unreferenced functions to make sure they end up with + // the correct types after type rewriting. + std::unordered_map replacementTypes; + + // Public types will never be rewritten, so we can use them as replacement + // types when they already have the desired signatures. + for (auto type : publicHeapTypes) { + if (type.isSignature()) { + replacementTypes.insert({type.getSignature(), type}); + } + } + + // Other types may be rewritten. We can use them as our replacement types if + // they will be rewritten to have the desired signatures. Do not iterate on + // newTypes directly because it is unordered and we need determinism. + for (auto& oldType : oldTypes) { + if (oldType.isSignature()) { + if (auto it = newTypes.find(oldType); it != newTypes.end()) { + replacementTypes.insert({it->second.getSignature(), oldType}); + } + } + } + + // If we can't reuse a type that will be rewritten to the desired signature, + // we need to create a new type that already has the desired signature and + // will not be rewritten. Make sure these new types are distinct from any + // types that will be globally rewritten. + UniqueRecGroups unique(wasm->features); + for (auto& [oldType, newType] : newTypes) { + if (!oldType.isSignature()) { + continue; + } + // New types we generate will always be the first types in their rec groups, + // so we only need to go out of our way to ensure that the new types are + // separate from other function types that are also the first in their rec + // groups. + if (oldType.getRecGroupIndex() != 0) { + continue; + } + if (oldType.getSignature() != newType.getSignature()) { + unique.insertOrGet(oldType.getRecGroup()); + } + } + + // Assign replacement types to unreferenced functions that need them. + for (Index i = 0; i < wasm->functions.size(); ++i) { + auto& func = wasm->functions[i]; + if (funcInfos[i].referenced) { + continue; + } + + auto params = func->getParams(); + std::vector keptParams; + keptParams.reserve(params.size()); + for (Index j = 0; j < params.size(); ++j) { + if (funcInfos[i].paramUsages[j]) { + keptParams.push_back(params[j]); + } + } + + auto newSig = + updateSignature({Type(std::move(keptParams)), func->getResults()}); + + // Look up or create a replacement type that will be rewritten to the + // desired signature. + auto [it, inserted] = replacementTypes.insert({newSig, HeapType(0)}); + if (inserted) { + // Create a new type with the desired signature that will not be rewritten + // to some other signature. + // TODO: We need to match sharedness here. + it->second = unique.insert(HeapType(newSig).getRecGroup())[0]; + } + + // The global type rewriting will not make the desired update, so we need a + // replacement type. + funcInfos[i].replacementType = it->second; + } +} + +} // anonymous namespace + +Pass* createDAE2Pass() { return new DAE2(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 23667aeecbf..539e2f7dc4a 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -105,6 +105,7 @@ void PassRegistry::registerPasses() { "removes arguments to calls in an lto-like manner, and " "optimizes where we removed", createDAEOptimizingPass); + registerPass("dae2", "Experimental reimplementation of DAE", createDAE2Pass); registerPass("abstract-type-refining", "refine and merge abstract (never-created) types", createAbstractTypeRefiningPass); diff --git a/src/passes/passes.h b/src/passes/passes.h index 6481e062c6d..a2e45747518 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -35,6 +35,7 @@ Pass* createConstantFieldPropagationPass(); Pass* createConstantFieldPropagationRefTestPass(); Pass* createDAEPass(); Pass* createDAEOptimizingPass(); +Pass* createDAE2Pass(); Pass* createDataFlowOptsPass(); Pass* createDeadCodeEliminationPass(); Pass* createDeInstrumentBranchHintsPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 41a285e9afb..4bdb83a9989 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -116,6 +116,9 @@ ;; CHECK-NEXT: lto-like manner, and optimizes ;; CHECK-NEXT: where we removed ;; CHECK-NEXT: +;; CHECK-NEXT: --dae2 Experimental reimplementation of +;; CHECK-NEXT: DAE +;; CHECK-NEXT: ;; CHECK-NEXT: --dce removes unreachable code ;; CHECK-NEXT: ;; CHECK-NEXT: --dealign forces all loads and stores to diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 5c08240c37a..7b9e139d16e 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -148,6 +148,9 @@ ;; CHECK-NEXT: lto-like manner, and optimizes ;; CHECK-NEXT: where we removed ;; CHECK-NEXT: +;; CHECK-NEXT: --dae2 Experimental reimplementation of +;; CHECK-NEXT: DAE +;; CHECK-NEXT: ;; CHECK-NEXT: --dce removes unreachable code ;; CHECK-NEXT: ;; CHECK-NEXT: --dealign forces all loads and stores to diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index a7997e36f12..6ad60d27a7a 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -80,6 +80,9 @@ ;; CHECK-NEXT: lto-like manner, and optimizes ;; CHECK-NEXT: where we removed ;; CHECK-NEXT: +;; CHECK-NEXT: --dae2 Experimental reimplementation of +;; CHECK-NEXT: DAE +;; CHECK-NEXT: ;; CHECK-NEXT: --dce removes unreachable code ;; CHECK-NEXT: ;; CHECK-NEXT: --dealign forces all loads and stores to diff --git a/test/lit/passes/dae2-configure-all.wast b/test/lit/passes/dae2-configure-all.wast new file mode 100644 index 00000000000..b5536a5117c --- /dev/null +++ b/test/lit/passes/dae2-configure-all.wast @@ -0,0 +1,139 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-opt %s -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (type $externs (array (mut externref))) + (type $externs (array (mut externref))) + ;; CHECK: (type $funcs (array (mut funcref))) + (type $funcs (array (mut funcref))) + ;; CHECK: (type $bytes (array (mut i8))) + (type $bytes (array (mut i8))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32 i64)))) + + ;; CHECK: (type $4 (func)) + + ;; CHECK: (type $not-configured (sub $super (func (param i32 i64)))) + + ;; CHECK: (type $configured (sub $super (func (param i32 i64)))) + + ;; CHECK: (type $7 (func)) + + ;; CHECK: (type $configureAll (func (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref))) + (type $configureAll (func (param (ref null $externs) + (ref null $funcs) + (ref null $bytes) + externref))) + + (rec + (type $super (sub (func (param i32 i64)))) + (type $configured (sub $super (func (param i32 i64)))) + (type $not-configured (sub $super (func (param i32 i64)))) + ) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $9 (func (param i32))) + + ;; CHECK: (type $10 (struct)) + + ;; CHECK: (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll) (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref))) + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + + ;; CHECK: (data $bytes "12345678") + (data $bytes "12345678") + + ;; CHECK: (elem $externs externref (item (ref.null noextern))) + (elem $externs externref + (ref.null noextern) + ) + + ;; CHECK: (elem $funcs func $configured) + (elem $funcs funcref + (ref.func $configured) + ) + + ;; CHECK: (elem declare func $referenced-not-configured) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $4) + ;; CHECK-NEXT: (call $configureAll + ;; CHECK-NEXT: (array.new_elem $externs $externs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_elem $funcs $funcs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_data $bytes $bytes + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $start + (call $configureAll + (array.new_elem $externs $externs + (i32.const 0) + (i32.const 1) + ) + (array.new_elem $funcs $funcs + (i32.const 0) + (i32.const 1) + ) + (array.new_data $bytes $bytes + (i32.const 0) + (i32.const 8) + ) + (ref.null noextern) + ) + ) + + ;; CHECK: (func $configured (type $configured) (param $0 i32) (param $1 i64) + ;; CHECK-NEXT: ) + (func $configured (type $configured) (param i32 i64) + ;; Even though we do not use the parameters, they must be kept so the + ;; function can be called as expected outside the module. + ) + + ;; CHECK: (func $referenced-not-configured (type $not-configured) (param $0 i32) (param $1 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced-not-configured) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced-not-configured (type $not-configured) (param i32 i64) + ;; This referenced function has a type in the same tree, so it also cannot + ;; be optimized. + (drop + (ref.func $referenced-not-configured) + ) + ) + + ;; CHECK: (func $unreferenced (type $4) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: ) + (func $unreferenced (type $configured) (param i32 i64) + ;; This unreferenced function can be optimized even though it has the same + ;; type as a configured function. + ) + + ;; CHECK: (func $call-configured (type $9) (param $0 i32) + ;; CHECK-NEXT: (call $configured + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-configured (param i32) + ;; The parameter must be kept because it is used when calling the configured + ;; function, which keeps its parameter. + (call $configured + (local.get 0) + (i64.const 1) + ) + ) +) diff --git a/test/lit/passes/dae2-nogc.wast b/test/lit/passes/dae2-nogc.wast new file mode 100644 index 00000000000..6a335f538f3 --- /dev/null +++ b/test/lit/passes/dae2-nogc.wast @@ -0,0 +1,62 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-opt %s -all --disable-gc --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $2 (func (param i32 i64))) + + ;; CHECK: (type $no-funcs (func (param i64))) + (type $no-funcs (func (param i64))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $referenced)) + + ;; CHECK: (elem $implicit-elem (i32.const 0) $referenced) + + ;; CHECK: (func $referenced (param $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $unused i32) + ;; Even though the parameter is not used in any use of this function type, + ;; we cannot optimize indirect calls without GC enabled. + (nop) + ) + + ;; CHECK: (func $not-referenced + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $not-referenced (type $f) (param $unused i32) + ;; Non-referenced functions with the same types can still be optimized. + (nop) + ) + + ;; CHECK: (func $caller (param $i32 i32) (param $i64 i64) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_indirect $t (type $no-funcs) + ;; CHECK-NEXT: (local.get $i64) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $i32 i32) (param $i64 i64) + ;; Since the param remains used, we should not optimize indirect calls and + ;; we should not optimize out the forwarded params. + (call_indirect (type $f) + (local.get $i32) + (i32.const 0) + ) + ;; Even function types with no functions cannot be optimized because in + ;; general this would require being able to generate different function + ;; types with the same signature, which we cannot do without GC. + (call_indirect (type $no-funcs) + (local.get $i64) + (i32.const 1) + ) + ) +) diff --git a/test/lit/passes/dae2-open-world.wast b/test/lit/passes/dae2-open-world.wast new file mode 100644 index 00000000000..48473c6d8ea --- /dev/null +++ b/test/lit/passes/dae2-open-world.wast @@ -0,0 +1,69 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-opt %s -all --dae2 -S -o - | filecheck %s + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + ;; CHECK: (type $no-funcs (func (param i64))) + (type $no-funcs (func (param i64))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func (param i32 i64 (ref $no-funcs)))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $referenced)) + + ;; CHECK: (elem $implicit-elem (i32.const 0) $referenced) + + ;; CHECK: (func $referenced (type $f) (param $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $unused i32) + ;; Even though the parameter is not used in any use of this function type, + ;; we cannot optimize indirect calls in an open world. + (nop) + ) + + ;; CHECK: (func $not-referenced (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $not-referenced (type $f) (param $unused i32) + ;; Non-referenced functions with the same types can still be optimized. + (nop) + ) + + ;; CHECK: (func $caller (type $3) (param $i32 i32) (param $i64 i64) (param $func (ref $no-funcs)) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $no-funcs + ;; CHECK-NEXT: (local.get $i64) + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $i32 i32) (param $i64 i64) (param $func (ref $no-funcs)) + ;; Since the param remains used, we should not optimize indirect calls or + ;; call_refs and we should not optimize out the forwarded params. + (call_indirect (type $f) + (local.get $i32) + (i32.const 0) + ) + (call_ref $f + (local.get $i32) + (ref.func $referenced) + ) + ;; Even function types with no functions cannot be optimized because in + ;; an open world there might be external functions with the type. + (call_ref $no-funcs + (local.get $i64) + (local.get $func) + ) + ) +) diff --git a/test/lit/passes/dae2-stack-switching.wast b/test/lit/passes/dae2-stack-switching.wast new file mode 100644 index 00000000000..fc50428d692 --- /dev/null +++ b/test/lit/passes/dae2-stack-switching.wast @@ -0,0 +1,376 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +;; TODO: Analyze and optimize stack switching instructions. + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $k (cont $f)) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + (type $k (cont $f)) + + (type $f-unused (func (param i32))) + (type $k-unused (cont $f-unused)) + + ) + ;; TODO: switch + + ;; CHECK: (elem declare func $f) + + ;; CHECK: (func $f (type $f) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (type $f) (param i32) + ;; This function is used in a continuation, but not in a way that it ever + ;; receives parameters, so it can be optimized. + (nop) + ) + + ;; CHECK: (func $cont-new (type $1) + ;; CHECK-NEXT: (local $k (ref null $k)) + ;; CHECK-NEXT: (local.set $k + ;; CHECK-NEXT: (block (result (ref null $k)) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.new $k + ;; CHECK-NEXT: (ref.func $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cont-new + ;; Having a local that uses a continuation type, and even moving values into + ;; and out of that local, is not enough to inhibit optimization. + (local $k (ref null $k)) + (local.set $k + (block (result (ref null $k)) + (local.get $k) + ) + ) + ;; A cont.new is not enough to inhibit optimizations, either. + (drop + (cont.new $k + (ref.func $f) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $k-sent (cont $f-sent)) + + ;; CHECK: (type $k (cont $f)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $f-sent (func)) + + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + (type $k (cont $f)) + + (type $f-sent (func (param i64))) + (type $k-sent (cont $f-sent)) + + ;; CHECK: (type $5 (func)) + + ;; CHECK: (type $6 (func (param i32 (ref $k)))) + + ;; CHECK: (elem declare func $f $f-sent) + + ;; CHECK: (tag $e (type $5)) + (tag $e) + + ;; CHECK: (func $f (type $f) (param $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (type $f) (param i32) + ;; This referenced function will not be optimized. + (drop + (ref.func $f) + ) + ) + + ;; CHECK: (func $unreferenced (type $2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $f) (param i32) + ;; This unreferenced function with the same type can still be optimized. + (nop) + ) + + ;; CHECK: (func $f-sent (type $f-sent) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f-sent) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f-sent (type $f-sent) (param i64) + ;; This can be optimized. + (drop + (ref.func $f-sent) + ) + ) + + ;; CHECK: (func $resume (type $6) (param $x i32) (param $k (ref $k)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref null $k-sent)) + ;; CHECK-NEXT: (resume $k (on $e $l) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $resume (param $x i32) (param $k (ref $k)) + ;; resume inhibits optimizations for the resumed continuation type, but not + ;; the continuation types it sends. + (drop + (block $l (result (ref null $k-sent)) + (resume $k (on $e $l) + (local.get $x) + (local.get $k) + ) + (unreachable) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $k-sent (cont $f-sent)) + + ;; CHECK: (type $k (cont $f)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $f-sent (func)) + + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + (type $k (cont $f)) + + (type $f-sent (func (param i64))) + (type $k-sent (cont $f-sent)) + + ;; CHECK: (type $5 (func)) + + ;; CHECK: (type $6 (func (param (ref $k)))) + + ;; CHECK: (elem declare func $f $f-sent) + + ;; CHECK: (tag $e (type $5)) + (tag $e) + + ;; CHECK: (func $f (type $f) (param $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (type $f) (param i32) + ;; This referenced function will not be optimized. + (drop + (ref.func $f) + ) + ) + + ;; CHECK: (func $f-sent (type $f-sent) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f-sent) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f-sent (type $f-sent) (param i64) + ;; This can be optimized. + (drop + (ref.func $f-sent) + ) + ) + + ;; CHECK: (func $resume-throw (type $6) (param $k (ref $k)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref null $k-sent)) + ;; CHECK-NEXT: (resume_throw $k $e (on $e $l) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $resume-throw (param $k (ref $k)) + ;; resume_throw inhibits optimizations for the resumed continuation type, + ;; but not the continuation types it sends. + (drop + (block $l (result (ref null $k-sent)) + (resume_throw $k $e (on $e $l) + (local.get $k) + ) + (unreachable) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $k-ret (cont $f-ret)) + + ;; CHECK: (type $k (cont $f)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $f-ret (func (param i64 (ref null $k)))) + (type $f-ret (func (param i64 (ref null $k)))) + (type $k-ret (cont $f-ret)) + ;; CHECK: (type $f (func (param i32 (ref null $k-ret)))) + (type $f (func (param i32 (ref null $k-ret)))) + (type $k (cont $f)) + ) + + ;; CHECK: (type $5 (func)) + + ;; CHECK: (type $6 (func (param i32 (ref $k)))) + + ;; CHECK: (elem declare func $f $f-ret) + + ;; CHECK: (tag $e (type $5)) + (tag $e) + + ;; CHECK: (func $f (type $f) (param $0 i32) (param $1 (ref null $k-ret)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (type $f) (param i32) (param (ref null $k-ret)) + ;; This referenced function will not be optimized. + (drop + (ref.func $f) + ) + ) + + ;; CHECK: (func $f-ret (type $f-ret) (param $0 i64) (param $1 (ref null $k)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f-ret) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f-ret (type $f-ret) (param i64) (param (ref null $k)) + ;; This cannot be optimized either. + (drop + (ref.func $f-ret) + ) + ) + + ;; CHECK: (func $switch (type $6) (param $x i32) (param $k (ref $k)) + ;; CHECK-NEXT: (tuple.drop 2 + ;; CHECK-NEXT: (switch $k $e + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block ;; (replaces unreachable StackSwitch we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $switch (param $x i32) (param $k (ref $k)) + ;; switch inhibits optimizations for both the target and return continuation + ;; types. + (tuple.drop 2 + (switch $k $e + (local.get $x) + (local.get $k) + ) + ) + ;; Do not get confused when the continuation is unreachable. + (switch $k $e + (local.get $x) + (unreachable) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $k2 (cont $f2)) + + ;; CHECK: (type $k1 (cont $f1)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $f2 (func (param i64))) + + ;; CHECK: (type $f1 (func (param i32 i64))) + (type $f1 (func (param i32 i64))) + (type $k1 (cont $f1)) + (type $f2 (func (param i64))) + (type $k2 (cont $f2)) + ;; CHECK: (type $5 (func)) + + ;; CHECK: (type $6 (func (param i32 (ref $k1)))) + + ;; CHECK: (elem declare func $f1 $f2) + + ;; CHECK: (tag $e (type $5)) + (tag $e) + + ;; CHECK: (func $f1 (type $f1) (param $0 i32) (param $1 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f1 (type $f1) (param i32) (param i64) + ;; This referenced function will not be optimized. + (drop + (ref.func $f1) + ) + ) + + ;; CHECK: (func $f2 (type $f2) (param $0 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f2 (type $f2) (param i64) + ;; This will not be optimized either. + (drop + (ref.func $f2) + ) + ) + + ;; CHECK: (func $cont-bind (type $6) (param $x i32) (param $k1 (ref $k1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.bind $k1 $k2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $k1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cont-bind (param $x i32) (param $k1 (ref $k1)) + ;; cont.bind inhibits optimizations for both the input and output types. + (drop + (cont.bind $k1 $k2 + (local.get $x) + (local.get $k1) + ) + ) + ) +) diff --git a/test/lit/passes/dae2-tnh.wast b/test/lit/passes/dae2-tnh.wast new file mode 100644 index 00000000000..f9384611f9e --- /dev/null +++ b/test/lit/passes/dae2-tnh.wast @@ -0,0 +1,134 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s -all --closed-world -tnh --dae2 --closed-world -S -o - \ +;; RUN: | filecheck %s + +;; RUN: wasm-opt %s -all --closed-world --dae2 --closed-world -S -o - \ +;; RUN: | filecheck %s --check-prefix=TRAPS + + +(module + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + + ;; CHECK: (type $desc (describes $struct) (struct)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $f (func)) + ;; TRAPS: (rec + ;; TRAPS-NEXT: (type $struct (descriptor $desc) (struct)) + + ;; TRAPS: (type $desc (describes $struct) (struct)) + + ;; TRAPS: (type $2 (func)) + + ;; TRAPS: (type $3 (func)) + + ;; TRAPS: (type $f (func)) + (type $f (func (param i32))) + + (rec + (type $struct (descriptor $desc) (struct)) + (type $desc (describes $struct) (struct)) + ) + + ;; CHECK: (memory $m 0 0) + ;; TRAPS: (rec + ;; TRAPS-NEXT: (type $5 (func (param i32))) + + ;; TRAPS: (type $6 (struct)) + + ;; TRAPS: (type $7 (func (param (ref null (exact $desc))))) + + ;; TRAPS: (rec + ;; TRAPS-NEXT: (type $8 (func (param anyref))) + + ;; TRAPS: (type $9 (struct)) + + ;; TRAPS: (memory $m 0 0) + (memory $m 0 0) + ;; CHECK: (func $division (type $2) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $division) + ;; CHECK-NEXT: ) + ;; TRAPS: (func $division (type $5) (param $x i32) + ;; TRAPS-NEXT: (call $division + ;; TRAPS-NEXT: (i32.div_u + ;; TRAPS-NEXT: (i32.const -1) + ;; TRAPS-NEXT: (local.get $x) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + (func $division (param $x i32) + (call $division + (i32.div_u + (i32.const -1) + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $load (type $2) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (call $load) + ;; CHECK-NEXT: ) + ;; TRAPS: (func $load (type $5) (param $x i32) + ;; TRAPS-NEXT: (call $load + ;; TRAPS-NEXT: (i32.load + ;; TRAPS-NEXT: (local.get $x) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + (func $load (param $x i32) + (call $load + (i32.load + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $cast (type $2) + ;; CHECK-NEXT: (local $ref anyref) + ;; CHECK-NEXT: (call $cast) + ;; CHECK-NEXT: ) + ;; TRAPS: (func $cast (type $8) (param $ref anyref) + ;; TRAPS-NEXT: (call $cast + ;; TRAPS-NEXT: (ref.cast (ref i31) + ;; TRAPS-NEXT: (local.get $ref) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + (func $cast (param $ref anyref) + (call $cast + (ref.cast (ref i31) + (local.get $ref) + ) + ) + ) + + ;; CHECK: (func $allocation (type $2) + ;; CHECK-NEXT: (local $desc (ref null (exact $desc))) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; TRAPS: (func $allocation (type $7) (param $desc (ref null (exact $desc))) + ;; TRAPS-NEXT: (drop + ;; TRAPS-NEXT: (struct.new_default_desc $struct + ;; TRAPS-NEXT: (local.get $desc) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + ;; TRAPS-NEXT: ) + (func $allocation (param $desc (ref null (exact $desc))) + (drop + (struct.new_desc $struct + (local.get $desc) + ) + ) + ) +) diff --git a/test/lit/passes/dae2.wast b/test/lit/passes/dae2.wast new file mode 100644 index 00000000000..89efbd7f0ef --- /dev/null +++ b/test/lit/passes/dae2.wast @@ -0,0 +1,3656 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; Trivially unused parameter. + (nop) + ) +) + +(module + ;; CHECK: (type $0 (func (param i64))) + + ;; CHECK: (global $g (mut i64) (i64.const 0)) + (global $g (mut i64) (i64.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i64) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) (param $used i64) + ;; Add a used parameter. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) (param $unused i64) + ;; Same, but use the other parameter. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32 f32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut f32) (f32.const 0)) + (global $g2 (mut f32) (f32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used1 i32) (param $used2 f32) + ;; CHECK-NEXT: (local $unused2 f64) + ;; CHECK-NEXT: (local $unused1 i64) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $used1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $used2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used1 i32) + (param $unused1 i64) + (param $used2 f32) + (param $unused2 f64) + ;; Multiple interleaved used and unused params. + (global.set $g1 + (local.get $used1) + ) + (global.set $g2 + (local.get $used2) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; The parameter is used only for a recursive call, so can still be removed. + (call $test + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test1 (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call $test2) + ;; CHECK-NEXT: ) + (func $test1 (param $unused i32) + ;; We can optimize mutual recursion. + (call $test2 + (local.get $unused) + ) + ) + + ;; CHECK: (func $test2 (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call $test1) + ;; CHECK-NEXT: ) + (func $test2 (param $unused i32) + (call $test1 + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test1 (type $0) (param $used i32) + ;; CHECK-NEXT: (call $test2 + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test1 (param $used i32) + (call $test2 + (local.get $used) + ) + ) + + ;; CHECK: (func $test2 (type $0) (param $used i32) + ;; CHECK-NEXT: (call $test1 + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 (param $used i32) + ;; Same, but now the parameter is actually used, so it must be kept + ;; throughout the recursive call chain. + (call $test1 + (local.get $used) + ) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $test (param i32 i32 i32) + ;; We can analyze recursive cycles involving multiple parameters being + ;; shuffled. + (call $test + (local.get 1) + (local.get 2) + (local.get 0) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32 i32 i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $0 i32) (param $1 i32) (param $2 i32) + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param i32 i32 i32) + ;; Same, but now one parameter is used so all must be kept. + (call $test + (local.get 1) + (local.get 2) + (local.get 0) + ) + (global.set $g + (local.get 2) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $used i32) + ;; The parameter is used here in the caller, but it can still be removed in + ;; the callee. + (global.set $g + (local.get $used) + ) + (call $callee + (local.get $used) + ) + ) + + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param $unused i32) + (nop) + ) +) + +(module + ;; CHECK: (type $0 (func (result i64))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32) (result i64))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $1) (param $used i32) (result i64) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $used i32) (result i64) + ;; Same, but now both functions have a concrete result. + (global.set $g + (local.get $used) + ) + (call $callee + (local.get $used) + ) + ) + + ;; CHECK: (func $callee (type $0) (result i64) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $callee (param $unused i32) (result i64) + (i64.const 0) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + ;; CHECK: (elem declare func $test) + + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (ref.func $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $f) (param $unused i32) + ;; If a parameter is unused in all referenced functions of a particular + ;; type, we can optimize it out in all such functions. + (call_ref $f + (local.get $unused) + (ref.func $test) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $test (type $f) (param $unused i32) + ;; Same, but now the passed operand is unreachable. The call cannot remain + ;; unreachable without its unreachable child, so we replace it entirely. + (call_ref $f + (unreachable) + (ref.func $test) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $test)) + ;; CHECK: (elem $implicit-elem (i32.const 0) $test) + + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $f) (param $unused i32) + ;; Same, but with a call_indirect. + (call_indirect (type $f) + (local.get $unused) + (i32.const 0) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + ;; CHECK: (elem declare func $test1 $test2) + + ;; CHECK: (func $test1 (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (ref.func $test1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test1 (type $f) (param $unused i32) + ;; We can optimize because the parameter is unused in both referenced + ;; functions. + (call_ref $f + (local.get $unused) + (ref.func $test1) + ) + ) + + ;; CHECK: (func $test2 (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (ref.func $test2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 (type $f) (param $unused i32) + (call_ref $f + (local.get $unused) + (ref.func $test2) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $test1 $test2)) + ;; CHECK: (elem $implicit-elem (i32.const 0) $test1 $test2) + + ;; CHECK: (func $test1 (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test1 (type $f) (param $unused i32) + ;; Same, but with call_indirect. We can optimize because the parameter is + ;; unused in both referenced functions. + (call_indirect (type $f) + (local.get $unused) + (i32.const 0) + ) + ) + + ;; CHECK: (func $test2 (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 (type $f) (param $unused i32) + (call_indirect (type $f) + (local.get $unused) + (i32.const 1) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $test1 $test2) + + ;; CHECK: (func $test1 (type $f) (param $unused i32) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: (ref.func $test1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test1 (type $f) (param $unused i32) + ;; We cannot optimize because the parameter is used in another referenced + ;; function of the same type. + (call_ref $f + (local.get $unused) + (ref.func $test1) + ) + ) + + ;; CHECK: (func $test2 (type $f) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $test2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 (type $f) (param $used i32) + (drop + (ref.func $test2) + ) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $test1 $test2)) + + ;; CHECK: (elem $implicit-elem (i32.const 0) $test1 $test2) + + ;; CHECK: (func $test1 (type $f) (param $unused i32) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test1 (type $f) (param $unused i32) + ;; Same, but with call_indirect. We cannot optimize because the parameter is + ;; used in another referenced function of the same type. + (call_indirect (type $f) + (local.get $unused) + (i32.const 0) + ) + ) + + ;; CHECK: (func $test2 (type $f) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test2 (type $f) (param $used i32) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func)) + (type $f (func (param i32 i64))) + ;; CHECK: (type $1 (func)) + + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $f (ref null $f)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + ;; There is no function with type $f, so we can remove all its parameters. + (local $f (ref null $f)) + (call_ref $f + (i32.const 0) + (i64.const 1) + (local.get $f) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func)) + (type $f (func (param i32 i64))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) + + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $f (ref null $f)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + ;; Same, but with call_indirect. There is no function with type $f, so we + ;; can remove all its parameters. + (local $f (ref null $f)) + (call_indirect (type $f) + (i32.const 0) + (i64.const 1) + (i32.const 0) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func))) + (type $super (sub (func (param i32 i64)))) + ;; CHECK: (type $sub1 (sub $super (func))) + (type $sub1 (sub $super (func (param i32 i64)))) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $sub2 (sub $super (func))) + (type $sub2 (sub $super (func (param i32 i64)))) + ) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $sub2) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $sub2) + (drop + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $test (type $super) + ;; CHECK-NEXT: (local $sub1 (ref null $sub1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (local.get $sub1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + ;; There is no function with type $sub1, but there is a referenced function + ;; with a type in the same tree. Since its parameters are not used, we can + ;; optimize all the types in the tree. + (local $sub1 (ref null $sub1)) + (call_ref $sub1 + (i32.const 0) + (i64.const 1) + (local.get $sub1) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32 i64)))) + (type $super (sub (func (param i32 i64)))) + ;; CHECK: (type $sub1 (sub $super (func (param i32 i64)))) + (type $sub1 (sub $super (func (param i32 i64)))) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $sub2 (sub $super (func (param i32 i64)))) + (type $sub2 (sub $super (func (param i32 i64)))) + ) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i64) (i64.const 0)) + (global $g2 (mut i64) (i64.const 0)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $sub2) (param $used1 i32) (param $used2 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $used1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $used2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $sub2) (param $used1 i32) (param $used2 i64) + (drop + (ref.func $referenced) + ) + (global.set $g1 + (local.get $used1) + ) + (global.set $g2 + (local.get $used2) + ) + ) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $sub1 (ref null $sub1)) + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: (local.get $sub1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + ;; Same, but now the parameters are used. We cannot optimize any of the + ;; types in the tree. + (local $sub1 (ref null $sub1)) + (call_ref $sub1 + (i32.const 0) + (i64.const 1) + (local.get $sub1) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func (param i32))) + (type $f (func (param i32 i32 i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $callee) + + ;; CHECK: (func $test (type $f) (param $forwarded i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $forwarded) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $forwarded i32) + ;; The forwarded parameter is forwarded from index 0 to index 2. Tests that + ;; the vectors in the reverse indirect call graph are the correct size. + (call_ref $f + (i32.const 0) + (i32.const 0) + (local.get $forwarded) + (ref.func $callee) + ) + ) + + ;; CHECK: (func $callee (type $f) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $callee (type $f) (param i32 i32 i32) + (global.set $g + (local.get 2) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + (type $f (func (param i32))) + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $unused i32) + ;; Now the other function is not referenced, so we can optimize. + (call_ref $f + (local.get $unused) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced (type $f) (param $used i32) + ;; This function will get a new type to avoid its type being rewritten along + ;; with $referenced's type. + (global.set $g + (local.get $used) + ) + ) +) + +(module + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f2 (func)) + + ;; CHECK: (type $f1 (func)) + (type $f1 (func (param i32))) + (type $f2 (func (param i64))) + ;; CHECK: (elem declare func $referenced1 $referenced2) + + ;; CHECK: (func $referenced1 (type $f1) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $f1) (param $unused i32) + (drop + (ref.func $referenced1) + ) + ) + + ;; CHECK: (func $referenced2 (type $f2) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $f2) (param $unused i64) + ;; Although it is optimized to the same signature, this must have a + ;; different type than $referenced1's type to maintain separate type + ;; identities + (drop + (ref.func $referenced2) + ) + ) + + ;; CHECK: (func $unreferenced1 (type $f2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced1 (type $f1) (param $unused i32) + ;; This does not have to reuse $f1, but it does because it will be optimized + ;; the same way. + (nop) + ) + + ;; CHECK: (func $unreferenced2 (type $f2) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced2 (type $f2) (param $unused i64) + ;; Same here. + (nop) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f2 (func)) + + ;; CHECK: (type $f1 (func)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + (type $f1 (func (param i32 i32))) + (type $f2 (func (param i64 i32))) + ;; CHECK: (elem declare func $referenced1 $referenced2) + + ;; CHECK: (func $referenced1 (type $f1) + ;; CHECK-NEXT: (local $unused2 i32) + ;; CHECK-NEXT: (local $unused1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $f1) (param $unused1 i32) (param $unused2 i32) + (drop + (ref.func $referenced1) + ) + ) + + ;; CHECK: (func $referenced2 (type $f2) + ;; CHECK-NEXT: (local $unused2 i32) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $f2) (param $unused i64) (param $unused2 i32) + ;; Although it is optimized to the same signature, this must have a + ;; different type than $referenced1's type to maintain separate type + ;; identities + (drop + (ref.func $referenced2) + ) + ) + + ;; CHECK: (func $unreferenced1 (type $0) (param $used i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced1 (type $f1) (param $unused i32) (param $used i32) + ;; Now this needs a new type because $f1 will be optimized differently. + (global.set $g + (local.get $used) + ) + ) + + ;; CHECK: (func $unreferenced2 (type $0) (param $used i32) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced2 (type $f2) (param $unused i64) (param $used i32) + ;; This also needs a new type. It should be the same new type as used by + ;; $unreferenced1 to minimize the number of new types and because identity + ;; doesn't matter for unreferenced functions. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $public (sub (func (param i32)))) + (type $public (sub (func (param i32)))) + + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + + ;; CHECK: (global $public (ref null $public) (ref.null nofunc)) + (global $public (export "public") (ref null $public) (ref.null nofunc)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (export "public" (global $public)) + + ;; CHECK: (func $referenced (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $unused i32) + (drop + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $public) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced (type $f) (param $used i32) + ;; Since type identity doesn't matter for unreferenced functions, it's ok + ;; to use a public type as the replacement. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $f) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $used i32) + (drop + (ref.func $referenced) + ) + (global.set $g + (local.get $used) + ) + ) + + ;; CHECK: (func $unreferenced (type $f) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced (type $f) (param $used i32) + ;; Now there is no replacement type necessary because all the parameters of + ;; type $f are used in referenced functions, so the type will not be + ;; optimized. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $f) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $used i32) + (drop + (ref.func $referenced) + ) + (global.set $g + (local.get $used) + ) + ) + + ;; CHECK: (func $unreferenced (type $1) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $f) (param $unused i32) + ;; Same, but now the unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $f (func (param (ref $f)))) + (type $f (func (param (ref $f) (ref $f)))) + + ;; CHECK: (type $1 (func (param (ref $f) (ref $f)))) + + ;; CHECK: (global $g (mut (ref null $f)) (ref.null nofunc)) + (global $g (mut (ref null $f)) (ref.null nofunc)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $f) (param $used (ref $f)) + ;; CHECK-NEXT: (local $unused (ref $f)) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $f) (param $used (ref $f)) (param $unused (ref $f)) + ;; The updated type should still be self-recursive. + (global.set $g + (local.get $used) + ) + (drop + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced1 (type $1) (param $used1 (ref $f)) (param $used2 (ref $f)) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced1 (type $f) (param $used1 (ref $f)) (param $used2 (ref $f)) + ;; In contrast, the replacement type does not need to be self-recursive + ;; because its identity does not matter. It should reference but not be the + ;; updated type $f. + (global.set $g + (local.get $used1) + ) + (global.set $g + (local.get $used2) + ) + ) + + ;; CHECK: (func $unreferenced2 (type $f) (param $used (ref $f)) + ;; CHECK-NEXT: (local $unused (ref $f)) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced2 (type $f) (param $unused (ref $f)) (param $used (ref $f)) + ;; Because this unreferenced function will be optimized to have the same + ;; that $f will be optimized to, it can keep using type $f. + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $optimized (func (param i64))) + + ;; CHECK: (type $uninhabited (func)) + (type $uninhabited (func (param i32))) + + (type $optimized (func (param i32 i64))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $2 (func (param i32))) + + ;; CHECK: (type $3 (struct)) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i64) (i64.const 0)) + (global $g2 (mut i64) (i64.const 0)) + + ;; CHECK: (global $use-uninhabited (ref null $uninhabited) (ref.null nofunc)) + (global $use-uninhabited (ref null $uninhabited) (ref.null nofunc)) + + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $optimized) (param $used i64) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $optimized) (param $unused i32) (param $used i64) + (global.set $g2 + (local.get $used) + ) + (drop + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $2) (param $used i32) + ;; CHECK-NEXT: (local $unused i64) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced (type $optimized) (param $used i32) (param $unused i64) + ;; The placeholder type here should not be the same as unoptimized + ;; $uninhabited, because then the global type rewrite would further rewrite + ;; it incorrectly. + (global.set $g1 + (local.get $used) + ) + ) +) + +(module + ;; Keep this type public so it will remain the same. + ;; CHECK: (type $public (struct)) + (type $public (struct)) + + (rec + ;; The type of the test function. Conflicts with the default brand. Will + ;; be optimized because no referenced functions have type $f. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func)) + (type $f (func (param (ref $public)))) + (type (struct)) + ) + ;; The signature of the test function, but without the brand. Since this type + ;; is not inhabited in this module, its parameters will be removed. + ;; CHECK: (type $f' (func)) + (type $f' (func (param (ref $public)))) + + ;; Make $public public. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $3 (func (param (ref $public)))) + + ;; CHECK: (type $4 (array (mut i8))) + + ;; CHECK: (global $public (ref null $public) (ref.null none)) + (global $public (export "public") (ref null $public) (ref.null none)) + + ;; Keep $f' alive. + ;; CHECK: (global $f' (ref null $f') (ref.null nofunc)) + (global $f' (ref null $f') (ref.null nofunc)) + + ;; CHECK: (global $g (mut (ref null $public)) (ref.null none)) + (global $g (mut (ref null $public)) (ref.null none)) + + ;; CHECK: (export "public" (global $public)) + + ;; CHECK: (func $test (type $3) (param $used (ref $public)) + ;; CHECK-NEXT: (local $1 anyref) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $f) (param $used (ref $public)) + ;; Regression test. A previous version of the code would try to create a new + ;; type with the desired signature, which is the same as the current + ;; signature, see that the new type is $f', which is mapped to a different + ;; signature, so then try to add a brand to disambiguate from $f'. But this + ;; was done incorrectly, so the replacement type was $f itself, which was + ;; then rewritten to have the parameters removed. The local.get below then + ;; picked up the wrong type, making the output invalid. + (local anyref) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $public (func (param i32))) + (type $public (func (param i32))) + + (func $import (import "" "") (type $public) (param i32)) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "" "" (func $import (type $public) (param i32))) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $public) (param $unused i32) + ;; CHECK-NEXT: (call_ref $public + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $public) (param $unused i32) + ;; This referenced function cannot be optimized because we cannot rewrite + ;; public types, and we cannot just give referenced functions new types. + ;; + ;; TODO: In many cases we could give referenced functions new types, as long + ;; as we do so for all referenced functions with types in the same tree. + ;; Investigate this. + (call_ref $public + (i32.const 0) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $1) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $public) (param $unused i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $public (func (param i32))) + (type $public (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (export "" (func $export)) + + ;; CHECK: (func $export (type $public) (param $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $export (export "") (type $public) (param $unused i32) + (nop) + ) + + ;; CHECK: (func $referenced (type $public) (param $unused i32) + ;; CHECK-NEXT: (call_ref $public + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $public) (param $unused i32) + ;; Same, but now the type is public because of an export. + (call_ref $public + (i32.const 0) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $1) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $public) (param $unused i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $public (func (param i32))) + (type $public (func (param i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (global $g (ref null $public) (ref.null nofunc)) + (global $g (export "") (ref null $public) (ref.null nofunc)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (export "" (global $g)) + + ;; CHECK: (func $referenced (type $public) (param $unused i32) + ;; CHECK-NEXT: (call_ref $public + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $public) (param $unused i32) + ;; Same, but now the type is public because of a global. + (call_ref $public + (i32.const 0) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $1) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $public) (param $unused i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $public (func (param i32))) + (type $public (func (param i32))) + ;; CHECK: (type $direct-public (struct (field (ref $public)))) + (type $direct-public (struct (field (ref $public)))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (global $g (ref null $direct-public) (ref.null none)) + (global $g (export "") (ref null $direct-public) (ref.null none)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (export "" (global $g)) + + ;; CHECK: (func $referenced (type $public) (param $unused i32) + ;; CHECK-NEXT: (call_ref $public + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $public) (param $unused i32) + ;; Same, but now the type is public only indirectly. + (call_ref $public + (i32.const 0) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $public) (param $unused i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $public-super (sub (func (param i32)))) + (type $public-super (sub (func (param i32)))) + ;; CHECK: (type $private-sub (sub $public-super (func (param i32)))) + (type $private-sub (sub $public-super (func (param i32)))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (global $g (ref null $public-super) (ref.null nofunc)) + (global $g (export "") (ref null $public-super) (ref.null nofunc)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (export "" (global $g)) + + ;; CHECK: (func $referenced (type $private-sub) (param $unused i32) + ;; CHECK-NEXT: (call_ref $private-sub + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $private-sub) (param $unused i32) + ;; The type is no longer public, but still cannot be rewritten because it is + ;; constrained by its public supertype. We cannot optimize. + (call_ref $private-sub + (i32.const 0) + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $private-sub) (param $unused i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + ;; CHECK: (type $public-super (sub (func (param i32)))) + (type $public-super (sub (func (param i32)))) + ;; CHECK: (type $private-sub (sub $public-super (func (param i32)))) + (type $private-sub (sub $public-super (func (param i32)))) + + ;; CHECK: (global $public (ref null $public-super) (ref.null nofunc)) + (global $public (export "") (ref null $public-super) (ref.null nofunc)) + + ;; Even though there are no functions with type $private-sub, we cannot + ;; optimize it because we cannot rewrite its public supertype. + ;; CHECK: (global $private (ref null $private-sub) (ref.null nofunc)) + (global $private (ref null $private-sub) (ref.null nofunc)) +) + +;; CHECK: (export "" (global $public)) +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $used-super (sub (func (param i32)))) + (type $used-super (sub (func (param i32)))) + ;; CHECK: (type $unused-sub (sub $used-super (func (param i32)))) + (type $unused-sub (sub $used-super (func (param i32)))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; Even though there are no functions with type $unused-sub, we cannot + ;; optimize it because we do not optimize its supertype. + ;; CHECK: (global $unused (ref null $unused-sub) (ref.null nofunc)) + (global $unused (ref null $unused-sub) (ref.null nofunc)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (func $referenced (type $used-super) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $used-super) (param $used i32) + (drop + (ref.func $referenced) + ) + (global.set $g + (local.get $used) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32)))) + (type $super (sub (func (param i32)))) + ;; CHECK: (type $sub1 (sub $super (func (param i32)))) + (type $sub1 (sub $super (func (param i32)))) + ;; CHECK: (type $sub2 (sub $super (func (param i32)))) + (type $sub2 (sub $super (func (param i32)))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (elem declare func $referenced) + + ;; CHECK: (tag $t (type $sub2) (param i32)) + (tag $t (type $sub2)) + + ;; CHECK: (func $referenced (type $sub1) (param $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced (type $sub1) (param i32) + ;; The tag type cannot be rewritten, so no other type in its type tree can + ;; be rewritten. We cannot optimize this referenced function. + (drop + (ref.func $referenced) + ) + ) + + ;; CHECK: (func $unreferenced (type $3) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $unreferenced (type $sub1) (param i32) + ;; The unreferenced function can still be optimized. + (nop) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32 f32)))) + (type $super (sub (func (param i32 i64 f32)))) + ;; CHECK: (type $sub2 (sub $super (func (param i32 f32)))) + + ;; CHECK: (type $sub1 (sub $super (func (param i32 f32)))) + (type $sub1 (sub $super (func (param i32 i64 f32)))) + (type $sub2 (sub $super (func (param i32 i64 f32)))) + ) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut f32) (f32.const 0)) + (global $g2 (mut f32) (f32.const 0)) + + ;; CHECK: (elem declare func $referenced1 $referenced2) + + ;; CHECK: (func $referenced1 (type $sub1) (param $0 i32) (param $1 f32) + ;; CHECK-NEXT: (local $2 i64) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $sub1) (param i32 i64 f32) + ;; Use parameter 0. Only parameter 1 will be removed.. + (global.set $g1 + (local.get 0) + ) + (drop + (ref.func $referenced1) + ) + ) + + ;; CHECK: (func $referenced2 (type $sub2) (param $0 i32) (param $1 f32) + ;; CHECK-NEXT: (local $2 i64) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $sub2) (param i32 i64 f32) + ;; Use parameter 2. Only parameter 1 will be removed. + (global.set $g2 + (local.get 2) + ) + (drop + (ref.func $referenced2) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func))) + (type $super (sub (func (param i32 i32)))) + ;; CHECK: (type $sub2 (sub $super (func))) + + ;; CHECK: (type $sub1 (sub $super (func))) + (type $sub1 (sub $super (func (param i32 i32)))) + (type $sub2 (sub $super (func (param i32 i32)))) + ) + + ;; CHECK: (elem declare func $referenced1 $referenced2) + + ;; CHECK: (func $referenced1 (type $sub1) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sub2 + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $sub1) (param i32 i32) + ;; Forward parameter 0 as parameter 1 in another type in the same tree. + (call_ref $sub2 + (i32.const 0) + (local.get 0) + (ref.func $referenced2) + ) + ) + + ;; CHECK: (func $referenced2 (type $sub2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $sub2) (param i32 i32) + ;; Forward parameter 1 as parameter 0 in another type in the same tree. + (call_ref $sub1 + (local.get 1) + (i32.const 1) + (ref.func $referenced1) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32 i32)))) + (type $super (sub (func (param i32 i32)))) + ;; CHECK: (type $sub1 (sub $super (func (param i32 i32)))) + (type $sub1 (sub $super (func (param i32 i32)))) + (type $sub2 (sub $super (func (param i32 i32)))) + ) + + ;; CHECK: (type $sub1_1 (sub $super (func (param i32 i32)))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced1 $referenced2) + + ;; CHECK: (func $referenced1 (type $sub1_1) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $sub1) (param i32 i32) + ;; Forward parameter 0 as parameter 1 in another type in the same tree. This + ;; time also use parameter 1. + (call_ref $sub2 + (i32.const 0) + (local.get 0) + (ref.func $referenced2) + ) + (global.set $g + (local.get 1) + ) + ) + + ;; CHECK: (func $referenced2 (type $sub1) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (call_ref $sub1_1 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $sub2) (param i32 i32) + ;; Forward parameter 1 as parameter 0 in another type in the same tree. + (call_ref $sub1 + (local.get 1) + (i32.const 1) + (ref.func $referenced1) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func))) + (type $super (sub (func (param i32 i32)))) + ;; CHECK: (type $sub2 (sub $super (func))) + + ;; CHECK: (type $sub1 (sub $super (func))) + (type $sub1 (sub $super (func (param i32 i32)))) + (type $sub2 (sub $super (func (param i32 i32)))) + ) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced2) + + ;; CHECK: (func $unreferenced1 (type $3) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sub2 + ;; CHECK-NEXT: (ref.func $referenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced1 (type $sub1) (param i32 i32) + ;; Forward parameter 0 as parameter 1 in another type in the same tree. This + ;; time the first function is not referenced. + (call_ref $sub2 + (i32.const 0) + (local.get 0) + (ref.func $referenced2) + ) + (global.set $g + (local.get 1) + ) + ) + + ;; CHECK: (func $referenced2 (type $sub2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $unreferenced1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced2 (type $sub2) (param i32 i32) + ;; Forward parameter 1 as parameter 0 in another type in the same tree (but + ;; it's not referenced) + (call $unreferenced1 + (local.get 1) + (i32.const 1) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (param i32)))) + (type $super (sub (func (param i32 i32)))) + ;; CHECK: (type $sub2 (sub $super (func (param i32)))) + + ;; CHECK: (type $sub1 (sub $super (func (param i32)))) + (type $sub1 (sub $super (func (param i32 i32)))) + (type $sub2 (sub $super (func (param i32 i32)))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $referenced1) + + ;; CHECK: (func $referenced1 (type $sub1) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $unreferenced2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced1 (type $sub1) (param i32 i32) + ;; Forward parameter 0 as parameter 1 in another type in the same tree. This + ;; time the second function is not referenced. + (call $unreferenced2 + (i32.const 0) + (local.get 0) + ) + (global.set $g + (local.get 1) + ) + ) + + ;; CHECK: (func $unreferenced2 (type $3) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (ref.func $referenced1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreferenced2 (type $sub2) (param i32 i32) + ;; Forward parameter 1 as parameter 0 in another type in the same tree. + (call_ref $sub1 + (local.get 1) + (i32.const 1) + (ref.func $referenced1) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; The parameter is unused, but there is a side effect we cannot remove. + (call $test + (block (result i32) + (call $effect) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $test (param $unused (ref any)) + ;; Same, but now the unused parameter type is not defaultable. We can still + ;; optimize. + (call $test + (block (result (ref any)) + (call $effect) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $1 (func (result (ref any)))) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + + ;; CHECK: (func $test (type $1) (result (ref any)) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref any)) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.set $unused + ;; CHECK-NEXT: (ref.i31 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: ) + (func $test (param $unused (ref any)) (result (ref any)) + ;; Same, but now there are also subsequent gets of the local. The get that + ;; can read the original parameter value must be removed. + (drop + (call $test + (block (result (ref any)) + (call $effect) + (local.get $unused) + ) + ) + ) + (drop + (local.get $unused) + ) + ;; The gets after this do not actually use the parameter and are not + ;; removed. + (local.set $unused + (ref.i31 + (i32.const 0) + ) + ) + (drop + (local.get $unused) + ) + (local.get $unused) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; The parameter is still not necessary for the recursive call alone, but it + ;; is also used elsewhere. + (call $test + (local.get $used) + ) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (import "" "" (func $imported (type $0) (param i32))) + (import "" "" (func $imported (param i32))) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (call $imported + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; Calling an imported function counts as a use. + (call $imported + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32 i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (import "" "" (func $imported (type $0) (param i32 i32))) + (import "" "" (func $imported (param i32 i32))) + + ;; CHECK: (func $test (type $1) (param $used i32) + ;; CHECK-NEXT: (call $imported + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; If the imported function is never reached due to an unreachable child, we + ;; could consider the get unused. But we leave handling this to DCE. + (call $imported + (local.get $used) + (unreachable) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $cwe (func (param i32 funcref))) + (type $cwe (func (param i32) (param funcref))) + (import "binaryen-intrinsics" "call.without.effects" + (func $call.without.effects (type $cwe) (param i32) (param funcref))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $call.without.effects (type $cwe) (param i32 funcref))) + + ;; CHECK: (elem declare func $callee $referenced-same-type) + + ;; CHECK: (func $test (type $0) (param $unused i32) + ;; CHECK-NEXT: (call $call.without.effects + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; We cannot yet optimize functions called via call.without.effects. + (call $call.without.effects + (local.get $unused) + (ref.func $callee) + ) + ) + + ;; CHECK: (func $callee (type $0) (param $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param $unused i32) + (nop) + ) + + ;; CHECK: (func $referenced-same-type (type $cwe) (param $0 i32) (param $1 funcref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $referenced-same-type) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $referenced-same-type (type $cwe) (param i32) (param funcref) + ;; Since call.without.effects is modeled as an import, it inhibits the + ;; optimization of other referenced functions that happen to share a type + ;; with it. + (drop + (ref.func $referenced-same-type) + ) + ) + + ;; CHECK: (func $unreferenced-same-type (type $2) + ;; CHECK-NEXT: (local $0 funcref) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: ) + (func $unreferenced-same-type (type $cwe) (param i32) (param funcref) + ;; But unreferenced functions with the same type can still be optimized. + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + ;; CHECK: (import "" "" (func $imported (type $f) (param i32))) + (import "" "" (func $imported (type $f) (param i32))) + + ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) + + ;; CHECK: (func $test (type $f) (param $used i32) + ;; CHECK-NEXT: (return_call $imported + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; We should not be confused by return calls. + (return_call $imported + (local.get 0) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + ;; CHECK: (import "" "" (func $imported (type $f) (param i32))) + (import "" "" (func $imported (type $f) (param i32))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $imported)) + + ;; CHECK: (elem $implicit-elem (i32.const 0) $imported) + + ;; CHECK: (func $test (type $f) (param $used i32) + ;; CHECK-NEXT: (return_call_indirect $t (type $f) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; We should not be confused by indirect return calls. + (return_call_indirect (type $f) + (local.get 0) + (i32.const 0) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param i32))) + (type $f (func (param i32))) + ;; CHECK: (import "" "" (func $imported (type $f) (param i32))) + (import "" "" (func $imported (type $f) (param i32))) + + ;; CHECK: (elem declare func $imported) + + ;; CHECK: (func $test (type $f) (param $used i32) + ;; CHECK-NEXT: (return_call_ref $f + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (ref.func $imported) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; We should not be confused by indirect reference return calls. + (return_call_ref $f + (local.get 0) + (ref.func $imported) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; The parameter is not used. + (drop + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; The parameter is not used here, but it is below. + (drop + (local.get $used) + ) + (global.set $g + (local.get $used) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (func $test (type $0) (param $used i32) (result i32) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + (func $test (param $used i32) (result i32) + ;; Returning the result of the local.get counts as using it. + ;; TODO: Optimize out unused function results as well. + (local.get $used) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + ;; The parameter is unused even though it flows through a block. + (drop + (block (result i32) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + ;; Now it is used even though it flows through a block. + (global.set $g + (block (result i32) + (local.get $used) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + (global.set $g + ;; The parameter can be used even if it flows through a unary op. + (i32.eqz + (local.get $used) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $lhs (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $lhs (param $used i32) + (global.set $g + ;; The parameter can be used if it flows through either side of a binary + ;; op. + (i32.add + (local.get $used) + (i32.const 1) + ) + ) + ) + + ;; CHECK: (func $rhs (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $rhs (param $used i32) + (global.set $g + (i32.add + (i32.const 1) + (local.get $used) + ) + ) + ) + + ;; CHECK: (func $dropped-lhs (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $dropped-lhs (param $unused i32) + ;; If the result of the add is dropped, then the parameter is not used. + (drop + (i32.add + (local.get $unused) + (i32.const 1) + ) + ) + ) + + ;; CHECK: (func $dropped-rhs (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $dropped-rhs (param $unused i32) + (drop + (i32.add + (i32.const 1) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $unreachable-lhs (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-lhs (param $unused i32) + (global.set $g + ;; Since the add never returns, we do not continue on to analyze the + ;; global.set, so we consider the parameter unused. + (i32.add + (local.get $unused) + (unreachable) + ) + ) + ) + + ;; CHECK: (func $unreachable-rhs (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-rhs (param $unused i32) + ;; Here the get does not read from the parameter because it is not + ;; reachable. We can still optimize out the parameter, but we do not remove + ;; the get. + (global.set $g + (i32.add + (unreachable) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $div_s (type $0) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.div_s + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $div_s (param $used i32) + ;; Since division can trap, it counts as a use all on its own. + (drop + (i32.div_s + (i32.const 1) + (local.get $used) + ) + ) + ) + + ;; CHECK: (func $div_u (type $0) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.div_u + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $div_u (param $used i32) + ;; Since division can trap, it counts as a use all on its own. + (drop + (i32.div_u + (i32.const 1) + (local.get $used) + ) + ) + ) + + ;; CHECK: (func $numerator (type $2) + ;; CHECK-NEXT: (local $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $numerator (param $used i32) + ;; This division is known not to trap, so we can optimize. + (drop + (i32.div_s + (local.get $used) + (i32.const 1) + ) + ) + ) + + ;; CHECK: (func $numerator-obscured (type $0) (param $used i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.div_s + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $numerator-obscured (param $used i32) + ;; The block prevents the effects analysis from knowing that the division + ;; will not trap. We do not optimize even though the value of the numerator + ;; cannot change trapping behavior. + (drop + (i32.div_s + (local.get $used) + (block (result i32) + (i32.const 1) + ) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $test (type $0) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used i32) + (global.set $g + ;; We are not precise about which tuple elements are extracted, so this + ;; counts as a use. + ;; TODO: We could be more precise about tracking tuple elements. + (tuple.extract 2 1 + (tuple.make 2 + (local.get 0) + (i32.const 1) + ) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (struct)) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $dropped-arm (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $dropped-arm (param $unused i32) + ;; local.gets of parameters in if arms can be removed. Here the ifFalse arm + ;; would become empty, so we just remove it. + (drop + (if (result i32) + (i32.const 0) + (then + (local.get $unused) + ) + (else + (local.get $unused) + ) + ) + ) + ) + + ;; CHECK: (func $dropped-arm-effects (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $dropped-arm-effects (param $unused i32) + ;; We must be careful to preserve the If structure so only the proper + ;; effects are executed. + (drop + (if (result i32) + (i32.const 0) + (then + (call $effect) + (local.get $unused) + ) + (else + (call $effect) + (local.get $unused) + ) + ) + ) + ) + + ;; CHECK: (func $used-iftrue (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $used-iftrue (param $used i32) + ;; local.gets of parameters in if arms can be treated like normal gets. + (global.set $g + (if (result i32) + (i32.const 0) + (then + (local.get $used) + ) + (else + (i32.const 1) + ) + ) + ) + ) + + ;; CHECK: (func $used-iffalse (type $1) (param $used i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $used-iffalse (param $used i32) + ;; Same, with the local.get in the other arm. + (global.set $g + (if (result i32) + (i32.const 0) + (then + (i32.const 1) + ) + (else + (local.get $used) + ) + ) + ) + ) + + ;; CHECK: (func $condition (type $1) (param $used i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $condition (param $used i32) + ;; The value flowing into an if condition controls which arm is executed. If + ;; the arms have side effects (which we conservatively assume they may), + ;; then potentially changing the value of the condition by making it an + ;; uninitialized local would be incorrect. Parameters flowing into if + ;; conditions must be considered used. + (if + (local.get $used) + (then + (nop) + ) + (else + (nop) + ) + ) + ) + + ;; CHECK: (func $condition-effect (type $1) (param $used i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $condition-effect (param $used i32) + ;; This would start trapping where it might not have before if we optimized. + (if + (local.get $used) + (then + (unreachable) + ) + (else + (nop) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (import "" "" (func $effect (type $1) (result i32))) + (import "" "" (func $effect (result i32))) + + ;; CHECK: (func $loop (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (loop $l + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $loop (param $unused i32) + ;; We can optimize out the unused parameter and its gets, but the loop must + ;; be preserved so the effects are executed the correct number of times. + (drop + (loop $l (result i32) + (drop + (call $effect) + ) + (drop + (local.get $unused) + ) + (br_if $l + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $loop-empty (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (loop $l + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $loop-empty (param $unused i32) + ;; We should not be tripped up when the entire loop body is erased. + (drop + (loop $l (result i32) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + + ;; CHECK: (tag $e (type $0)) + (tag $e) + + ;; CHECK: (func $try-empty (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-empty (param $unused i32) + (drop + (try (result i32) + (do + (local.get $unused) + ) + ) + ) + ) + + ;; CHECK: (func $try-effect (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-effect (param $unused i32) + (drop + (try (result i32) + (do + (call $effect) + (local.get $unused) + ) + ) + ) + ) + + ;; CHECK: (func $try-catch (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch_all + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-catch (param $unused i32) + (drop + (try (result i32) + (do + (i32.const 0) + ) + (catch $e + (local.get $unused) + ) + (catch $e + (call $effect) + (i32.const 1) + ) + (catch_all + (call $effect) + (i32.const 2) + ) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param (ref any)))) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $2 (func)) + + ;; CHECK: (type $3 (func (result i32))) + + ;; CHECK: (type $4 (func)) + + ;; CHECK: (type $5 (func)) + + ;; CHECK: (type $6 (func (param i32) (result i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $7 (func (param i32))) + + ;; CHECK: (type $8 (struct)) + + ;; CHECK: (import "" "" (func $effect (type $1) (result i32))) + (import "" "" (func $effect (result i32))) + + ;; CHECK: (func $labeled-block (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $labeled-block (param $unused i32) + ;; We do not bother keeping what would be an empty block. + (drop + (block $l (result i32) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $labeled-block-effect (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $labeled-block-effect (param $unused i32) + ;; We keep the effect, but we don't know that it doesn't branch, so we set + ;; up the trampoline anyway. + (drop + (block $l (result i32) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $br (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $l + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br (param $unused i32) + ;; Here the local.get is not reachable, so we don't try to remove it. + (drop + (block $l (result i32) + (drop + (call $effect) + ) + (br $l + (i32.const 0) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $br-if (type $7) (param $used i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-if (param $unused i32) (param $used i32) + (drop + (block $l (result i32) + (drop + (call $effect) + ) + (drop + (br_if $l + (i32.const 0) + (local.get $used) + ) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $br-table-mixed (type $6) (param $used i32) (result i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (block $outer (result i32) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $inner (result i32) + ;; CHECK-NEXT: (br_table $inner $l $outer + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-table-mixed (param $unused i32) (param $used i32) (result i32) + ;; This is a case where it would be impossible to remove the branch values + ;; because they also go out to labels we are not modifying. + (block $outer (result i32) + (drop + (block $l (result i32) + (drop + (call $effect) + ) + (drop + (block $inner (result i32) + (br_table $inner $l $outer + (i32.const 0) + (local.get $used) + ) + ) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + (i32.const 1) + ) + ) + + ;; CHECK: (func $br-on-non-null (type $0) (param $used (ref any)) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref any)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_on_non_null $l + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-on-non-null (param $unused (ref any)) (param $used (ref any)) + (drop + (block $l (result (ref any)) + (drop + (call $effect) + ) + (br_on_non_null $l + (local.get $used) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $br-on-cast (type $0) (param $used (ref any)) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref any)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_on_cast $l (ref any) (ref i31) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-on-cast (param $unused (ref any)) (param $used (ref any)) + (drop + (block $l (result (ref any)) + (drop + (call $effect) + ) + (drop + (br_on_cast $l (ref any) (ref i31) + (local.get $used) + ) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $br-on-cast-fail (type $0) (param $used (ref any)) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref any)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_on_cast_fail $l (ref any) (ref i31) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-on-cast-fail (param $unused (ref any)) (param $used (ref any)) + (drop + (block $l (result (ref any)) + (drop + (call $effect) + ) + (drop + (br_on_cast_fail $l (ref any) (ref i31) + (local.get $used) + ) + ) + (drop + (call $effect) + ) + (local.get $unused) + ) + ) + ) + + ;; CHECK: (func $no-overwrite-label (type $2) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $outer (result i32) + ;; CHECK-NEXT: (block $inner + ;; CHECK-NEXT: (br_if $inner + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $no-overwrite-label (param $unused i32) + (drop + ;; When we add the trampoline, we should not accidentally remove the used + ;; $inner label. + (block $outer (result i32) + (block $inner + (br_if $inner + (i32.const 0) + ) + ) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (func (param i32 i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $2 (func)) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (import "" "" (func $effect (type $0) (param i32))) + (import "" "" (func $effect (param i32))) + ;; CHECK: (tag $e (type $3)) + (tag $e) + + ;; CHECK: (func $test (type $1) (param $used1 i32) (param $used2 i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (local.get $used1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $used2) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $effect + ;; CHECK-NEXT: (i32.const 6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) (param $used1 i32) (param $used2 i32) + ;; Test mixed and nested control flow structures, all of which will have + ;; their values removed at the same time. The locations of the effects + ;; inside and before the control flow structures should remain unchanged. + (drop + (block (result i32) + (call $effect (i32.const 0)) + (block $l (result i32) + (call $effect (i32.const 1)) + (loop $loop (result i32) + (call $effect (i32.const 2)) + (br_if $loop (local.get $used1)) + (try (result i32) + (do + (call $effect (i32.const 3)) + (i32.const 0) + ) + (catch $e + (call $effect (i32.const 4)) + (if (result i32) + (local.get $used2) + (then + (call $effect (i32.const 5)) + (i32.const 1) + ) + (else + (call $effect (i32.const 6)) + (local.get $unused) + ) + ) + ) + ) + ) + ) + ) + ) + ) +) + +(module + ;; CHECK: (type $f (func)) + (type $f (func (param i32))) + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (import "" "" (func $effect (type $1) (result i32))) + (import "" "" (func $effect (result i32))) + ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) + ;; CHECK: (func $test (type $f) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (block $trampoline0 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $f) (param $unused i32) + ;; Regression test. If the call_indirect is incorrectly marked for removal + ;; during optimization, the $effect will end up appearing in the IR twice. + (drop + (block $l (result i32) + (call_indirect (type $f) + (local.get $unused) + (call $effect) + ) + (local.get $unused) + ) + ) + ) +) + +(module + ;; CHECK: (type $f (func (param (ref any)))) + (type $f (func (param (ref any)))) + ;; CHECK: (type $1 (func (result (ref $f)))) + + ;; CHECK: (import "" "" (func $effect (type $1) (result (ref $f)))) + (import "" "" (func $effect (result (ref $f)))) + ;; CHECK: (func $test (type $f) (param $used (ref any)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result (ref any)) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $used) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $used (ref any)) + ;; Similarly, if the call_ref here is incorrectly marked for removal, it + ;; it will incorrectly be removed. This also tests that we properly + ;; propagate the use of the parameter in the call_ref of public type $f back + ;; to the caller even though there is no referenced function of type $f. + (drop + (block $l (result (ref any)) + (call_ref $f + (local.get $used) + (call $effect) + ) + (local.get $used) + ) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func)) + (type $f (func (param i32))) + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $2 (func (param (ref null $f)))) + + ;; CHECK: (func $test (type $2) (param $f (ref null $f)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $f + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $f (ref null $f)) + ;; We should not get confused when a parameter is the target of a call_ref. + (call_ref $f + (i32.const 0) + (local.get $f) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $f (func)) + (type $f (func (param i64))) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) + + ;; CHECK: (func $test (type $0) (param $i i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_indirect $t (type $f) + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $i i32) + ;; Same with a call_indirect. + (call_indirect (type $f) + (i64.const 0) + (local.get $i) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func)) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (tag $e (type $1) (param i32)) + (tag $e (param i32)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (try + (do + (nop) + ) + (catch $e + ;; When we optimize out the parameter, the dropped pop will be inside a + ;; new block. This is invalid, so we must fix it up. + (call $callee + (pop i32) + ) + (nop) + ) + ) + ) + + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param $unused i32) + (nop) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "" "" (func $effect (type $0))) + (import "" "" (func $effect)) + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (loop $l + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $0 i32) + ;; Regression test. A previous version of the expression removal code would + ;; incorrectly duplicate the loop label and effect in the output. + (drop + (block (result i32) + (drop + (block (result i32) + (drop + (loop $l (result i32) + (call $effect) + (local.get $0) + ) + ) + (i32.const 0) + ) + ) + (local.get $0) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (result i32))) + + ;; CHECK: (func $test (type $0) (result i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) (result i32) + ;; If we did not remove the If from the set of removed expressions after + ;; processing it once, then when we process it a second time it would fail + ;; the assertion that removed Ifs must have two arms. + (i32.add + (if (result i32) + (unreachable) + (then + (i32.const 0) + ) + (else + (local.get $unused) + ) + ) + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (func $test (type $0) (param $x i32) + ;; CHECK-NEXT: (local $unused (ref any)) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $unused + ;; CHECK-NEXT: (ref.i31 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $test (param $unused (ref any)) (param $x i32) + (if + (local.get $x) + (then + (local.set $unused + (ref.i31 + (i32.const 0) + ) + ) + ) + ) + ;; We can optimize this out even though it might not read from the parameter. + ;; This should not produce validation errors about reads of uninitialized + ;; parameters. + (drop + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (func (param (ref any) i32))) + + ;; CHECK: (type $1 (struct)) + + ;; CHECK: (global $g (mut anyref) (ref.null none)) + (global $g (mut anyref) (ref.null none)) + + ;; CHECK: (func $test (type $0) (param $unused (ref any)) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $unused + ;; CHECK-NEXT: (ref.i31 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $unused) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused (ref any)) (param $x i32) + (if + (local.get $x) + (then + (local.set $unused + (ref.i31 + (i32.const 0) + ) + ) + ) + ) + ;; Same, but now the parameter is actually used and we do not optimize. + (global.set $g + (local.get $unused) + ) + ) +) + +(module + ;; CHECK: (type $struct (struct)) + (type $struct (struct)) + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "" "" (global $g (mut (ref $struct)))) + (import "" "" (global $g (mut (ref $struct)))) + + ;; CHECK: (func $test (type $1) + ;; CHECK-NEXT: (local $a (ref $struct)) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $unused i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.tee $a + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $unused i32) + (local $a (ref $struct)) + (local $1 i32) + ;; If we were to run the non-defaultable local fixup after renumbering + ;; locals but before updating function types, then the fixup code would get + ;; confused and think that this is a get of an uninitialized non-nullable + ;; local because it thinks local index 1 has type (ref $struct). + (drop + (local.get $1) + ) + ;; Set $a just to make the get below valid in the input. + (local.set $a + (unreachable) + ) + ;; This only validates if $a remains non-nullable. If the confused + ;; non-defaultable local fixup runs, this will become invalid. + (global.set $g + (local.get $a) + ) + ) +)