From 1cebca9d7bbfbafb6e48f1e5bd5ea1491e29e255 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 14 Jul 2026 08:14:02 -0700 Subject: [PATCH 1/5] Remove unused results in DAE2 Extend DeadArgumentElimination2.cpp to track and eliminate unused function return values in addition to dead parameters. Generalize the analysis graph by introducing a unified `Location` variant that handles both parameter and result locations across functions and function type trees (`FuncResultLoc` and `TypeResultLoc`). Extend value-flow tracking to follow return values flowing into function returns, including tracking tail calls (`tailCallees` and `tailCalleeTypes`) so that result usage constraints propagate correctly from callees to tail-callers. Update the fixed-point solver to propagate result usage bidirectionally across function implementations and type trees. When a function or type result is determined to be unused, update its signature result to `Type::none` during type rewriting and strip the return expressions and call site result types during optimization. --- src/passes/DeadArgumentElimination2.cpp | 633 +++++++++++++----- test/lit/passes/dae2-results-cont.wast | 101 +++ .../lit/passes/dae2-results-control-flow.wast | 281 ++++++++ test/lit/passes/dae2-results-cycles.wast | 155 +++++ test/lit/passes/dae2-results-indirect.wast | 589 ++++++++++++++++ test/lit/passes/dae2-results-intrinsics.wast | 115 ++++ test/lit/passes/dae2-results-open-world.wast | 80 +++ test/lit/passes/dae2-results-returns.wast | 389 +++++++++++ test/lit/passes/dae2-results.wast | 354 ++++++++++ test/lit/passes/dae2.wast | 213 +++--- 10 files changed, 2619 insertions(+), 291 deletions(-) create mode 100644 test/lit/passes/dae2-results-cont.wast create mode 100644 test/lit/passes/dae2-results-control-flow.wast create mode 100644 test/lit/passes/dae2-results-cycles.wast create mode 100644 test/lit/passes/dae2-results-indirect.wast create mode 100644 test/lit/passes/dae2-results-intrinsics.wast create mode 100644 test/lit/passes/dae2-results-open-world.wast create mode 100644 test/lit/passes/dae2-results-returns.wast create mode 100644 test/lit/passes/dae2-results.wast diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 1ebc576fa9a..075f7770867 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -114,13 +114,28 @@ using Params = std::vector; // Function index and parameter index. using FuncParamLoc = std::pair; +// Function index identifying the function' result (we treat result tuples as a +// single value). +using FuncResultLoc = Index; + // Function type and parameter index. using TypeParamLoc = std::pair; +// Function type identifying the function type's result (we treat result tuples +// as a single value). +using TypeResultLoc = HeapType; + +using Location = + std::variant; + // 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>; +// Map param indices (in the outer vector) to lists of locations. +// TODO: Experiment with using a set in place of the inner vector. +using ParamLocations = std::vector>; + using TypeMap = GlobalTypeRewriter::TypeMap; // Analysis results and call graph information for a single function. @@ -130,32 +145,46 @@ 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; + // Analysis result for the function result. It will remain bot if there is no + // result. + Used::Element resultUsage; + + // Map direct callee function names to the source locations forwarded to their + // parameters. + std::unordered_map forwardedToDirectParams; + + // Map the root supertypes of indirect callee types to the source locations + // forwarded to their parameters. + std::unordered_map forwardedToIndirectParams; + + // Locations forwarded to this function's result. These locations will become + // used if the result turns out ot be used. + std::vector resultSources; - // 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 locations that will become + // used if the parameter turns out to be used. Computed by reversing the + // forwardedToDirectParams graph. + ParamLocations paramSources; - // 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; + // Locations used in an observable way by the code in this function. + // Propagation of usage will begin at these locations. + // TODO: Experiment with making this a set. + std::vector usedLocations; // 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. + // We do not yet analyze parameter or result 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. + // Whether we need to additionally propagate param usage to and result usage + // from 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 @@ -169,6 +198,12 @@ struct FunctionInfo { // 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; + + // Functions that this function directly tail-calls. + std::vector tailCallees; + + // Root function types that this function indirectly tail-calls. + std::vector tailCalleeTypes; }; // Analysis results and call graph information for a tree of related function @@ -178,20 +213,30 @@ struct RootFuncTypeInfo { // For each parameter in the type, whether it is used. Params paramUsages; + // Analysis result for the function result. + Used::Element resultUsage; + // 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 root function type, the list of parameters of - // indirect callers (of any function type in this tree) that become used when - // the parameter in this root function type becomes used. Computed by - // reversing indirectForwardedParams from the function infos. - std::vector> callerParams; + // For each parameter in this root function type, the list of locations that + // become used when the parameter in this root function type becomes used. + // Computed by reversing indirectForwardedParams from the function infos for + // functions with this root type. + ParamLocations paramSources; + + // Tail-callers of this type tree. If this type tree's result is used, + // these callers' results must also be used. Normally, result usage only + // flows from types to their referenced functions, but tail calls require + // this reverse constraint. + std::vector resultSources; RootFuncTypeInfo(Used& used, HeapType type) : paramUsages(type.getSignature().params.size(), used.getBottom()), - callerParams(type.getSignature().params.size()) {} + resultUsage(used.getBottom()), + paramSources(type.getSignature().params.size()) {} }; struct DAE2 : public Pass { @@ -288,16 +333,53 @@ struct DAE2 : public Pass { std::fill(usages.begin(), usages.end(), used.getTop()); } + void markResultsUsed(Index funcIndex) { + funcInfos[funcIndex].resultUsage = used.getTop(); + } + + void markResultsUsed(Name func) { markResultsUsed(funcIndices.at(func)); } + + void markResultsUsed(HeapType rootType) { + getTypeTreeInfo(rootType).resultUsage = used.getTop(); + } + bool join(FuncParamLoc loc, const Used::Element& other) { auto& elem = funcInfos[loc.first].paramUsages[loc.second]; return used.join(elem, other); } + bool join(FuncResultLoc loc, const Used::Element& other) { + auto& elem = funcInfos[loc].resultUsage; + 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); } + + bool join(TypeResultLoc loc, const Used::Element& other) { + assert(loc == getRootType(loc)); + auto& elem = getTypeTreeInfo(loc).resultUsage; + return used.join(elem, other); + } + + bool join(Location loc, const Used::Element& other) { + if (auto* l = std::get_if(&loc)) { + return join(*l, other); + } + if (std::get_if(&loc)) { + return join(std::get(loc), other); + } + if (auto* l = std::get_if(&loc)) { + return join(*l, other); + } + if (std::get_if(&loc)) { + return join(std::get(loc), other); + } + WASM_UNREACHABLE("unexpected loc"); + } }; struct GraphBuilder : public WalkerPass> { @@ -392,13 +474,6 @@ struct GraphBuilder : public WalkerPass> { 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) { @@ -408,64 +483,69 @@ struct GraphBuilder : public WalkerPass> { 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 forwardToDirectParam(Location source, + const ExpressionList& operands, + Expression* arg, + Name target) { + auto argIndex = getArgIndex(operands, arg); + auto& forwarded = funcInfos[index].forwardedToDirectParams[target]; + if (forwarded.empty()) { + forwarded.resize(operands.size()); + } + forwarded[argIndex].push_back(source); } - void handleIndirectForwardedParam(LocalGet* get, - Expression* arg, - const ExpressionList& operands, - HeapType type) { + void forwardToIndirectParam(Location source, + const ExpressionList& operands, + Expression* arg, + 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 (!getFunction()->isParam(curr->index)) { - return; + auto& forwarded = funcInfos[index].forwardedToIndirectParams[rootType]; + if (forwarded.empty()) { + forwarded.resize(operands.size()); } + forwarded[argIndex].push_back(source); + } - // 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; }); + void forwardToResult(Location source) { + funcInfos[index].resultSources.push_back(source); + } - if (!usesParam) { - // The original parameter value does not reach here. + // Record the fact that `curr`'s result comes from `source`. + void getValueFromLocation(Expression* curr, Location source) { + // Look at the transitive users of this value (i.e. its parent and further + // ancestors) to see if it flows (possibly with transformations) into + // another location, `dest`. If it is, we say that `src` is "forwarded" to + // `dest`. We will create an edge in the analysis graph so that if `dest` is + // used, then `src` will be marked as used as well. We must make sure the + // current function doesn't first use `src` in other ways, though, for + // example by teeing it to a local or by performing a branching or trapping + // cast on it. As a conservative approximation, consider the `src` used if + // any of the expressions between `curr` and `dest` have non-removable side + // effects (even if those side effects do not depend on the value flowing + // from `curr`). + if (curr == getFunction()->body) { + // No parents to look at, but the result is forwarded to the function + // result. + forwardToResult(source); 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]; + // TODO: Experiment with caching the location (or lack of location, or + // use) reached from parent expressions so we can avoid traversing the + // same parents more than once. + if (auto* call = parent->dynCast()) { - handleDirectForwardedParam(curr, expr, call); + forwardToDirectParam(source, call->operands, expr, call->target); return; } if (auto* call = parent->dynCast(); call && expr != call->target && optimizeReferencedFuncs) { - handleIndirectForwardedParam( - curr, expr, call->operands, call->heapType); + forwardToIndirectParam(source, call->operands, expr, call->heapType); return; } if (auto* call = parent->dynCast(); @@ -475,24 +555,36 @@ struct GraphBuilder : public WalkerPass> { return; } auto heapType = call->target->type.getHeapType(); - handleIndirectForwardedParam(curr, expr, call->operands, heapType); + forwardToIndirectParam(source, call->operands, expr, heapType); + return; + } + + if (parent->is()) { + forwardToResult(source); return; } - // If the parameter flows into an If condition, we must consider it used + // TODO: Handle unconditional branches to blocks that fall through to the + // end of the function as well? Handle all unconditional branches to + // blocks in general? + + // If the value 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; } + // TODO: Skip the effects analysis below when we are flowing out of a + // block. Side effects earlier in the block don't matter. + // If the current parent expression has unremovable side effects, we - // conservatively treat the parameter as used. + // conservatively treat the value 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. + // Conservatively assume this expression uses the value in some way that + // prevents us from removing it. break; } @@ -500,11 +592,95 @@ struct GraphBuilder : public WalkerPass> { // 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. + + // If the value flows out of the function body, it is forwarded to this + // function's results. + if (parent == getFunction()->body) { + forwardToResult(source); + return; + } + } + + // The value is used by something we aren't analyzing. + if (auto* l = std::get_if(&source)) { + // Parameter uses are local to the function and can be updated in + // parallel. + funcInfos[index].paramUsages[l->second] = used.getTop(); + } else { + // Other locations will have to be handled later. + funcInfos[index].usedLocations.push_back(source); + } + } + + void visitLocalGet(LocalGet* curr) { + if (!getFunction()->isParam(curr->index)) { + 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); + + getValueFromLocation(curr, FuncParamLoc{index, curr->index}); + } + + void visitCall(Call* curr) { + if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) { + auto target = curr->operands.back()->cast()->func; + funcInfos[funcIndices.at(target)].usedInIntrinsic = true; + } + auto* callee = getModule()->getFunction(curr->target); + if (callee->getResults().isConcrete()) { + Location source = FuncResultLoc{funcIndices.at(curr->target)}; + if (curr->isReturn) { + forwardToResult(source); + funcInfos[index].tailCallees.push_back(funcIndices.at(curr->target)); + } else { + getValueFromLocation(curr, source); + } + } + } + + void visitCallIndirect(CallIndirect* curr) { + auto sig = curr->heapType.getSignature(); + if (sig.results.isConcrete()) { + if (curr->isReturn) { + Location source = TypeResultLoc{getRootType(curr->heapType)}; + forwardToResult(source); + funcInfos[index].tailCalleeTypes.push_back(getRootType(curr->heapType)); + } else if (optimizeReferencedFuncs) { + Location source = TypeResultLoc{getRootType(curr->heapType)}; + getValueFromLocation(curr, source); + } + } + } + + void visitCallRef(CallRef* curr) { + if (curr->target->type.isSignature()) { + auto sig = curr->target->type.getHeapType().getSignature(); + if (sig.results.isConcrete()) { + if (curr->isReturn) { + Location source = + TypeResultLoc{getRootType(curr->target->type.getHeapType())}; + forwardToResult(source); + auto heapType = curr->target->type.getHeapType(); + funcInfos[index].tailCalleeTypes.push_back(getRootType(heapType)); + } else if (optimizeReferencedFuncs) { + Location source = + TypeResultLoc{getRootType(curr->target->type.getHeapType())}; + getValueFromLocation(curr, source); + } + } } - // The parameter value is used by something other than a call. - funcInfos[index].paramUsages[curr->index] = used.getTop(); } }; @@ -515,7 +691,8 @@ void DAE2::analyzeModule() { 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); + funcInfos[i].resultUsage = used.getBottom(); + funcInfos[i].paramSources.resize(numParams); } // Analyze functions to find forwarded and used parameters as well as @@ -526,9 +703,26 @@ void DAE2::analyzeModule() { // Find additional function references at the module level. builder.walkModuleCode(wasm); + // Update the locations for which we observed direct usage. + for (Index i = 0; i < wasm->functions.size(); ++i) { + auto& info = funcInfos[i]; + for (auto loc : info.usedLocations) { + if (auto* l = std::get_if(&loc)) { + markResultsUsed(*l); + } else if (auto* l = std::get_if(&loc)) { + markResultsUsed(*l); + } else { + // Function parameter uses were already handled in parallel. It is + // impossible to directly use a type parameter since they are used only + // transitively through function parameters. + WASM_UNREACHABLE("unexpected location"); + } + } + } + // Model imported and exported functions as referenced so that marking the - // parameters of their types as used will prevent optimizations of the - // functions themselves. + // parameters or results 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; @@ -542,26 +736,29 @@ void DAE2::analyzeModule() { } // Functions called with call.without.effects cannot yet be optimized. Mark - // their parameters as used. + // their parameters and results as used. for (Index i = 0; i < wasm->functions.size(); ++i) { if (funcInfos[i].usedInIntrinsic) { markParamsUsed(i); + markResultsUsed(i); } } // JS-called functions will be called externally, so we cannot optimize out - // their parameters. + // their parameters or results. // TODO: Consider optimizing out a suffix of their parameters. for (auto name : Intrinsics(*wasm).getJSCalledFunctions()) { markParamsUsed(name); + markResultsUsed(name); } - // If we're not optimizing referenced functions, mark all their parameters as - // used. + // If we're not optimizing referenced functions, mark all their parameters + // and results as used. if (!optimizeReferencedFuncs) { for (Index i = 0; i < wasm->functions.size(); ++i) { if (funcInfos[i].referenced) { markParamsUsed(i); + markResultsUsed(i); } } } @@ -607,37 +804,54 @@ void DAE2::analyzeModule() { for (auto root : unrewritableRoots) { markParamsUsed(root); + markResultsUsed(root); } } void DAE2::prepareReverseGraph() { // Compute the reverse graph used by the fixed point analysis from the // forward graph we have built. + + // Collect the referenced functions for each type tree. for (Index i = 0; i < funcInfos.size(); ++i) { - funcInfos[i].callerParams.resize(funcInfos[i].paramUsages.size()); + funcInfos[i].paramSources.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}); + auto& callerInfo = funcInfos[callerIndex]; + // Collect the source locations for direct callees. + for (auto& [callee, forwardedParams] : callerInfo.forwardedToDirectParams) { + auto& calleeInfo = funcInfos[funcIndices.at(callee)]; + for (Index destParam = 0; destParam < forwardedParams.size(); + ++destParam) { + for (auto sourceLoc : forwardedParams[destParam]) { + calleeInfo.paramSources[destParam].push_back(sourceLoc); + } } } - for (auto& [calleeRootType, forwarded] : - funcInfos[callerIndex].indirectForwardedParams) { + // Collect the source locations for indirect callees. + for (auto& [calleeRootType, forwardedParams] : + callerInfo.forwardedToIndirectParams) { assert(getRootType(calleeRootType) == calleeRootType); - auto& callerParams = getTypeTreeInfo(calleeRootType).callerParams; - for (auto& [srcParam, destParam] : forwarded) { - assert(destParam < callerParams.size()); - callerParams[destParam].push_back({callerIndex, srcParam}); + auto& typeTreeInfo = getTypeTreeInfo(calleeRootType); + for (Index destParam = 0; destParam < forwardedParams.size(); + ++destParam) { + for (auto sourceLoc : forwardedParams[destParam]) { + typeTreeInfo.paramSources[destParam].push_back(sourceLoc); + } } } + // Collect the tail callers of each callee function and type tree. + Location callerResult = FuncResultLoc{callerIndex}; + for (auto calleeIndex : callerInfo.tailCallees) { + funcInfos[calleeIndex].resultSources.push_back(callerResult); + } + for (auto calleeRootType : callerInfo.tailCalleeTypes) { + getTypeTreeInfo(calleeRootType).resultSources.push_back(callerResult); + } } } @@ -648,18 +862,21 @@ void DAE2::prepareReverseGraph() { // 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; + // List of destination locations (i.e. params and results, either of functions + // or root function types) from which we may need to propagate usage + // information. Initialized with all locations we have observed to be used in + // the IR. + // TODO: Consider propagating by connected components instead. + 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(FuncParamLoc{i, j}); } } + if (funcInfos[i].resultUsage) { + work.push_back(FuncResultLoc{i}); + } } for (auto& [rootType, info] : typeTreeInfos) { for (Index i = 0; i < info.paramUsages.size(); ++i) { @@ -667,59 +884,96 @@ void DAE2::computeFixedPoint() { work.push_back(TypeParamLoc{rootType, i}); } } + if (info.resultUsage) { + work.push_back(TypeResultLoc{rootType}); + } } + while (!work.empty()) { - auto item = work.back(); + auto loc = work.back(); work.pop_back(); - if (auto* loc = std::get_if(&item)) { - auto [rootType, calleeParamIndex] = *loc; + if (auto* l = std::get_if(&loc)) { + auto [rootType, paramIndex] = *l; auto& typeTreeInfo = getTypeTreeInfo(rootType); - const auto& elem = typeTreeInfo.paramUsages[calleeParamIndex]; + const auto& elem = typeTreeInfo.paramUsages[paramIndex]; 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 back to locations forwarded from indirect callers. + for (auto source : typeTreeInfo.paramSources[paramIndex]) { + if (join(source, elem)) { + work.push_back(source); } } // 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) { - FuncParamLoc param = {funcIndex, calleeParamIndex}; + FuncParamLoc param = {funcIndex, paramIndex}; if (join(param, elem)) { work.push_back(param); } } - continue; - } - - if (auto* loc = std::get_if(&item)) { - auto [calleeIndex, calleeParamIndex] = *loc; + } else if (auto* l = std::get_if(&loc)) { + auto& typeTreeInfo = getTypeTreeInfo(*l); + const auto& elem = typeTreeInfo.resultUsage; + assert(elem && "unexpected unused result"); + // 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) { + FuncResultLoc res = funcIndex; + if (join(res, elem)) { + work.push_back(res); + } + } + // Propagate to tail callers. + for (auto source : typeTreeInfo.resultSources) { + if (join(source, elem)) { + work.push_back(source); + } + } + } else if (auto* l = std::get_if(&loc)) { + auto [calleeIndex, calleeParamIndex] = *l; 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); + // Propagate usage back to locations forwarded from direct callers. + for (auto source : calleeInfo.paramSources[calleeParamIndex]) { + if (join(source, elem)) { + work.push_back(source); } } - if (calleeInfo.referenced) { // Propagate the use to the function type. It will be propagated from - // there to indirect callers of this type. + // there to indirect callers and other functions of this type. auto calleeType = wasm->functions[calleeIndex]->type.getHeapType(); TypeParamLoc param = {getRootType(calleeType), calleeParamIndex}; if (join(param, elem)) { work.push_back(param); } } - continue; + } else if (auto* l = std::get_if(&loc)) { + auto calleeIndex = *l; + auto& calleeInfo = funcInfos[calleeIndex]; + const auto& elem = calleeInfo.resultUsage; + assert(elem && "unexpected unused result"); + // Propagate usage back to sources of this result, including tail callers. + for (auto source : calleeInfo.resultSources) { + if (join(source, elem)) { + work.push_back(source); + } + } + if (calleeInfo.referenced) { + // Propagate the use to the function type. It will be propagated from + // there to other functions of this type. + auto calleeType = wasm->functions[calleeIndex]->type.getHeapType(); + TypeResultLoc res = getRootType(calleeType); + if (join(res, elem)) { + work.push_back(res); + } + } + } else { + WASM_UNREACHABLE("unexpected location"); } - WASM_UNREACHABLE("unexpected item"); } } @@ -736,7 +990,8 @@ struct DAETypeUpdater : GlobalTypeRewriter { // 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; + auto& info = parent.getTypeTreeInfo(getRootType(oldType)); + auto& usages = info.paramUsages; bool hasRemoved = std::any_of( usages.begin(), usages.end(), [&](auto& use) { return !use; }); if (hasRemoved) { @@ -749,6 +1004,9 @@ struct DAETypeUpdater : GlobalTypeRewriter { } sig.params = getTempTupleType(std::move(keptParams)); } + if (!info.resultUsage) { + sig.results = Type::none; + } } // Return the sorted list of old types (used for deterministic ordering) and @@ -825,11 +1083,13 @@ struct Optimizer auto& paramUsages = funcInfo->paramUsages; assert(paramUsages.size() == numParams); - bool hasRemoved = std::any_of( + bool hasRemovedParam = std::any_of( paramUsages.begin(), paramUsages.end(), [&](auto& use) { return !use; }); + bool hasRemovedResult = + !funcInfo->resultUsage && func->getResults().isConcrete(); bool hasNewNonNullableLocal = false; - if (hasRemoved) { + if (hasRemovedParam || hasRemovedResult) { // Remove the parameters and replace them with scratch locals. Map the // indices of removed params to their types and names so we can create // properly typed and named scratch locals later. @@ -864,10 +1124,11 @@ struct Optimizer 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()); + // parameters and result. 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), + funcInfo->resultUsage ? func->getResults() : Type::none); TypeBuilder builder(1); builder[0] = sig; builder[0].setShared(originalType.getHeapType().getShared()); @@ -891,6 +1152,8 @@ struct Optimizer Super::runOnFunction(wasm, func); + assert(!hasRemovedResult || !func->body->type.isConcrete()); + // We may have moved pops around. Fix them. EHUtils::handleBlockNestedPops(func, *wasm); @@ -912,6 +1175,28 @@ struct Optimizer } } + // Mark the parents of the current expression for removal until they either + // stop propagating the unused value or reach a call parameter or function + // result. + void markParentsRemoved() { + for (Index i = expressionStack.size() - 1; i > 0; --i) { + auto* parent = expressionStack[i - 1]; + if (parent->is() || parent->is() || + parent->is() || parent->is()) { + break; + } + if (!removedExpressions.insert(parent).second) { + // This expression, and therefore its relevant parents, have already + // been marked, so we do not need to continue. + break; + } + if (!parent->type.isConcrete()) { + // The value does not flow further, so stop removing. + break; + } + } + } + void visitLocalSet(LocalSet* curr) { curr->index = newIndices[curr->index]; } void visitLocalGet(LocalGet* curr) { @@ -923,22 +1208,8 @@ struct Optimizer if (curr->index < funcInfo->paramUsages.size() && !funcInfo->paramUsages[curr->index] && funcInfo->paramGets.contains(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()) { - // The value does not flow further, so stop removing. - break; - } - } + removedExpressions.insert(curr); + markParentsRemoved(); } curr->index = newIndices[curr->index]; } @@ -948,6 +1219,22 @@ struct Optimizer // non-concrete type because its value will have been removed. Expression* getReplacement(Expression* curr); + // Given an expression, return its replacement or a nop if it is marked for + // removal, or otherwise return it dropped. + Expression* getReplacementOrDrop(Expression* curr) { + Builder builder(*getModule()); + if (removedExpressions.contains(curr)) { + if (auto* replacement = getReplacement(curr)) { + return replacement; + } + return builder.makeNop(); + } + if (curr->type.isConcrete()) { + return builder.makeDrop(curr); + } + return curr; + } + // Remove operands from any kind of call (i.e. Call, CallIndirect, or // CallRef), given the call's list of operands and analysis results for those // operands. @@ -1012,10 +1299,12 @@ struct Optimizer } void visitCall(Call* curr) { - removeOperands( - curr, - curr->operands, - parent.funcInfos[parent.funcIndices.at(curr->target)].paramUsages); + auto& calleeInfo = parent.funcInfos[parent.funcIndices.at(curr->target)]; + if (!calleeInfo.resultUsage && curr->type.isConcrete()) { + curr->type = Type::none; + markParentsRemoved(); + } + removeOperands(curr, curr->operands, calleeInfo.paramUsages); } void handleIndirectCall(Expression* curr, @@ -1026,9 +1315,18 @@ struct Optimizer } 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. + // No analysis results for this type, so none of its parameters or results + // are used. + if (curr->type.isConcrete()) { + curr->type = Type::none; + markParentsRemoved(); + } removeOperands(curr, operands, {}); } else { + if (!it->second.resultUsage && curr->type.isConcrete()) { + curr->type = Type::none; + markParentsRemoved(); + } removeOperands(curr, operands, it->second.paramUsages); } } @@ -1044,6 +1342,18 @@ struct Optimizer handleIndirectCall(curr, curr->operands, curr->heapType); } + void visitReturn(Return* curr) { + if (curr->value && !funcInfo->resultUsage) { + // We are removing this function's unused result. Remove or drop the + // original value (depending on whether or not the value uses another + // removed location) and then return. + Builder builder(*getModule()); + replaceCurrent( + builder.makeSequence(getReplacementOrDrop(curr->value), curr)); + curr->value = nullptr; + } + } + void visitExpression(Expression* curr) { if (curr->type.isConcrete()) { // If this expression should be removed, that will be handled by the call @@ -1069,6 +1379,13 @@ struct Optimizer // There are no kept children. builder.replaceWithIdenticalType(curr); } + + void visitFunction(Function* func) { + // Remove the function result if necessary. + if (func->body->type.isConcrete() && !funcInfo->resultUsage) { + func->body = getReplacementOrDrop(func->body); + } + } }; Expression* Optimizer::getReplacement(Expression* curr) { @@ -1429,8 +1746,12 @@ void DAE2::makeUnreferencedFunctionTypes(const std::vector& oldTypes, } } - auto newSig = - updateSignature({Type(std::move(keptParams)), func->getResults()}); + Type keptResults = func->getResults(); + if (!funcInfos[i].resultUsage) { + keptResults = Type::none; + } + + auto newSig = updateSignature({Type(std::move(keptParams)), keptResults}); // Look up or create a replacement type that will be rewritten to the // desired signature. diff --git a/test/lit/passes/dae2-results-cont.wast b/test/lit/passes/dae2-results-cont.wast new file mode 100644 index 00000000000..e02ce7c2cd9 --- /dev/null +++ b/test/lit/passes/dae2-results-cont.wast @@ -0,0 +1,101 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sig2 (func (result i32))) + + ;; CHECK: (type $cont2 (cont $sig2)) + + ;; CHECK: (type $cont1 (cont $sig1)) + + ;; CHECK: (type $sig1 (func (param i32) (result i32))) + (type $sig1 (func (param i32) (result i32))) + (type $sig2 (func (result i32))) + (type $cont1 (cont $sig1)) + (type $cont2 (cont $sig2)) + + ;; CHECK: (func $target (type $3) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (type $sig1) (param i32) (result i32) + ;; Even though $sig1 is used in a continuation, $target is unreferenced, + ;; so DAE2 can optimize it by giving it a new type. + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $5) (param $k (ref $cont1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref $cont2)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (cont.bind $cont1 $cont2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $k (ref $cont1)) (result (ref $cont2)) + (drop (call $target (i32.const 0))) + (cont.bind $cont1 $cont2 (i32.const 0) (local.get $k)) + ) +) + +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sig2 (func (result i32))) + + ;; CHECK: (type $cont2 (cont $sig2)) + + ;; CHECK: (type $cont1 (cont $sig1)) + + ;; CHECK: (type $sig1 (func (param i32) (result i32))) + (type $sig1 (func (param i32) (result i32))) + (type $sig2 (func (result i32))) + (type $cont1 (cont $sig1)) + (type $cont2 (cont $sig2)) + + ;; CHECK: (elem declare func $target) + (elem declare func $target) + + ;; CHECK: (func $target (type $sig1) (param $0 i32) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $target (type $sig1) (param i32) (result i32) + ;; Same, but $target is referenced, so we cannot change its type. Because + ;; $sig1 is used in a continuation, we cannot yet optimize it. + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $5) (param $k (ref $cont1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref $cont2)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (cont.bind $cont1 $cont2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $k) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $k (ref $cont1)) (result (ref $cont2)) + (drop (call $target (i32.const 0))) + (drop (ref.func $target)) + (cont.bind $cont1 $cont2 (i32.const 0) (local.get $k)) + ) +) diff --git a/test/lit/passes/dae2-results-control-flow.wast b/test/lit/passes/dae2-results-control-flow.wast new file mode 100644 index 00000000000..32baff3f72e --- /dev/null +++ b/test/lit/passes/dae2-results-control-flow.wast @@ -0,0 +1,281 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (func $div (type $0) (param $x i32) (param $y i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.div_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $div (param $x i32) (param $y i32) (result i32) + ;; The possible trap prevents us from removing the parameters, but we can + ;; still remove the result. + (i32.div_s (local.get $x) (local.get $y)) + ) + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (call $div + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $div (i32.const 1) (i32.const 0))) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $div (type $0) (param $x i32) (param $y i32) (result i32) + ;; CHECK-NEXT: (i32.div_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $div (param $x i32) (param $y i32) (result i32) + (i32.div_s (local.get $x) (local.get $y)) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $div + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used, too. + (global.set $g (call $div (i32.const 1) (i32.const 0))) + ) +) + +(module + ;; CHECK: (import "env" "side_effect" (func $side_effect (type $0))) + (import "env" "side_effect" (func $side_effect)) + ;; CHECK: (func $test (type $1) (param $cond i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $side_effect) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $cond i32) (result i32) + ;; The parameter is used, but we can still remove the result. + (if (result i32) + (local.get $cond) + (then + (block (result i32) + (call $side_effect) + (i32.const 1) + ) + ) + (else + (i32.const 2) + ) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $test (i32.const 1))) + ) +) + +(module + ;; CHECK: (import "env" "side_effect" (func $side_effect (type $0))) + (import "env" "side_effect" (func $side_effect)) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $test (type $1) (param $cond i32) (result i32) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $side_effect) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $cond i32) (result i32) + (if (result i32) (local.get $cond) + (then + (block (result i32) + (call $side_effect) + (i32.const 1) + ) + ) + (else + (i32.const 2) + ) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used. + (global.set $g (call $test (i32.const 1))) + ) +) + +(module + ;; CHECK: (func $test (type $0) (param $cond i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $label (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $label + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $cond i32) (result i32) + ;; We can drop the unused result returned via a labeled block. + (block $label (result i32) + (drop + (br_if $label (i32.const 42) (local.get $cond)) + ) + (i32.const 100) + ) + ) + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $test (i32.const 1))) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $test (type $0) (param $cond i32) (result i32) + ;; CHECK-NEXT: (block $label (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $label + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $cond i32) (result i32) + (block $label (result i32) + (drop + (br_if $label (i32.const 42) (local.get $cond)) + ) + (i32.const 100) + ) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used. + (global.set $g (call $test (i32.const 1))) + ) +) + +(module + ;; CHECK: (import "env" "side_effect" (func $side_effect (type $0))) + (import "env" "side_effect" (func $side_effect)) + ;; CHECK: (tag $tag (type $0)) + (tag $tag) + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (try (result i32) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (call $side_effect) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32) + ;; We can drop an unused result returned via a try-catch. + (try (result i32) + (do (i32.const 1)) + (catch $tag + (call $side_effect) + (i32.const 2) + ) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $test)) + ) +) + +(module + ;; CHECK: (import "env" "side_effect" (func $side_effect (type $0))) + (import "env" "side_effect" (func $side_effect)) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (tag $tag (type $0)) + (tag $tag) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $test (type $1) (result i32) + ;; CHECK-NEXT: (try (result i32) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (call $side_effect) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32) + (try (result i32) + (do (i32.const 1)) + (catch $tag + (call $side_effect) + (i32.const 2) + ) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used. + (global.set $g (call $test)) + ) +) diff --git a/test/lit/passes/dae2-results-cycles.wast b/test/lit/passes/dae2-results-cycles.wast new file mode 100644 index 00000000000..efe7665b528 --- /dev/null +++ b/test/lit/passes/dae2-results-cycles.wast @@ -0,0 +1,155 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (func $a (type $0) + ;; CHECK-NEXT: (call $b) + ;; CHECK-NEXT: ) + (func $a (result i32) + (call $b) + ) + ;; CHECK: (func $b (type $0) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $b (result i32) + (call $a) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $caller + ;; Despite the cycle, the results of $a and $b are unused and can be + ;; removed. + (drop (call $a)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $a (type $0) (result i32) + ;; CHECK-NEXT: (call $b) + ;; CHECK-NEXT: ) + (func $a (result i32) + (call $b) + ) + ;; CHECK: (func $b (type $0) (result i32) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $b (result i32) + (call $a) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Now the result of $a is used, so the result of $b is as well. + (global.set $g (call $a)) + ) +) + +(module + ;; CHECK: (func $a (type $0) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $a (result i32) + ;; Self-recursion: the result can be removed. + (call $a) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $a)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $a (type $0) (result i32) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $a (result i32) + (call $a) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used. + (global.set $g (call $a)) + ) +) + +(module + ;; CHECK: (func $a (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $a (param i32) (result i32) + (i32.add + (call $a + (call $a + (local.get 0) + ) + ) + (local.get 0) + ) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $caller + ;; Cycles between params and results can still be removed. + (drop (call $a (i32.const 0))) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $a (type $0) (param $0 i32) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (call $a + ;; CHECK-NEXT: (call $a + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $a (param i32) (result i32) + (i32.add + (call $a + (call $a + (local.get 0) + ) + ) + (local.get 0) + ) + ) + + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $a + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now the result is used so nothing can be removed. + (global.set $g (call $a (i32.const 0))) + ) +) diff --git a/test/lit/passes/dae2-results-indirect.wast b/test/lit/passes/dae2-results-indirect.wast new file mode 100644 index 00000000000..66dadf17303 --- /dev/null +++ b/test/lit/passes/dae2-results-indirect.wast @@ -0,0 +1,589 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (type $sig (func)) + (type $sig (func (result i32))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $test)) + + ;; CHECK: (func $test (type $sig) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $sig) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Call via call_indirect, result unused + (drop + (call_indirect (type $sig) + (i32.const 0) + ) + ) + ;; Call via call_ref, result unused + (drop + (call_ref $sig + (ref.func $test) + ) + ) + ) +) + +(module + ;; CHECK: (type $sig (func)) + (type $sig (func (result i32 i64))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $test)) + + ;; CHECK: (func $test (type $sig) + ;; CHECK-NEXT: (tuple.drop 2 + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i64.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (type $sig) (result i32 i64) + (tuple.make 2 (i32.const 42) (i64.const 100)) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Call via call_indirect, result unused + (tuple.drop 2 + (call_indirect (type $sig) + (i32.const 0) + ) + ) + ;; Call via call_ref, result unused + (tuple.drop 2 + (call_ref $sig + (ref.func $test) + ) + ) + ) +) + +(module + ;; CHECK: (type $sig (func)) + (type $sig (func (param i32) (result i32))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $callee)) + + ;; CHECK: (func $callee (type $sig) + ;; CHECK-NEXT: (local $p i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (type $sig) (param $p i32) (result i32) + ;; The param is unused because the result is unused. + (local.get $p) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Call via call_indirect, result unused + (drop + (call_indirect (type $sig) + (i32.const 42) + (i32.const 0) + ) + ) + ;; Call via call_ref, result unused + (drop + (call_ref $sig + (i32.const 42) + (ref.func $callee) + ) + ) + ) +) + +(module + ;; CHECK: (type $sig (func)) + (type $sig (func (param i32) (result i32))) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $callee)) + + ;; CHECK: (func $callee (type $sig) + ;; CHECK-NEXT: (local $p i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (type $sig) (param $p i32) (result i32) + (local.get $p) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (local $p i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (type $sig) (param $p i32) (result i32) + ;; $caller's param flows to $callee, and $caller's result is unused. + ;; Both caller and callee are called indirectly or via ref. + (drop + (call_indirect (type $sig) + (local.get $p) + (i32.const 0) + ) + ) + (drop + (call_ref $sig + (local.get $p) + (ref.func $callee) + ) + ) + (i32.const 0) + ) + + ;; CHECK: (func $main (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $caller) + ;; CHECK-NEXT: ) + (func $main + (drop + (call $caller (i32.const 100)) + ) + ) +) + +(module + ;; CHECK: (type $sig (func)) + (type $sig (func (result i32))) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $other $callee)) + + ;; CHECK: (func $other (type $sig) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $other (type $sig) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $callee (type $sig) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $callee (type $sig) (result i32) + ;; call via call_ref + (call_ref $sig (ref.func $other)) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; call $callee via call_indirect, result unused. Since all referenced + ;; functions of type $sig have unused parameters, we can optimize them. + (drop + (call_indirect (type $sig) + (i32.const 1) + ) + ) + ) +) + +(module + ;; CHECK: (type $sig (sub (func (result i32)))) + (type $sig (sub (func (result i32)))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $f1 $f2)) + + ;; CHECK: (func $f1 (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f1 (type $sig) (result i32) + (i32.const 1) + ) + + ;; CHECK: (func $f2 (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $f2 (type $sig) (result i32) + (i32.const 2) + ) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $f1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $f2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; $f1's result is unused + (drop + (call_ref $sig (ref.func $f1)) + ) + ;; $f2's result is used, inhibiting optimization for the whole type tree + (global.set $g + (call_ref $sig (ref.func $f2)) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (result i32)))) + (type $super (sub (func (result i32)))) + ;; CHECK: (type $sub (sub $super (func (result i32)))) + (type $sub (sub $super (func (result i32)))) + ) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $f_super $f_sub)) + + ;; CHECK: (func $f_super (type $super) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f_super (type $super) (result i32) + (i32.const 1) + ) + + ;; CHECK: (func $f_sub (type $sub) (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $f_sub (type $sub) (result i32) + (i32.const 2) + ) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (ref.func $f_super) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $sub + ;; CHECK-NEXT: (ref.func $f_sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; $f_super's result is used, inhibiting super + (global.set $g + (call_ref $super (ref.func $f_super)) + ) + ;; $f_sub's result is unused, but should be inhibited by super + (drop + (call_ref $sub (ref.func $f_sub)) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (result i32)))) + (type $super (sub (func (result i32)))) + ;; CHECK: (type $sub (sub $super (func (result i32)))) + (type $sub (sub $super (func (result i32)))) + ) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $f_super $f_sub)) + + ;; CHECK: (func $f_super (type $super) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f_super (type $super) (result i32) + (i32.const 1) + ) + + ;; CHECK: (func $f_sub (type $sub) (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $f_sub (type $sub) (result i32) + (i32.const 2) + ) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (ref.func $f_super) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $sub + ;; CHECK-NEXT: (ref.func $f_sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; $f_super's result is unused + (drop + (call_ref $super (ref.func $f_super)) + ) + ;; $f_sub's result is used, inhibiting super as well + (global.set $g + (call_ref $sub (ref.func $f_sub)) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (func (result i32)))) + (type $super (sub (func (result i32)))) + ;; CHECK: (type $sub2 (sub $super (func (result i32)))) + + ;; CHECK: (type $sub1 (sub $super (func (result i32)))) + (type $sub1 (sub $super (func (result i32)))) + (type $sub2 (sub $super (func (result i32)))) + ) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t funcref (elem $f_sub1 $f_sub2)) + + ;; CHECK: (func $f_sub1 (type $sub1) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f_sub1 (type $sub1) (result i32) + (i32.const 1) + ) + + ;; CHECK: (func $f_sub2 (type $sub2) (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $f_sub2 (type $sub2) (result i32) + (i32.const 2) + ) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $sub1 + ;; CHECK-NEXT: (ref.func $f_sub1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $sub2 + ;; CHECK-NEXT: (ref.func $f_sub2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; $f_sub1's result is used + (global.set $g + (call_ref $sub1 (ref.func $f_sub1)) + ) + ;; $f_sub2's result is unused, but inhibited by sibling via super + (drop + (call_ref $sub2 (ref.func $f_sub2)) + ) + ) +) + +(module + ;; CHECK: (type $sig (sub (func (result i32)))) + (type $sig (sub (func (result i32)))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 1 1 funcref) + (table $t funcref (elem $f_ref)) + + ;; $f_ref is referenced (address taken in table) + ;; CHECK: (func $f_ref (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f_ref (type $sig) (result i32) + (i32.const 1) + ) + + ;; $f_unref is NOT referenced (address not taken, only called directly) + ;; CHECK: (func $f_unref (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f_unref (type $sig) (result i32) + (i32.const 2) + ) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (ref.func $f_ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $f_unref) + ;; CHECK-NEXT: ) + (func $caller + ;; $f_ref's result is used, inhibiting $f_ref and $sig + (global.set $g + (call_ref $sig (ref.func $f_ref)) + ) + ;; $f_unref is called directly, its result is unused. + ;; It should be optimized even though $sig is inhibited. + (drop + (call $f_unref) + ) + ) +) + +(module + ;; CHECK: (type $caller (sub (func (param (ref $callee))))) + + ;; CHECK: (type $callee (sub (func))) + (type $callee (sub (func (result i32)))) + (type $caller (sub (func (param (ref $callee)) (result i32)))) + + ;; CHECK: (func $callee (type $callee) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $callee (type $callee) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $caller) (param $callee (ref $callee)) + ;; CHECK-NEXT: (return_call_ref $callee + ;; CHECK-NEXT: (local.get $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (type $caller) (param $callee (ref $callee)) (result i32) + (return_call_ref $callee (local.get $callee)) + ) + + ;; CHECK: (func $main (type $0) + ;; CHECK-NEXT: (call_ref $caller + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: (ref.func $caller) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $main + ;; The results are unused, so we can optimize both types. + (drop (call_ref $caller (ref.func $callee) (ref.func $caller))) + ) +) + +(module + ;; CHECK: (type $caller (sub (func (param (ref $callee)) (result i32)))) + + ;; CHECK: (type $callee (sub (func (result i32)))) + (type $callee (sub (func (result i32)))) + (type $caller (sub (func (param (ref $callee)) (result i32)))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $callee (type $callee) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $callee (type $callee) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $caller) (param $callee (ref $callee)) (result i32) + ;; CHECK-NEXT: (return_call_ref $callee + ;; CHECK-NEXT: (local.get $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (type $caller) (param $callee (ref $callee)) (result i32) + (return_call_ref $callee (local.get $callee)) + ) + + ;; CHECK: (func $main (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $caller + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: (ref.func $caller) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $main + ;; Same, but now the result of $callee is used separately. Because of the + ;; tail call, $caller cannot be optimized, either. + (drop (call_ref $caller (ref.func $callee) (ref.func $caller))) + (global.set $g (call $callee)) + ) +) diff --git a/test/lit/passes/dae2-results-intrinsics.wast b/test/lit/passes/dae2-results-intrinsics.wast new file mode 100644 index 00000000000..55463a709a2 --- /dev/null +++ b/test/lit/passes/dae2-results-intrinsics.wast @@ -0,0 +1,115 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe (type $2) (param funcref) (result i32))) + (import "binaryen-intrinsics" "call.without.effects" (func $cwe (param funcref) (result i32))) + ;; CHECK: (func $target (type $1) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 42)) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $cwe + ;; CHECK-NEXT: (ref.func $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; We cannot optimize function called via intrinsics. + (drop (call $cwe (ref.func $target))) + ) +) + +;; Test 8.2: JS-Called via configureAll +(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: (type $sig (func (result i32))) + + ;; 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))) + (type $sig (func (result i32))) + + ;; 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 (item (ref.null noextern))) + ;; CHECK: (elem $funcs func $target) + (elem $funcs funcref (ref.func $target)) + + ;; CHECK: (start $start) + + ;; CHECK: (func $target (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $target (type $sig) (result i32) + ;; This function is JS-called via configureAll. We cannot optimize it. + (i32.const 42) + ) + + (start $start) + ;; CHECK: (func $start (type $3) + ;; 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 $caller (type $3) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $target)) + ) +) + +(module + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe (type $1) (param funcref) (result i32))) + (import "binaryen-intrinsics" "call.without.effects" (func $cwe (param funcref) (result i32))) + ;; CHECK: (@binaryen.js.called) + ;; CHECK-NEXT: (func $target (type $2) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (@binaryen.js.called) + (func $target (result i32) + ;; Now the function is explicitly marked JS-called. We cannot optimize. + (i32.const 42) + ) +) diff --git a/test/lit/passes/dae2-results-open-world.wast b/test/lit/passes/dae2-results-open-world.wast new file mode 100644 index 00000000000..d0a58a5178c --- /dev/null +++ b/test/lit/passes/dae2-results-open-world.wast @@ -0,0 +1,80 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 -S -o - | filecheck %s --check-prefix=OPEN +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s --check-prefix=CLOSED + +(module + ;; OPEN: (func $target (type $0) (result i32) + ;; OPEN-NEXT: (i32.const 42) + ;; OPEN-NEXT: ) + ;; CLOSED: (func $target (type $0) (result i32) + ;; CLOSED-NEXT: (i32.const 42) + ;; CLOSED-NEXT: ) + (func $target (export "target") (result i32) + ;; The result is unused, but the function is exported, so we must keep it + ;; in both open and closed worlds. + (i32.const 42) + ) + ;; OPEN: (func $caller (type $1) + ;; OPEN-NEXT: (drop + ;; OPEN-NEXT: (call $target) + ;; OPEN-NEXT: ) + ;; OPEN-NEXT: ) + ;; CLOSED: (func $caller (type $1) + ;; CLOSED-NEXT: (drop + ;; CLOSED-NEXT: (call $target) + ;; CLOSED-NEXT: ) + ;; CLOSED-NEXT: ) + (func $caller (export "caller") + (drop (call $target)) + ) +) + +(module + ;; OPEN: (import "env" "target" (func $target (type $0) (result i32))) + ;; CLOSED: (import "env" "target" (func $target (type $0) (result i32))) + (import "env" "target" (func $target (result i32))) + ;; OPEN: (func $caller (type $1) + ;; OPEN-NEXT: (drop + ;; OPEN-NEXT: (call $target) + ;; OPEN-NEXT: ) + ;; OPEN-NEXT: ) + ;; CLOSED: (func $caller (type $1) + ;; CLOSED-NEXT: (drop + ;; CLOSED-NEXT: (call $target) + ;; CLOSED-NEXT: ) + ;; CLOSED-NEXT: ) + (func $caller (export "caller") + ;; Same, but with an imported function. + (drop (call $target)) + ) +) + +(module + ;; OPEN: (type $sig (func (result i32))) + ;; CLOSED: (type $sig (func)) + (type $sig (func (result i32))) + ;; OPEN: (func $target (type $sig) (result i32) + ;; OPEN-NEXT: (i32.const 42) + ;; OPEN-NEXT: ) + ;; CLOSED: (func $target (type $sig) + ;; CLOSED-NEXT: (drop + ;; CLOSED-NEXT: (i32.const 42) + ;; CLOSED-NEXT: ) + ;; CLOSED-NEXT: ) + (func $target (type $sig) (result i32) (i32.const 42)) + ;; OPEN: (func $caller (type $1) + ;; OPEN-NEXT: (drop + ;; OPEN-NEXT: (ref.func $target) + ;; OPEN-NEXT: ) + ;; OPEN-NEXT: ) + ;; CLOSED: (func $caller (type $0) + ;; CLOSED-NEXT: (drop + ;; CLOSED-NEXT: (ref.func $target) + ;; CLOSED-NEXT: ) + ;; CLOSED-NEXT: ) + (func $caller + ;; $target is referenced and $sig's result is unused. We can optimize in + ;; closed world but not open world. + (drop (ref.func $target)) + ) +) diff --git a/test/lit/passes/dae2-results-returns.wast b/test/lit/passes/dae2-results-returns.wast new file mode 100644 index 00000000000..58e3ddb428f --- /dev/null +++ b/test/lit/passes/dae2-results-returns.wast @@ -0,0 +1,389 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (func $fallthrough (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $fallthrough (result i32) + (i32.const 1) + ) + ;; CHECK: (func $explicit (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + (func $explicit (result i32) + (return (i32.const 2)) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $fallthrough) + ;; CHECK-NEXT: (call $explicit) + ;; CHECK-NEXT: ) + (func $caller + ;; We should be able to remove results with or without explicit returns. + (drop (call $fallthrough)) + (drop (call $explicit)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $fallthrough (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $fallthrough (result i32) + (i32.const 1) + ) + ;; CHECK: (func $explicit (type $0) (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $explicit (result i32) + (return (i32.const 2)) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (call $fallthrough) + ;; CHECK-NEXT: (call $explicit) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same setup, but now the results are used. + (global.set $g + (i32.add + (call $fallthrough) + (call $explicit) + ) + ) + ) +) + +(module + ;; CHECK: (func $deep (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $deep (result i32) + ;; We should be able to remove both explicit and implicit returns in the + ;; same function. + (i32.add + (block (result i32) + (return (i32.const 42)) + ) + (i32.const 1) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $deep) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $deep)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $deep (type $0) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $deep (result i32) + (i32.add + (block (result i32) + (return (i32.const 42)) + ) + (i32.const 1) + ) + ) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $deep) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but the result is used. + (global.set $g (call $deep)) + ) +) + +(module + ;; CHECK: (func $b (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $b (result i32) + (i32.const 42) + ) + ;; CHECK: (func $a (type $0) + ;; CHECK-NEXT: (return_call $b) + ;; CHECK-NEXT: ) + (func $a (result i32) + (return_call $b) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + (func $caller + ;; $a's result is not used, which means $b's result is not used. + (drop (call $a)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $b (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $b (result i32) + (i32.const 42) + ) + ;; CHECK: (func $a (type $0) (result i32) + ;; CHECK-NEXT: (return_call $b) + ;; CHECK-NEXT: ) + (func $a (result i32) + (return_call $b) + ) + ;; CHECK: (func $caller1 (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller1 + ;; $a's result is not used, but now $b's result is used. Since $a tail calls + ;; $b, it cannot just drop $b's result, so we cannot remove $a's result + ;; either, even though it is unused. + (drop (call $a)) + ) + ;; CHECK: (func $caller2 (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller2 + (global.set $g (call $b)) + ) +) + +(module + ;; CHECK: (type $sig (func (result i32))) + (type $sig (func (result i32))) + (table 1 funcref) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $b (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $b (type $sig) (result i32) + (i32.const 42) + ) + ;; CHECK: (func $a (type $sig) (result i32) + ;; CHECK-NEXT: (return_call_indirect $0 (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $a (result i32) + (return_call_indirect (type $sig) (i32.const 0)) + ) + ;; CHECK: (func $caller1 (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller1 + ;; Same, but now with indirect tail calls. $sig's result is used in + ;; $caller2, so we cannot remove the result from $a, even though it is + ;; unused. + (drop (call $a)) + ) + ;; CHECK: (func $caller2 (type $0) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_indirect $0 (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller2 + (global.set $g (call_indirect (type $sig) (i32.const 0))) + ) +) + +(module + ;; CHECK: (type $sig (func (result i32))) + (type $sig (func (result i32))) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $b (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $b (type $sig) (result i32) (i32.const 42)) + ;; CHECK: (func $a (type $4) (param $ref (ref $sig)) (result i32) + ;; CHECK-NEXT: (return_call_ref $sig + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $a (param $ref (ref $sig)) (result i32) + (return_call_ref $sig (local.get $ref)) + ) + ;; CHECK: (func $caller1 (type $3) (param $ref (ref $sig)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $a + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller1 (param $ref (ref $sig)) + ;; Same, but with return_call_ref. + (drop (call $a (local.get $ref))) + ) + ;; CHECK: (func $caller2 (type $3) (param $ref (ref $sig)) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller2 (param $ref (ref $sig)) + (global.set $g (call_ref $sig (local.get $ref))) + ) +) + +(module + ;; CHECK: (type $sig (func (result i32))) + (type $sig (func (result i32))) + (table 1 funcref) + (elem (i32.const 0) $callee) + ;; CHECK: (func $callee (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $callee (export "callee") (type $sig) (result i32) + (i32.const 42) + ) + ;; CHECK: (func $caller (type $sig) (result i32) + ;; CHECK-NEXT: (return_call_indirect $0 (type $sig) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (type $sig) (result i32) + ;; Now the reason we can't remove $sig's result is that $sig is public. We + ;; cannot remove $caller's result, either. + (return_call_indirect (type $sig) + (i32.const 0) + ) + ) +) + +(module + ;; CHECK: (type $sig (sub (func (result i32)))) + (type $sig (sub (func (result i32)))) + ;; CHECK: (func $callee (type $sig) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $callee (export "callee") (type $sig) (result i32) + (i32.const 42) + ) + ;; CHECK: (func $caller (type $sig) (result i32) + ;; CHECK-NEXT: (return_call_ref $sig + ;; CHECK-NEXT: (ref.func $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (type $sig) (result i32) + ;; Same, but with return_call_ref. + (return_call_ref $sig + (ref.func $callee) + ) + ) +) + +(module + ;; CHECK: (global $g (mut f64) (f64.const 0)) + (global $g (mut f64) (f64.const 0)) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller (result f64) + ;; Fallthrough call where callee and caller return types are eliminated. + ;; DAE2 should not generate a drop of the call's none type. + (call $callee + (global.get $g) + ) + ) + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $0 f64) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $callee (param f64) (result f64) + (unreachable) + ) +) + +(module + ;; CHECK: (global $g (mut f64) (f64.const 0)) + (global $g (mut f64) (f64.const 0)) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result f64) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (result f64) + ;; Same, but with an explicit return after an unreachable. + (unreachable) + (return + (call $callee + (global.get $g) + ) + ) + ) + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $0 f64) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $callee (param f64) (result f64) + (unreachable) + ) +) diff --git a/test/lit/passes/dae2-results.wast b/test/lit/passes/dae2-results.wast new file mode 100644 index 00000000000..b98916b4fda --- /dev/null +++ b/test/lit/passes/dae2-results.wast @@ -0,0 +1,354 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --dae2 --closed-world -S -o - | filecheck %s + +(module + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32) + ;; Unused result should be removed. + (i32.const 42) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + (func $caller + (drop (call $test)) + ) +) + +(module + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32) + ;; Same, but this time there is no caller at all. + (i32.const 42) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i64.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32 i64) + ;; One of the results is unused. Since we don't support partial result + ;; removal, we keep both. + (tuple.make 2 (i32.const 42) (i64.const 100)) + ) + + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (global.set $g (tuple.extract 2 0 (call $test))) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (tuple.drop 2 + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i64.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result i32 i64) + ;; Fully unused tuple results can still be removed. + (tuple.make 2 (i32.const 42) (i64.const 100)) + ) +) + +(module + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param i32) (result i32) + (local.get 0) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller + ;; $callee's result is unused, so its parameter is also unused. + (drop (call $callee (i32.const 42))) + ) +) + +(module + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param i32) (result i32) + (local.get 0) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller (param i32) + ;; Same, but now the $caller further has a param that is unused. + (drop (call $callee (local.get 0))) + ) +) + +(module + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $callee (param i32) (result i32) + (block (result i32) + (block (result i32) + (local.get 0) + ) + ) + ) + + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller (param i32) + ;; Same, but now with intermediate expressions. + (drop + (block (result i32) + (call $callee + (block (result i32) + (local.get 0) + ) + ) + ) + ) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $callee (type $0) (param $0 i32) (result i32) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + (func $callee (param i32) (result i32) + (local.get 0) + ) + + ;; CHECK: (func $caller (type $2) (param $0 i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (call $callee + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param i32) + ;; $callee's result is used, so $callee's param and $caller's param are also + ;; used. + (global.set $g (call $callee (local.get 0))) + ) +) + +(module + ;; CHECK: (func $other (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $other (result i32) + (i32.const 42) + ) + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (call $other) + ;; CHECK-NEXT: ) + (func $callee (result i32) + (call $other) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller + ;; $callee's result is unused, so $other's result should also be unused. + (drop (call $callee)) + ) +) + +(module + ;; CHECK: (func $other (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $other (result i32) + (block (result i32) + (i32.const 42) + ) + ) + ;; CHECK: (func $callee (type $0) + ;; CHECK-NEXT: (call $other) + ;; CHECK-NEXT: ) + (func $callee (result i32) + (block (result i32) + (call $other) + ) + ) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $callee) + ;; CHECK-NEXT: ) + (func $caller + ;; Same, but now with intermediate expressions. + (drop + (block (result i32) + (call $callee) + ) + ) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $target (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 42)) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (local $x i32) + ;; The result is used indirectly via a local. It cannot be removed. + (local.set $x (call $target)) + (global.set $g (local.get $x)) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $target (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 1)) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; The result is used in control flow. It cannot be removed. + (if (call $target) + (then (global.set $g (i32.const 100))) + ) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $target (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 1)) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (block $l + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + (block $l + ;; Same, but now the control flow is a br_if instead of an if. + (br_if $l + (call $target) + ) + (global.set $g (i32.const 100)) + ) + ) +) + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (func $target (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 1)) + ;; CHECK: (func $caller (type $2) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; The result is combined with other values and then used. + (global.set $g (i32.add (call $target) (i32.const 2))) + ) +) + +(module + ;; CHECK: (func $target (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (result i32) (i32.const 1)) + ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller + ;; The result is combined with other values, but not used. + (drop (i32.add (call $target) (i32.const 2))) + ) + ;; CHECK: (func $main (type $0) + ;; CHECK-NEXT: (call $caller) + ;; CHECK-NEXT: ) + (func $main + (call $caller) + ) +) \ No newline at end of file diff --git a/test/lit/passes/dae2.wast b/test/lit/passes/dae2.wast index 594ad0054de..a74e9709612 100644 --- a/test/lit/passes/dae2.wast +++ b/test/lit/passes/dae2.wast @@ -264,46 +264,6 @@ ) ) -(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) - ) -) ;; Tests with indirect function calls. @@ -1873,18 +1833,14 @@ (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: (func $test (type $0) ;; 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: (block + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (call $test) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (local.set $unused @@ -1895,23 +1851,20 @@ ;; 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)) + (func $test (param $unused (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) - ) + (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 + ;; The get after this does not actually use the parameter and is not ;; removed. (local.set $unused (ref.i31 @@ -1921,7 +1874,6 @@ (drop (local.get $unused) ) - (local.get $unused) ) ) @@ -2175,22 +2127,6 @@ ) ) -(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)) @@ -2835,18 +2771,14 @@ ;; CHECK: (rec ;; CHECK-NEXT: (type $2 (func)) - ;; CHECK: (type $3 (func (result i32))) + ;; CHECK: (type $3 (func)) ;; 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-NEXT: (type $5 (func (param i32))) - ;; CHECK: (type $8 (struct)) + ;; CHECK: (type $6 (struct)) ;; CHECK: (import "" "" (func $effect (type $1) (result i32))) (import "" "" (func $effect (result i32))) @@ -2927,7 +2859,7 @@ ) ) - ;; CHECK: (func $br-if (type $7) (param $used i32) + ;; CHECK: (func $br-if (type $5) (param $used i32) ;; CHECK-NEXT: (local $unused i32) ;; CHECK-NEXT: (block $trampoline0 ;; CHECK-NEXT: (drop @@ -2973,59 +2905,63 @@ ) ) - ;; CHECK: (func $br-table-mixed (type $6) (param $used i32) (result i32) + ;; CHECK: (func $br-table-mixed (type $5) (param $used 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: (drop + ;; 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: (drop - ;; CHECK-NEXT: (call $effect) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $trampoline0) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $trampoline0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $br-table-mixed (param $unused i32) (param $used i32) (result i32) + (func $br-table-mixed (param $unused i32) (param $used 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 + (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) ) - (drop - (call $effect) - ) - (local.get $unused) ) + (i32.const 1) ) - (i32.const 1) ) ) @@ -3551,34 +3487,41 @@ ) (module - ;; CHECK: (type $0 (func (result i32))) + ;; CHECK: (type $0 (func)) - ;; CHECK: (func $test (type $0) (result i32) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $test (type $0) ;; CHECK-NEXT: (local $unused i32) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (global.set $g + ;; 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: ) ;; CHECK-NEXT: ) - (func $test (param $unused i32) (result i32) + (func $test (param $unused 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) + (global.set $g + (i32.add + (if (result i32) + (unreachable) + (then + (i32.const 0) + ) + (else + (local.get $unused) + ) ) + (local.get $unused) ) - (local.get $unused) ) ) ) From 4d22772f48de6a9fd0dbb40160038d6cb5c1a372 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 17 Jul 2026 02:24:28 -0700 Subject: [PATCH 2/5] remove redundant std::get --- src/passes/DeadArgumentElimination2.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 075f7770867..95fee34ccc4 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -369,14 +369,14 @@ struct DAE2 : public Pass { if (auto* l = std::get_if(&loc)) { return join(*l, other); } - if (std::get_if(&loc)) { - return join(std::get(loc), other); + if (auto* l = std::get_if(&loc)) { + return join(*l, other); } if (auto* l = std::get_if(&loc)) { return join(*l, other); } - if (std::get_if(&loc)) { - return join(std::get(loc), other); + if (auto* l = std::get_if(&loc)) { + return join(*l, other); } WASM_UNREACHABLE("unexpected loc"); } From d599b877b38c43e739501de2d4477facae025002 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 17 Jul 2026 02:36:25 -0700 Subject: [PATCH 3/5] reduce duplication between CallRef and CallIndirect --- src/passes/DeadArgumentElimination2.cpp | 34 ++++++++++--------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 95fee34ccc4..620f6a361b5 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -650,36 +650,28 @@ struct GraphBuilder : public WalkerPass> { } } - void visitCallIndirect(CallIndirect* curr) { - auto sig = curr->heapType.getSignature(); + void handleIndirectCall(Expression* curr, HeapType type, bool isReturn) { + auto sig = type.getSignature(); if (sig.results.isConcrete()) { - if (curr->isReturn) { - Location source = TypeResultLoc{getRootType(curr->heapType)}; + HeapType rootType = getRootType(type); + Location source = TypeResultLoc{rootType}; + if (isReturn) { forwardToResult(source); - funcInfos[index].tailCalleeTypes.push_back(getRootType(curr->heapType)); + funcInfos[index].tailCalleeTypes.push_back(rootType); } else if (optimizeReferencedFuncs) { - Location source = TypeResultLoc{getRootType(curr->heapType)}; getValueFromLocation(curr, source); } } } + void visitCallIndirect(CallIndirect* curr) { + handleIndirectCall(curr, curr->heapType, curr->isReturn); + } + void visitCallRef(CallRef* curr) { - if (curr->target->type.isSignature()) { - auto sig = curr->target->type.getHeapType().getSignature(); - if (sig.results.isConcrete()) { - if (curr->isReturn) { - Location source = - TypeResultLoc{getRootType(curr->target->type.getHeapType())}; - forwardToResult(source); - auto heapType = curr->target->type.getHeapType(); - funcInfos[index].tailCalleeTypes.push_back(getRootType(heapType)); - } else if (optimizeReferencedFuncs) { - Location source = - TypeResultLoc{getRootType(curr->target->type.getHeapType())}; - getValueFromLocation(curr, source); - } - } + auto targetType = curr->target->type; + if (targetType.isSignature()) { + handleIndirectCall(curr, targetType.getHeapType(), curr->isReturn); } } }; From 7594586124c5ba23df2e80c99ef8ec8177444a4c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 17 Jul 2026 02:37:18 -0700 Subject: [PATCH 4/5] Remove AI test title --- test/lit/passes/dae2-results-intrinsics.wast | 1 - 1 file changed, 1 deletion(-) diff --git a/test/lit/passes/dae2-results-intrinsics.wast b/test/lit/passes/dae2-results-intrinsics.wast index 55463a709a2..878a8089a96 100644 --- a/test/lit/passes/dae2-results-intrinsics.wast +++ b/test/lit/passes/dae2-results-intrinsics.wast @@ -21,7 +21,6 @@ ) ) -;; Test 8.2: JS-Called via configureAll (module ;; CHECK: (type $externs (array (mut externref))) (type $externs (array (mut externref))) From 0f9ce99b3a5a2cadee7c87fe4a9df8c993100a2d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 17 Jul 2026 03:45:18 -0700 Subject: [PATCH 5/5] improve ResultLoc comments --- src/passes/DeadArgumentElimination2.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 620f6a361b5..65f2a2d45a6 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -114,15 +114,15 @@ using Params = std::vector; // Function index and parameter index. using FuncParamLoc = std::pair; -// Function index identifying the function' result (we treat result tuples as a -// single value). +// Function index identifying the function's result (we treat result tuples as a +// single value, so no index into the results is necessary). using FuncResultLoc = Index; // Function type and parameter index. using TypeParamLoc = std::pair; // Function type identifying the function type's result (we treat result tuples -// as a single value). +// as a single value, so no index into the results is necessary). using TypeResultLoc = HeapType; using Location =