diff options
-rw-r--r-- | src/passes/DeadArgumentElimination.cpp | 27 | ||||
-rw-r--r-- | src/passes/SignaturePruning.cpp | 68 | ||||
-rw-r--r-- | src/passes/param-utils.cpp | 183 | ||||
-rw-r--r-- | src/passes/param-utils.h | 61 | ||||
-rw-r--r-- | test/lit/passes/dae_all-features.wast | 255 | ||||
-rw-r--r-- | test/lit/passes/dae_tnh.wast | 52 | ||||
-rw-r--r-- | test/lit/passes/signature-pruning.wast | 301 |
7 files changed, 822 insertions, 125 deletions
diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp index d0705961b..4a341571e 100644 --- a/src/passes/DeadArgumentElimination.cpp +++ b/src/passes/DeadArgumentElimination.cpp @@ -216,11 +216,26 @@ struct DAE : public Pass { allDroppedCalls[name] = calls; } } + // Track which functions we changed, and optimize them later if necessary. std::unordered_set<Function*> changed; + // If we refine return types then we will need to do more type updating // at the end. bool refinedReturnTypes = false; + + // If we find that localizing call arguments can help (by moving their + // effects outside, so ParamUtils::removeParameters can handle them), then + // we do that at the end and perform another cycle. It is simpler to just do + // another cycle than to track the locations of calls, which is tricky as + // localization might move a call (if a call happens to be another call's + // param). In practice it is rare to find call arguments we want to remove, + // and even more rare to find effects get in the way, so this should not + // cause much overhead. + // + // This set tracks the functions for whom calls to it should be modified. + std::unordered_set<Name> callTargetsToLocalize; + // We now have a mapping of all call sites for each function, and can look // for optimization opportunities. for (auto& [name, calls] : allCalls) { @@ -263,12 +278,15 @@ struct DAE : public Pass { if (numParams == 0) { continue; } - auto removedIndexes = ParamUtils::removeParameters( + auto [removedIndexes, outcome] = ParamUtils::removeParameters( {func}, infoMap[name].unusedParams, calls, {}, module, getPassRunner()); if (!removedIndexes.empty()) { // Success! changed.insert(func); } + if (outcome == ParamUtils::RemovalOutcome::Failure) { + callTargetsToLocalize.insert(name); + } } // We can also tell which calls have all their return values dropped. Note // that we can't do this if we changed anything so far, as we may have @@ -307,10 +325,15 @@ struct DAE : public Pass { changed.insert(func.get()); } } + if (!callTargetsToLocalize.empty()) { + ParamUtils::localizeCallsTo( + callTargetsToLocalize, *module, getPassRunner()); + } if (optimize && !changed.empty()) { OptUtils::optimizeAfterInlining(changed, module, getPassRunner()); } - return !changed.empty() || refinedReturnTypes; + return !changed.empty() || refinedReturnTypes || + !callTargetsToLocalize.empty(); } private: diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index 23295a66a..2e4be89e8 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -67,6 +67,16 @@ struct SignaturePruning : public Pass { return; } + // The first iteration may suggest additional work is possible. If so, run + // another cycle. (Even more cycles may help, but limit ourselves to 2 for + // now.) + if (iteration(module)) { + iteration(module); + } + } + + // Returns true if more work is possible. + bool iteration(Module* module) { // First, find all the information we need. Start by collecting inside each // function in parallel. @@ -101,6 +111,16 @@ struct SignaturePruning : public Pass { // Map heap types to all functions with that type. InsertOrderedMap<HeapType, std::vector<Function*>> sigFuncs; + // Heap types of call targets that we found we should localize calls to, in + // order to fully handle them. (See similar code in DeadArgumentElimination + // for individual functions; here we handle a HeapType at a time.) A slight + // complication is that we cannot track heap types here: heap types are + // rewritten using |GlobalTypeRewriter::updateSignatures| below, and even + // types that we do not modify end up replaced (as the entire set of types + // becomes one new big rec group). We therefore need something more stable + // to track here, which we do using either a Call or a Call Ref. + std::unordered_set<Expression*> callTargetsToLocalize; + // Combine all the information we gathered into that map, iterating in a // deterministic order as we build up vectors where the order matters. for (auto& f : module->functions) { @@ -215,12 +235,23 @@ struct SignaturePruning : public Pass { } auto oldParams = sig.params; - auto removedIndexes = ParamUtils::removeParameters(funcs, - unusedParams, - info.calls, - info.callRefs, - module, - getPassRunner()); + auto [removedIndexes, outcome] = + ParamUtils::removeParameters(funcs, + unusedParams, + info.calls, + info.callRefs, + module, + getPassRunner()); + if (outcome == ParamUtils::RemovalOutcome::Failure) { + // Use either a Call or a CallRef that has this type (see explanation + // above on |callTargetsToLocalize|. + if (!info.calls.empty()) { + callTargetsToLocalize.insert(info.calls[0]); + } else { + assert(!info.callRefs.empty()); + callTargetsToLocalize.insert(info.callRefs[0]); + } + } if (removedIndexes.empty()) { continue; } @@ -262,6 +293,31 @@ struct SignaturePruning : public Pass { // Rewrite the types. GlobalTypeRewriter::updateSignatures(newSignatures, *module); + + if (callTargetsToLocalize.empty()) { + return false; + } + + // Localize after updating signatures, to not interfere with that + // operation (localization adds locals, and the indexes of locals must be + // taken into account in |GlobalTypeRewriter::updateSignatures| (as var + // indexes change when params are pruned). + std::unordered_set<HeapType> callTargetTypes; + for (auto* call : callTargetsToLocalize) { + HeapType type; + if (auto* c = call->dynCast<Call>()) { + type = module->getFunction(c->target)->type; + } else if (auto* c = call->dynCast<CallRef>()) { + type = c->target->type.getHeapType(); + } else { + WASM_UNREACHABLE("bad call"); + } + callTargetTypes.insert(type); + } + + ParamUtils::localizeCallsTo(callTargetTypes, *module, getPassRunner()); + + return true; } }; diff --git a/src/passes/param-utils.cpp b/src/passes/param-utils.cpp index e94ea95b1..0caccff11 100644 --- a/src/passes/param-utils.cpp +++ b/src/passes/param-utils.cpp @@ -14,11 +14,16 @@ * limitations under the License. */ +#include "passes/param-utils.h" +#include "ir/eh-utils.h" #include "ir/function-utils.h" #include "ir/local-graph.h" +#include "ir/localize.h" #include "ir/possible-constant.h" #include "ir/type-updating.h" +#include "pass.h" #include "support/sorted_vector.h" +#include "wasm-traversal.h" #include "wasm.h" namespace wasm::ParamUtils { @@ -45,12 +50,12 @@ std::unordered_set<Index> getUsedParams(Function* func) { return usedParams; } -bool removeParameter(const std::vector<Function*>& funcs, - Index index, - const std::vector<Call*>& calls, - const std::vector<CallRef*>& callRefs, - Module* module, - PassRunner* runner) { +RemovalOutcome removeParameter(const std::vector<Function*>& funcs, + Index index, + const std::vector<Call*>& calls, + const std::vector<CallRef*>& callRefs, + Module* module, + PassRunner* runner) { assert(funcs.size() > 0); auto* first = funcs[0]; #ifndef NDEBUG @@ -74,28 +79,31 @@ bool removeParameter(const std::vector<Function*>& funcs, // propagating that out, or by appending an unreachable after the call, but // for simplicity just ignore such cases; if we are called again later then // if DCE ran meanwhile then we could optimize. - auto hasBadEffects = [&](auto* call) { - auto& operands = call->operands; - bool hasUnremovable = - EffectAnalyzer(runner->options, *module, operands[index]) - .hasUnremovableSideEffects(); - bool wouldChangeType = call->type == Type::unreachable && !call->isReturn && - operands[index]->type == Type::unreachable; - return hasUnremovable || wouldChangeType; + auto checkEffects = [&](auto* call) { + auto* operand = call->operands[index]; + + if (operand->type == Type::unreachable) { + return Failure; + } + + bool hasUnremovable = EffectAnalyzer(runner->options, *module, operand) + .hasUnremovableSideEffects(); + + return hasUnremovable ? Failure : Success; }; - bool callParamsAreValid = - std::none_of(calls.begin(), calls.end(), [&](Call* call) { - return hasBadEffects(call); - }); - if (!callParamsAreValid) { - return false; + + for (auto* call : calls) { + auto result = checkEffects(call); + if (result != Success) { + return result; + } } - bool callRefParamsAreValid = - std::none_of(callRefs.begin(), callRefs.end(), [&](CallRef* call) { - return hasBadEffects(call); - }); - if (!callRefParamsAreValid) { - return false; + + for (auto* call : callRefs) { + auto result = checkEffects(call); + if (result != Success) { + return result; + } } // The type must be valid for us to handle as a local (since we @@ -104,7 +112,7 @@ bool removeParameter(const std::vector<Function*>& funcs, // local bool typeIsValid = TypeUpdating::canHandleAsLocal(first->getLocalType(index)); if (!typeIsValid) { - return false; + return Failure; } // We can do it! @@ -161,17 +169,18 @@ bool removeParameter(const std::vector<Function*>& funcs, call->operands.erase(call->operands.begin() + index); } - return true; + return Success; } -SortedVector removeParameters(const std::vector<Function*>& funcs, - SortedVector indexes, - const std::vector<Call*>& calls, - const std::vector<CallRef*>& callRefs, - Module* module, - PassRunner* runner) { +std::pair<SortedVector, RemovalOutcome> +removeParameters(const std::vector<Function*>& funcs, + SortedVector indexes, + const std::vector<Call*>& calls, + const std::vector<CallRef*>& callRefs, + Module* module, + PassRunner* runner) { if (indexes.empty()) { - return {}; + return {{}, Success}; } assert(funcs.size() > 0); @@ -188,8 +197,8 @@ SortedVector removeParameters(const std::vector<Function*>& funcs, SortedVector removed; while (1) { if (indexes.has(i)) { - if (removeParameter(funcs, i, calls, callRefs, module, runner)) { - // Success! + auto outcome = removeParameter(funcs, i, calls, callRefs, module, runner); + if (outcome == Success) { removed.insert(i); } } @@ -198,7 +207,11 @@ SortedVector removeParameters(const std::vector<Function*>& funcs, } i--; } - return removed; + RemovalOutcome finalOutcome = Success; + if (removed.size() < indexes.size()) { + finalOutcome = Failure; + } + return {removed, finalOutcome}; } SortedVector applyConstantValues(const std::vector<Function*>& funcs, @@ -246,4 +259,98 @@ SortedVector applyConstantValues(const std::vector<Function*>& funcs, return optimized; } +void localizeCallsTo(const std::unordered_set<Name>& callTargets, + Module& wasm, + PassRunner* runner) { + struct LocalizerPass : public WalkerPass<PostWalker<LocalizerPass>> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr<Pass> create() override { + return std::make_unique<LocalizerPass>(callTargets); + } + + const std::unordered_set<Name>& callTargets; + + LocalizerPass(const std::unordered_set<Name>& callTargets) + : callTargets(callTargets) {} + + void visitCall(Call* curr) { + if (!callTargets.count(curr->target)) { + return; + } + + ChildLocalizer localizer( + curr, getFunction(), *getModule(), getPassOptions()); + auto* replacement = localizer.getReplacement(); + if (replacement != curr) { + replaceCurrent(replacement); + optimized = true; + } + } + + bool optimized = false; + + void visitFunction(Function* curr) { + if (optimized) { + // Localization can add blocks, which might move pops. + EHUtils::handleBlockNestedPops(curr, *getModule()); + } + } + }; + + LocalizerPass(callTargets).run(runner, &wasm); +} + +void localizeCallsTo(const std::unordered_set<HeapType>& callTargets, + Module& wasm, + PassRunner* runner) { + struct LocalizerPass : public WalkerPass<PostWalker<LocalizerPass>> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr<Pass> create() override { + return std::make_unique<LocalizerPass>(callTargets); + } + + const std::unordered_set<HeapType>& callTargets; + + LocalizerPass(const std::unordered_set<HeapType>& callTargets) + : callTargets(callTargets) {} + + void visitCall(Call* curr) { + handleCall(curr, getModule()->getFunction(curr->target)->type); + } + + void visitCallRef(CallRef* curr) { + auto type = curr->target->type; + if (type.isRef()) { + handleCall(curr, type.getHeapType()); + } + } + + void handleCall(Expression* call, HeapType type) { + if (!callTargets.count(type)) { + return; + } + + ChildLocalizer localizer( + call, getFunction(), *getModule(), getPassOptions()); + auto* replacement = localizer.getReplacement(); + if (replacement != call) { + replaceCurrent(replacement); + optimized = true; + } + } + + bool optimized = false; + + void visitFunction(Function* curr) { + if (optimized) { + EHUtils::handleBlockNestedPops(curr, *getModule()); + } + } + }; + + LocalizerPass(callTargets).run(runner, &wasm); +} + } // namespace wasm::ParamUtils diff --git a/src/passes/param-utils.h b/src/passes/param-utils.h index 202c8b007..4c458390a 100644 --- a/src/passes/param-utils.h +++ b/src/passes/param-utils.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef wasm_ir_function_h -#define wasm_ir_function_h +#ifndef wasm_pass_param_utils_h +#define wasm_pass_param_utils_h #include "pass.h" #include "support/sorted_vector.h" @@ -44,6 +44,16 @@ namespace wasm::ParamUtils { // } std::unordered_set<Index> getUsedParams(Function* func); +// The outcome of an attempt to remove a parameter(s). +enum RemovalOutcome { + // We removed successfully. + Success = 0, + // We failed, but only because of fixable nested effects. The caller can move + // those effects out (e.g. using ChildLocalizer, or the helper localizeCallsTo + // below) and repeat. + Failure = 1, +}; + // Try to remove a parameter from a set of functions and replace it with a local // instead. This may not succeed if the parameter type cannot be used in a // local, or if we hit another limitation, in which case this returns false and @@ -64,21 +74,26 @@ std::unordered_set<Index> getUsedParams(Function* func); // need adjusting and it is easier to do it all in one place. Also, the caller // can update all the types at once throughout the program after making // multiple calls to removeParameter(). -bool removeParameter(const std::vector<Function*>& funcs, - Index index, - const std::vector<Call*>& calls, - const std::vector<CallRef*>& callRefs, - Module* module, - PassRunner* runner); +RemovalOutcome removeParameter(const std::vector<Function*>& funcs, + Index index, + const std::vector<Call*>& calls, + const std::vector<CallRef*>& callRefs, + Module* module, + PassRunner* runner); // The same as removeParameter, but gets a sorted list of indexes. It tries to -// remove them all, and returns which we removed. -SortedVector removeParameters(const std::vector<Function*>& funcs, - SortedVector indexes, - const std::vector<Call*>& calls, - const std::vector<CallRef*>& callRefs, - Module* module, - PassRunner* runner); +// remove them all, and returns which we removed, as well as an indication as +// to whether we might remove more if effects were not in the way (specifically, +// we return Success if we removed any index, Failure if we removed none, and +// FailureDueToEffects if at least one index could have been removed but for +// effects). +std::pair<SortedVector, RemovalOutcome> +removeParameters(const std::vector<Function*>& funcs, + SortedVector indexes, + const std::vector<Call*>& calls, + const std::vector<CallRef*>& callRefs, + Module* module, + PassRunner* runner); // Given a set of functions and the calls and call_refs that reach them, find // which parameters are passed the same constant value in all the calls. For @@ -92,6 +107,20 @@ SortedVector applyConstantValues(const std::vector<Function*>& funcs, const std::vector<CallRef*>& callRefs, Module* module); +// Helper that localizes all calls to a set of targets, in an entire module. +// This basically calls ChildLocalizer in each function, on the relevant calls. +// This is useful when we get FailureDueToEffects, see above. +// +// The set of targets can be function names (the individual functions we want to +// handle calls towards) or heap types (which will then include all functions +// with those types). +void localizeCallsTo(const std::unordered_set<Name>& callTargets, + Module& wasm, + PassRunner* runner); +void localizeCallsTo(const std::unordered_set<HeapType>& callTargets, + Module& wasm, + PassRunner* runner); + } // namespace wasm::ParamUtils -#endif // wasm_ir_function_h +#endif // wasm_pass_param_utils_h diff --git a/test/lit/passes/dae_all-features.wast b/test/lit/passes/dae_all-features.wast index d4390351f..17ea77942 100644 --- a/test/lit/passes/dae_all-features.wast +++ b/test/lit/passes/dae_all-features.wast @@ -109,25 +109,31 @@ (func $b33 (call $a3 (i32.const 4)) ) - ;; CHECK: (func $a4 (type $1) (param $x i32) + ;; CHECK: (func $a4 (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $a4 (param $x i32) ;; diff value, but with effects + (func $a4 (param $x i32) + ;; This function is called with one constant and one unreachable. We can + ;; remove the param despite the unreachable's effects. ) ;; CHECK: (func $b4 (type $0) - ;; CHECK-NEXT: (call $a4 - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) (func $b4 + ;; This call will vanish entirely, because the unreachable child executes + ;; first (so we cannot see here that we removed the parameter from $a4, but + ;; that can be confirmed in $a4 itself). (call $a4 (unreachable)) ) ;; CHECK: (func $b43 (type $0) - ;; CHECK-NEXT: (call $a4 - ;; CHECK-NEXT: (i32.const 4) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $a4) ;; CHECK-NEXT: ) (func $b43 + ;; We will remove the parameter here. (call $a4 (i32.const 4)) ) ;; CHECK: (func $a5 (type $0) @@ -659,27 +665,33 @@ ) (module - ;; CHECK: (type $0 (func (param i32))) + ;; CHECK: (type $0 (func)) - ;; CHECK: (func $0 (type $0) (param $0 i32) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $0 + ;; CHECK: (func $0 (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (return) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (return) ;; CHECK-NEXT: ) (func $0 (param $0 i32) (result i32) - ;; The result of this function can be removed, which makes us modify the - ;; returns (we should not return a value any more) and also the calls (the - ;; calls must be dropped). The returns here are nested in each other, and one - ;; is a recursive call to this function itself, which makes this a corner case - ;; we might emit invalid code for. + ;; The returns here are nested in each other, and one is a recursive call to + ;; this function itself, which makes this a corner case we might emit invalid + ;; code for. We end up removing the parameter, and then the call vanishes as + ;; it was unreachable; we also remove the return as well as it is dropped in + ;; the other caller, below. (return (drop (call $0 @@ -690,6 +702,17 @@ ) ) ) + + ;; CHECK: (func $other-call (type $0) + ;; CHECK-NEXT: (call $0) + ;; CHECK-NEXT: ) + (func $other-call + (drop + (call $0 + (i32.const 1) + ) + ) + ) ) (module @@ -727,3 +750,193 @@ ) ) ) + +(module + ;; CHECK: (type $0 (func (param f64) (result i32))) + + ;; CHECK: (func $target (type $0) (param $0 f64) (result i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (f64.const 1.1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (f64.const 4.4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (param $a i32) (param $b f64) (param $c i32) (result i32) + ;; Test removing a parameter despite calls having interesting non-unreachable + ;; effects. This also tests recursion of such calls. We can remove all the i32 + ;; parameters here. + (call $target + (call $target + (i32.const 0) + (f64.const 1.1) + (i32.const 2) + ) + (local.get $b) + (call $target + (i32.const 3) + (f64.const 4.4) + (i32.const 5) + ) + ) + ) +) + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $v128 (func (result v128))) + (type $v128 (func (result v128))) + + ;; CHECK: (type $2 (func (result f32))) + + ;; CHECK: (table $0 10 funcref) + (table $0 10 funcref) + + ;; CHECK: (func $caller-effects (type $0) + ;; CHECK-NEXT: (local $0 v128) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result f32) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (call_indirect $0 (type $v128) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller-effects + (drop + (call $target + (i64.const 0) + ;; We'd like to remove this unused parameter, but it has effects, so we'll + ;; move it to a local first. + (call_indirect $0 (type $v128) + (i32.const 0) + ) + (i64.const 0) + ) + ) + ) + + ;; CHECK: (func $target (type $2) (result f32) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 v128) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (param $0 i64) (param $1 v128) (param $2 i64) (result f32) + ;; All parameters here should vanish. + (unreachable) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32 i64))) + + ;; CHECK: (type $1 (func (param i64 i64))) + + ;; CHECK: (func $caller-later-br (type $0) (param $x i32) (param $y i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller-later-br (param $x i32) (param $y i64) + (block $block + (drop + (call $target + (i64.const 0) + ;; We'd like to remove this unused parameter, and we can do so by moving + ;; it to a local, but we need to be careful: the br right after us must be + ;; kept around, as it is the only thing that makes the outer block have + ;; type none and not unreachable. + (block (result i32) + (if + (local.get $x) + (then + (return) + ) + ) + (i32.const 42) + ) + ;; We'll move this around, but won't remove it, as explained above. + (br $block) + ) + ) + ) + ;; Another call, to show the effect of removing the i32 parameter (also, if + ;; no calls remain after removing the unreachable one before us, then the pass + ;; would stop before removing parameters in $target - we don't remove params + ;; from functions that look dead). + (drop + (call $target + (local.get $y) + (local.get $x) + (local.get $y) + ) + ) + ) + + ;; CHECK: (func $target (type $1) (param $0 i64) (param $1 i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result f32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (param $0 i64) (param $1 i32) (param $2 i64) (result f32) + ;; The i32 parameter should vanish. + (drop + (local.get $0) + ) + (drop + (local.get $2) + ) + (unreachable) + ) +) diff --git a/test/lit/passes/dae_tnh.wast b/test/lit/passes/dae_tnh.wast index 33e6eb09b..6d75aed30 100644 --- a/test/lit/passes/dae_tnh.wast +++ b/test/lit/passes/dae_tnh.wast @@ -39,14 +39,18 @@ ;; CHECK: (type $1 (func (param i32))) ;; CHECK: (func $caller (type $0) - ;; CHECK-NEXT: (call $target - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) (func $caller - ;; Removing this parameter would require the type of the call to change from - ;; unreachable to none. We don't handle such complexity and ignore such - ;; cases. + ;; Removing this parameter would make the type of the call change from + ;; unreachable to none. But the call itself is in unreachable code, so we + ;; will replace it with an unreachable (and then, once the call is gone, the + ;; target can be better optimized; however, no other calls remain here, so + ;; the pass does nothing as it considers it dead at that point). + ;; + ;; This test verifies we do the proper thing even in TNH mode, as in TNH + ;; mode |unreachable| seems to have no effects, but for validation reasons + ;; we must still replace the call here. (call $target (unreachable) ) @@ -59,13 +63,40 @@ ) ) -;; As above, but use a return_call. We can optimize that, since return_calls -;; have type unreachable anyhow, and the optimization would not change the type. +;; As above but the called target has a result. +(module + ;; CHECK: (type $0 (func (result i32))) + + ;; CHECK: (type $1 (func (param i32) (result i32))) + + ;; CHECK: (func $caller (type $0) (result i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $caller (result i32) + ;; Again, the call is replaced by an unreachable. + (call $target + (unreachable) + ) + ) + + ;; CHECK: (func $target (type $1) (param $0 i32) (result i32) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (func $target (param i32) (result i32) + (i32.const 42) + ) +) + +;; As above, but use a return_call. We can optimize that too (return_calls have +;; type unreachable anyhow, and the optimization would not change the type, so +;; it is even simpler). (module ;; CHECK: (type $0 (func)) + ;; CHECK: (type $1 (func (param i32))) + ;; CHECK: (func $caller (type $0) - ;; CHECK-NEXT: (return_call $target) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) (func $caller (return_call $target @@ -73,8 +104,7 @@ ) ) - ;; CHECK: (func $target (type $0) - ;; CHECK-NEXT: (local $0 i32) + ;; CHECK: (func $target (type $1) (param $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $target (param i32) diff --git a/test/lit/passes/signature-pruning.wast b/test/lit/passes/signature-pruning.wast index 4d22ef619..cad9af82b 100644 --- a/test/lit/passes/signature-pruning.wast +++ b/test/lit/passes/signature-pruning.wast @@ -136,7 +136,7 @@ ;; CHECK: (rec ;; CHECK-NEXT: (type $0 (func)) - ;; CHECK: (type $sig (sub (func (param i32 i64 f32)))) + ;; CHECK: (type $sig (sub (func (param i64 f32)))) (type $sig (sub (func (param i32) (param i64) (param f32) (param f64)))) (memory 1 1) @@ -145,19 +145,20 @@ ;; CHECK: (elem declare func $foo) - ;; CHECK: (func $foo (type $sig) (param $0 i32) (param $1 i64) (param $2 f32) - ;; CHECK-NEXT: (local $3 f64) + ;; CHECK: (func $foo (type $sig) (param $0 i64) (param $1 f32) + ;; CHECK-NEXT: (local $2 f64) + ;; CHECK-NEXT: (local $3 i32) ;; CHECK-NEXT: (i64.store ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (f32.store ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $foo (type $sig) (param $i32 i32) (param $i64 i64) (param $f32 f32) (param $f64 f64) - ;; Use the middle two parameters. + ;; Use the middle two parameters. The other two vanish. (i64.store (i32.const 0) (local.get $i64) @@ -169,25 +170,29 @@ ) ;; CHECK: (func $caller (type $0) - ;; CHECK-NEXT: (call $foo - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (call $caller) - ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $caller) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: (f32.const 2) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i64.const 1) - ;; CHECK-NEXT: (f32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (call_ref $sig - ;; CHECK-NEXT: (i32.const 4) ;; CHECK-NEXT: (i64.const 5) ;; CHECK-NEXT: (f32.const 6) ;; CHECK-NEXT: (ref.func $foo) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $caller - ;; As above, but now one of the unused parameters has a side effect which - ;; prevents us from removing it (flattening the IR first would avoid this - ;; limitation). We only end up removing a single unused param, the last. + ;; As above, but now one of the unused parameters has a side effect. We + ;; move it to a local, which allows us to remove it (and also the last, + ;; which is trivial). (call $foo (block (result i32) (call $caller) @@ -207,13 +212,13 @@ ) ) -;; As above, but with the effects on a call_ref. Once more, we can only optimize -;; away the very last param. +;; As above, but with the effects on a call_ref. Once more, we can optimize +;; even with effects on a param, using locals. (module ;; CHECK: (rec ;; CHECK-NEXT: (type $0 (func)) - ;; CHECK: (type $sig (sub (func (param i32 i64 f32)))) + ;; CHECK: (type $sig (sub (func (param i64 f32)))) (type $sig (sub (func (param i32) (param i64) (param f32) (param f64)))) (memory 1 1) @@ -222,15 +227,16 @@ ;; CHECK: (elem declare func $foo) - ;; CHECK: (func $foo (type $sig) (param $0 i32) (param $1 i64) (param $2 f32) - ;; CHECK-NEXT: (local $3 f64) + ;; CHECK: (func $foo (type $sig) (param $0 i64) (param $1 f32) + ;; CHECK-NEXT: (local $2 f64) + ;; CHECK-NEXT: (local $3 i32) ;; CHECK-NEXT: (i64.store ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (f32.store ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $foo (type $sig) (param $i32 i32) (param $i64 i64) (param $f32 f32) (param $f64 f64) @@ -245,19 +251,23 @@ ) ;; CHECK: (func $caller (type $0) + ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (call $foo - ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (i64.const 1) ;; CHECK-NEXT: (f32.const 2) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call_ref $sig - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (call $caller) - ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $caller) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (i64.const 5) + ;; CHECK-NEXT: (f32.const 6) + ;; CHECK-NEXT: (ref.func $foo) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i64.const 5) - ;; CHECK-NEXT: (f32.const 6) - ;; CHECK-NEXT: (ref.func $foo) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $caller @@ -912,3 +922,232 @@ (unreachable) ) ) + +;; Test corner cases with var updating. To remove the parameter of $func we +;; must move the parameter to a local first. We must then adjust local types +;; properly while adjusting the signature (when the signature loses a parameter, +;; local indexes change, which is a delicate dance handled by +;; GlobalTypeRewriter::updateSignatures and ParamUtils::removeParameters; +;; moving the parameter to a local first should not get in the way there). +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (sub (struct (field v128)))) + (type $struct (sub (struct (field v128)))) + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $func (func)) + (type $func (func (param v128))) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (local $0 v128) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func (type $func) (param $0 v128) + ;; The parameter will be removed. + (nop) + ) + + ;; CHECK: (func $caller (type $1) + ;; CHECK-NEXT: (local $0 (ref $struct)) + ;; CHECK-NEXT: (local $1 externref) + ;; CHECK-NEXT: (local $2 v128) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.tee $0 + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $func + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $param externref) + (local $var (ref $struct)) + ;; The parameter of this call_ref will be removed. + (call_ref $func + ;; Use a struct.get, which would error if the type the nested tee were + ;; incorrect (it asserts on it being a struct type). + (struct.get $struct 0 + ;; Use a tee to test the updating of tee'd vars, as mentioned above. + (local.tee $var + (struct.new_default $struct) + ) + ) + (ref.func $func) + ) + ) +) + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (func (result i32))) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (tag $tag (param i32)) + (tag $tag (param i32)) + + ;; CHECK: (func $catch-pop (type $2) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (block $block (result i32) + ;; CHECK-NEXT: (try $try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $catch-pop (result i32) + (block $block (result i32) + (try $try + (do + (nop) + ) + (catch $tag + (call $target + (pop i32) + ;; We can remove this parameter by moving it to a local first, which + ;; also moves the pop, which then needs to be fixed up. + (br_if $block + (i32.const 1) + (i32.const 2) + ) + ) + ;; This nop causes the call to be in a block. When we add another + ;; block to hold the code that we move, we'd get an error if we don't + ;; apply fixups. + (nop) + ) + ) + (i32.const 3) + ) + ) + + ;; CHECK: (func $target (type $1) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (param $x i32) (param $y i32) + ;; Use only the first param. The second will be removed. + (drop + (local.get $x) + ) + ) +) + +;; As above, but remove the other parameter (the pop). +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $1 (func (param i32))) + + ;; CHECK: (type $2 (func (result i32))) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (tag $tag (param i32)) + (tag $tag (param i32)) + + ;; CHECK: (func $catch-pop (type $2) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (block $block (result i32) + ;; CHECK-NEXT: (try $try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $target + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $catch-pop (result i32) + (block $block (result i32) + (try $try + (do + (nop) + ) + (catch $tag + (call $target + (pop i32) + (br_if $block + (i32.const 1) + (i32.const 2) + ) + ) + (nop) + ) + ) + (i32.const 3) + ) + ) + + ;; CHECK: (func $target (type $1) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $target (param $x i32) (param $y i32) + (drop + (local.get $y) ;; this changed from $x to $y + ) + ) +) |