diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ef0977d12fa..16f752e1017 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,86 +19,263 @@ // PassOptions structure; see more details there. // +#include + #include "ir/effects.h" #include "ir/module-utils.h" +#include "ir/subtypes.h" +#include "ir/element-utils.h" +#include "ir/table-utils.h" #include "pass.h" #include "support/unique_deferring_queue.h" #include "wasm.h" namespace wasm { +namespace { + +struct FuncInfo { + // Effects in this function. + std::optional effects; + + // Directly-called functions from this function. + std::unordered_set calledFunctions; + + // Types that are targets of indirect calls. + std::unordered_set indirectCalledTypes; +}; + +std::map analyzeFuncs(Module& module, + const PassOptions& passOptions) { + ModuleUtils::ParallelFunctionAnalysis analysis( + module, [&](Function* func, FuncInfo& funcInfo) { + if (func->imported()) { + // Imports can do anything, so we need to assume the worst anyhow, + // which is the same as not specifying any effects for them in the + // map (which we do by not setting funcInfo.effects). + return; + } + + // Gather the effects. + funcInfo.effects.emplace(passOptions, module, func); + + if (funcInfo.effects->calls) { + // There are calls in this function, which we will analyze in detail. + // Clear the |calls| field first, and we'll handle calls of all sorts + // below. + funcInfo.effects->calls = false; + + // Clear throws as well, as we are "forgetting" calls right now, and + // want to forget their throwing effect as well. If we see something + // else that throws, below, then we'll note that there. + funcInfo.effects->throws_ = false; + + struct CallScanner + : public PostWalker> { + Module& wasm; + const PassOptions& options; + FuncInfo& funcInfo; + + CallScanner(Module& wasm, + const PassOptions& options, + FuncInfo& funcInfo) + : wasm(wasm), options(options), funcInfo(funcInfo) {} + + void visitExpression(Expression* curr) { + ShallowEffectAnalyzer effects(options, wasm, curr); + if (auto* call = curr->dynCast()) { + // Note the direct call. + funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls) { + HeapType type; + if (auto* callRef = curr->dynCast()) { + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; + } else { + assert(false && "Unexpected type of call"); + } + + funcInfo.indirectCalledTypes.insert(type); + } else { + // No call here, but update throwing if we see it. (Only do so, + // however, if we have effects; if we cleared it - see before - + // then we assume the worst anyhow, and have nothing to update.) + if (effects.throws_ && funcInfo.effects) { + funcInfo.effects->throws_ = true; + } + } + } + }; + CallScanner scanner(module, passOptions, funcInfo); + scanner.walkFunction(func); + } + }); + + return analysis.map; +} + +template +std::unordered_map> +transitiveClosure2(const std::unordered_map>& in) { + UniqueNonrepeatingDeferredQueue> work; + + for (const auto& [curr, neighbors] : in) { + for (const auto& neighbor : neighbors) { + work.push({curr, neighbor}); + } + } + + std::unordered_map> closure; + while (!work.empty()) { + auto [curr, neighbor] = work.pop(); + + closure[curr].insert(neighbor); + + auto neighborNeighbors = in.find(neighbor); + if (neighborNeighbors == in.end()) + continue; + for (const auto& neighborNeighbor : neighborNeighbors->second) { + work.push({curr, neighborNeighbor}); + } + } + + return closure; +} + +std::unordered_map> +transitiveClosure(const Module& module, + const std::map& funcInfos) { + // Compute the transitive closure of effects. To do so, first construct for + // each function a list of the functions that it is called by (so we need to + // propogate its effects to them), and then we'll construct the closure of + // that. + // + // callers[foo] = [func that calls foo, another func that calls foo, ..] + // + std::unordered_map> callers; + + // Our work queue contains info about a new call pair: a call from a caller + // to a called function, that is information we then apply and propagate. + using CallPair = std::pair; // { caller, called } + UniqueDeferredQueue work; + for (auto& [func, info] : funcInfos) { + for (auto& called : info.calledFunctions) { + work.push({func->name, called}); + } + } + + // Compute the transitive closure of the call graph, that is, fill out + // |callers| so that it contains the list of all callers - even through a + // chain - of each function. + while (!work.empty()) { + auto [caller, called] = work.pop(); + + // We must not already have an entry for this call (that would imply we + // are doing wasted work). + assert(!callers[called].contains(caller)); + + // Apply the new call information. + callers[called].insert(caller); + + // We just learned that |caller| calls |called|. It also calls + // transitively, which we need to propagate to all places unaware of that + // information yet. + // + // caller => called => called by called + // + auto it = funcInfos.find(module.getFunction(called)); + auto& calledInfo = it->second; + // auto& calledInfo = funcInfos.at(module.getFunction(called)); + for (auto calledByCalled : calledInfo.calledFunctions) { + if (!callers[calledByCalled].contains(caller)) { + work.push({caller, calledByCalled}); + } + } + } + + return callers; +} + +template +std::unordered_map> +flip(const std::unordered_map>& in) { + std::unordered_map> flipped; + for (const auto& [k, vs] : in) { + for (const auto& v : vs) { + flipped[v].insert(k); + } + } + return flipped; +} + +std::unordered_map> +callersOfHeapType(std::map funcInfos, + const SubTypes& subtypes) { + // Find the 'entry points' of indirect calls Name -> HeapType. + // At the same time, start recording connections for the transitive closure of + // indirect calls HeapType -> HeapType. This will be used to find the set of + // HeapTypes that are reachable via any sequence of indirect calls from a + // given function (Name -> HeapType) + std::unordered_map> + indirectCallersNonTransitive; + std::unordered_map> indirectCalls; + for (auto& [func, info] : funcInfos) { + auto& set = indirectCallersNonTransitive[func->name]; + auto& indirectCallsSet = indirectCalls[func->type.getHeapType()]; + for (auto& calledType : info.indirectCalledTypes) { + set.insert(calledType); + indirectCallsSet.insert(calledType); + } + } + + std::unordered_map> subTypesToAdd; + + for (const auto& [type, _] : indirectCalls) { + subtypes.iterSubTypes(type, [&subTypesToAdd, type](HeapType sub, int _) { + subTypesToAdd[type].insert(sub); + return true; + }); + } + + for (const auto& [k, v] : subTypesToAdd) { + auto it = indirectCalls.find(k); + + // No need to add this. It wasn't in the map because no function has this + // type, so there are no effects to aggregate and we can forget about it. + if (it == indirectCalls.end()) + continue; + + it->second.insert(v.begin(), v.end()); + } + + auto a = transitiveClosure2(indirectCalls); + + // Pretend that each subtype is indirect called by its supertype. + // This might not be the case but it's accurate enough since any caller that + // may indirect call a given type may also indirect call its subtype. + for (const auto& [k, v] : indirectCallersNonTransitive) { + for (const auto& x : v) { + auto y = a[x]; + + // we're leaving what was already there but should be fine + // since it's covered under transitive calls anyway + indirectCallersNonTransitive[k].insert(y.begin(), y.end()); + } + } + + return flip(indirectCallersNonTransitive); +} struct GenerateGlobalEffects : public Pass { void run(Module* module) override { // First, we do a scan of each function to see what effects they have, // including which functions they call directly (so that we can compute // transitive effects later). + auto funcInfos = analyzeFuncs(*module, getPassOptions()); - struct FuncInfo { - // Effects in this function. - std::optional effects; - - // Directly-called functions from this function. - std::unordered_set calledFunctions; - }; - - ModuleUtils::ParallelFunctionAnalysis analysis( - *module, [&](Function* func, FuncInfo& funcInfo) { - if (func->imported()) { - // Imports can do anything, so we need to assume the worst anyhow, - // which is the same as not specifying any effects for them in the - // map (which we do by not setting funcInfo.effects). - return; - } - - // Gather the effects. - funcInfo.effects.emplace(getPassOptions(), *module, func); - - if (funcInfo.effects->calls) { - // There are calls in this function, which we will analyze in detail. - // Clear the |calls| field first, and we'll handle calls of all sorts - // below. - funcInfo.effects->calls = false; - - // Clear throws as well, as we are "forgetting" calls right now, and - // want to forget their throwing effect as well. If we see something - // else that throws, below, then we'll note that there. - funcInfo.effects->throws_ = false; - - struct CallScanner - : public PostWalker> { - Module& wasm; - PassOptions& options; - FuncInfo& funcInfo; - - CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo) - : wasm(wasm), options(options), funcInfo(funcInfo) {} - - void visitExpression(Expression* curr) { - ShallowEffectAnalyzer effects(options, wasm, curr); - if (auto* call = curr->dynCast()) { - // Note the direct call. - funcInfo.calledFunctions.insert(call->target); - } else if (effects.calls) { - // This is an indirect call of some sort, so we must assume the - // worst. To do so, clear the effects, which indicates nothing - // is known (so anything is possible). - // TODO: We could group effects by function type etc. - funcInfo.effects.reset(); - } else { - // No call here, but update throwing if we see it. (Only do so, - // however, if we have effects; if we cleared it - see before - - // then we assume the worst anyhow, and have nothing to update.) - if (effects.throws_ && funcInfo.effects) { - funcInfo.effects->throws_ = true; - } - } - } - }; - CallScanner scanner(*module, getPassOptions(), funcInfo); - scanner.walkFunction(func); - } - }); + SubTypes subtypes(*module); + auto indirectCallers = callersOfHeapType(funcInfos, subtypes); // Compute the transitive closure of effects. To do so, first construct for // each function a list of the functions that it is called by (so we need to @@ -107,50 +284,14 @@ struct GenerateGlobalEffects : public Pass { // // callers[foo] = [func that calls foo, another func that calls foo, ..] // - std::unordered_map> callers; - - // Our work queue contains info about a new call pair: a call from a caller - // to a called function, that is information we then apply and propagate. - using CallPair = std::pair; // { caller, called } - UniqueDeferredQueue work; - for (auto& [func, info] : analysis.map) { - for (auto& called : info.calledFunctions) { - work.push({func->name, called}); - } - } - - // Compute the transitive closure of the call graph, that is, fill out - // |callers| so that it contains the list of all callers - even through a - // chain - of each function. - while (!work.empty()) { - auto [caller, called] = work.pop(); - - // We must not already have an entry for this call (that would imply we - // are doing wasted work). - assert(!callers[called].contains(caller)); - - // Apply the new call information. - callers[called].insert(caller); - - // We just learned that |caller| calls |called|. It also calls - // transitively, which we need to propagate to all places unaware of that - // information yet. - // - // caller => called => called by called - // - auto& calledInfo = analysis.map[module->getFunction(called)]; - for (auto calledByCalled : calledInfo.calledFunctions) { - if (!callers[calledByCalled].contains(caller)) { - work.push({caller, calledByCalled}); - } - } - } + std::unordered_map> callers = + transitiveClosure(*module, funcInfos); // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a // trap). - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { if (callers[func->name].contains(func->name)) { if (info.effects) { info.effects->trap = true; @@ -158,12 +299,56 @@ struct GenerateGlobalEffects : public Pass { } } + std::unordered_set funcsWithAddress; + + auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(*module); + funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); + + ElementUtils::iterAllElementFunctionNames(module, [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); + + for (const auto& export_ : module->exports) { + funcsWithAddress.insert(export_->name); + } + + std::cout<<"funcsWithAddress\n"; + for (auto name : funcsWithAddress) { + std::cout<name]) { - auto& callerEffects = analysis.map[module->getFunction(caller)].effects; + for (const auto& caller : callers[func->name]) { + auto& callerEffects = funcInfos[module->getFunction(caller)].effects; + if (!callerEffects) { + // Nothing is known for the caller, which is already the worst case. + continue; + } + + if (!funcEffects) { + // Nothing is known for the called function, which means nothing is + // known for the caller either. + callerEffects.reset(); + continue; + } + + // Add func's effects to the caller. + callerEffects->mergeIn(*funcEffects); + } + + if (!funcsWithAddress.contains(func->name)) { + // This function hasn't had its address taken, so no-one can indirect call it + continue; + } + + auto indirectCallersOfThisFunction = + indirectCallers.find(func->type.getHeapType()); + if (indirectCallersOfThisFunction == indirectCallers.end()) { + continue; + } + for (Name caller : indirectCallersOfThisFunction->second) { + auto& callerEffects = funcInfos[module->getFunction(caller)].effects; if (!callerEffects) { // Nothing is known for the caller, which is already the worst case. continue; @@ -183,12 +368,13 @@ struct GenerateGlobalEffects : public Pass { // Generate the final data, starting from a blank slate where nothing is // known. - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { func->effects.reset(); if (!info.effects) { continue; } + std::cout << func->name << " has effects " << *info.effects << "\n"; func->effects = std::make_shared(*info.effects); } } @@ -202,6 +388,8 @@ struct DiscardGlobalEffects : public Pass { } }; +} // namespace + Pass* createGenerateGlobalEffectsPass() { return new GenerateGlobalEffects(); } Pass* createDiscardGlobalEffectsPass() { return new DiscardGlobalEffects(); } diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast new file mode 100644 index 00000000000..85823bf43dd --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,192 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s + +(module + ;; CHECK: (type $maybe-has-effects (func (param i32 i32))) + (type $maybe-has-effects (func (param i32 i32))) + + ;; CHECK: (type $only-has-effects-in-non-exported-function (func (param f32 f32))) + + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + ;; CHECK: (type $sub (sub $super (struct))) + (type $sub (sub $super (struct))) + + ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) + + ;; CHECK: (type $uninhabited (func (param f32))) + (type $uninhabited (func (param f32))) + + ;; Subtype + (type $func-with-sub-param (sub (func (param (ref $sub))))) + + ;; Subtype + ;; CHECK: (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + + (type $only-has-effects-in-non-exported-function (func (param f32 f32))) + + (table 10 funcref) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32 i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32 i32) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $7) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-ref (param $ref (ref $nopType)) + ;; This can only possibly be a nop in closed-world + ;; Ideally vacuum could optimize this out but we don't have a way to share + ;; this information with other passes today. + ;; For now, we can at least annotate that $f has no effects. + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $7) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $nopType)) + ;; $calls-nop-via-ref has no effects because we determined that it can only + ;; call $nop. We can optimize this call out. + (call $calls-nop-via-ref (local.get $ref)) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $8) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call_ref $maybe-has-effects + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-effectful-function-via-ref (param $ref (ref $maybe-has-effects)) + (call_ref $maybe-has-effects (i32.const 1) (i32.const 2) (local.get $ref)) + ) + + ;; CHECK: (func $g (type $8) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref $maybe-has-effects)) + ;; This may be a nop or it may trap depending on the ref + ;; We don't know so don't optimize it out. + (call $calls-effectful-function-via-ref (local.get $ref)) + ) + + ;; CHECK: (func $calls-uninhabited (type $9) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-uninhabited (param $ref (ref $uninhabited)) + (call_ref $uninhabited (f32.const 0) (local.get $ref)) + ) + + ;; CHECK: (func $h (type $9) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $h (param $ref (ref $uninhabited)) + ;; There's no function with this type, so it's impossible to create a ref to + ;; call this function with and there are no effects to aggregate. + ;; Remove this call. + (call $calls-uninhabited (local.get $ref)) + ) + + ;; CHECK: (func $nop-with-supertype (type $func-with-sub-param) (param $0 (ref $sub)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $func-with-sub-param) (param (ref $sub)) + ) + + ;; CHECK: (func $effectful-with-subtype (type $func-with-super-param) (param $0 (ref $super)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $func-with-super-param) (param (ref $super)) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-subtype (type $10) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call_ref $func-with-sub-param + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-subtype (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) + ) + + ;; CHECK: (func $asdf (type $10) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call $calls-ref-with-subtype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $asdf (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; Check that we account for subtyping correctly. + ;; The type $func-with-sub-param (the supertype) has no effects (i.e. the + ;; union of all effects of functions with this type is empty). + ;; However, a subtype of $func-with-sub-param ($func-with-super-param) does + ;; have effects, and we can call_ref with that subtype, so we need to + ;; include the unreachable effect and we can't optimize out this call. + (call $calls-ref-with-subtype (local.get $func) (local.get $sub)) + ) + + ;; CHECK: (func $no-effects (type $only-has-effects-in-non-exported-function) (param $0 f32) (param $1 f32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $no-effects (type $only-has-effects-in-non-exported-function) (param f32 f32) + ) + + ;; CHECK: (func $has-effects-but-not-exported (type $only-has-effects-in-non-exported-function) (param $0 f32) (param $1 f32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $has-effects-but-not-exported (type $only-has-effects-in-non-exported-function) (param f32 f32) + (unreachable) + ) + + ;; CHECK: (func $calls-type-with-effects-but-not-addressable (type $11) (param $ref (ref $only-has-effects-in-non-exported-function)) + ;; CHECK-NEXT: (call_ref $only-has-effects-in-non-exported-function + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-type-with-effects-but-not-addressable (param $ref (ref $only-has-effects-in-non-exported-function)) + (call_ref $only-has-effects-in-non-exported-function (f32.const 0) (f32.const 0) (local.get $ref)) + ) + + ;; CHECK: (func $i (type $11) (param $ref (ref $only-has-effects-in-non-exported-function)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $i (param $ref (ref $only-has-effects-in-non-exported-function)) + (call $calls-type-with-effects-but-not-addressable (local.get $ref)) + ) +)