diff options
-rw-r--r-- | src/passes/RemoveUnusedModuleElements.cpp | 44 | ||||
-rw-r--r-- | test/lit/passes/remove-unused-module-elements-refs.wast | 112 |
2 files changed, 151 insertions, 5 deletions
diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 2466d6252..686a95032 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -23,6 +23,7 @@ #include <memory> #include "ir/element-utils.h" +#include "ir/intrinsics.h" #include "ir/module-utils.h" #include "ir/utils.h" #include "pass.h" @@ -62,8 +63,8 @@ struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { // // TODO: We assume a closed world in the GC space atm, but eventually should // have a flag for that, and when the world is not closed we'd need to - // check for RefFuncs that flow out to exports. - std::unordered_map<HeapType, std::vector<Name>> uncalledRefFuncMap; + // check for RefFuncs that flow out to exports or imports + std::unordered_map<HeapType, std::unordered_set<Name>> uncalledRefFuncMap; ReachabilityAnalyzer(Module* module, const std::vector<ModuleElement>& roots) : module(module) { @@ -124,7 +125,29 @@ struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { void visitCall(Call* curr) { maybeAdd(ModuleElement(ModuleElementKind::Function, curr->target)); + + if (Intrinsics(*module).isCallWithoutEffects(curr)) { + // A call-without-effects receives a function reference and calls it, the + // same as a CallRef. When we have a flag for non-closed-world, we should + // handle this automatically by the reference flowing out to an import, + // which is what binaryen intrinsics look like. For now, to support use + // cases of a closed world but that also use this intrinsic, handle the + // intrinsic specifically here. + auto* target = curr->operands.back(); + if (auto* refFunc = target->dynCast<RefFunc>()) { + // We can see exactly where this goes. + Call call(module->allocator); + call.target = refFunc->func; + visitCall(&call); + } else { + // All we can see is the type, so do a CallRef of that. + CallRef callRef(module->allocator); + callRef.target = target; + visitCallRef(&callRef); + } + } } + void visitCallIndirect(CallIndirect* curr) { maybeAddTable(curr->table); } void visitCallRef(CallRef* curr) { @@ -186,7 +209,7 @@ struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { maybeAdd(ModuleElement(ModuleElementKind::Function, curr->func)); } else { // We've never seen a CallRef for this, but might see one later. - uncalledRefFuncMap[type].push_back(curr->func); + uncalledRefFuncMap[type].insert(curr->func); } } void visitTableGet(TableGet* curr) { maybeAddTable(curr->table); } @@ -277,24 +300,35 @@ struct RemoveUnusedModuleElements : public Pass { for (auto target : targets) { uncalledRefFuncs.insert(target); } + + // We cannot have a type in both this map and calledSignatures. + assert(analyzer.calledSignatures.count(type) == 0); + } + +#ifndef NDEBUG + for (auto type : analyzer.calledSignatures) { + assert(analyzer.uncalledRefFuncMap.count(type) == 0); } +#endif // Remove unreachable elements. module->removeFunctions([&](Function* curr) { if (analyzer.reachable.count( ModuleElement(ModuleElementKind::Function, curr->name))) { + // This is reached. return false; } if (uncalledRefFuncs.count(curr->name)) { - // See comment above on uncalledRefFuncs. + // This is not reached, but has a reference. See comment above on + // uncalledRefFuncs. if (!curr->imported()) { curr->body = Builder(*module).makeUnreachable(); } return false; } - // The function is not reached and has no references; remove it. + // The function is not reached and has no reference; remove it. return true; }); module->removeGlobals([&](Global* curr) { diff --git a/test/lit/passes/remove-unused-module-elements-refs.wast b/test/lit/passes/remove-unused-module-elements-refs.wast index a31854d43..6d408ebcf 100644 --- a/test/lit/passes/remove-unused-module-elements-refs.wast +++ b/test/lit/passes/remove-unused-module-elements-refs.wast @@ -224,3 +224,115 @@ ;; This function is not reachable. ) ) + +;; The call.without.effects intrinsic does a call to the reference given to it, +;; but for now other imports do not (until we add a flag for closed-world). +(module + ;; CHECK: (type $A (func_subtype func)) + (type $A (func)) + + ;; CHECK: (type $funcref_=>_none (func_subtype (param funcref) func)) + + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) + (import "binaryen-intrinsics" "call.without.effects" + (func $call-without-effects (param funcref))) + + ;; CHECK: (import "other" "import" (func $other-import (param funcref))) + (import "other" "import" + (func $other-import (param funcref))) + + ;; CHECK: (elem declare func $target-drop $target-keep) + + ;; CHECK: (export "foo" (func $foo)) + + ;; CHECK: (func $foo (type $A) + ;; CHECK-NEXT: (call $call-without-effects + ;; CHECK-NEXT: (ref.func $target-keep) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $other-import + ;; CHECK-NEXT: (ref.func $target-drop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $foo (export "foo") + ;; Calling the intrinsic with a reference is considered a call of the + ;; reference, so we will not remove $target-keep. + (call $call-without-effects + (ref.func $target-keep) + ) + ;; The other import is not enough to keep $target-drop alive. + (call $other-import + (ref.func $target-drop) + ) + ) + + ;; CHECK: (func $target-keep (type $A) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $target-keep (type $A) + ) + + ;; CHECK: (func $target-drop (type $A) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $target-drop (type $A) + ) +) + +;; As above, but now the call to the intrinsic does not let us see the exact +;; function being called. +(module + ;; CHECK: (type $A (func_subtype func)) + (type $A (func)) + + ;; CHECK: (type $funcref_=>_none (func_subtype (param funcref) func)) + + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) + (import "binaryen-intrinsics" "call.without.effects" + (func $call-without-effects (param funcref))) + + ;; CHECK: (import "other" "import" (func $other-import (param funcref))) + (import "other" "import" + (func $other-import (param funcref))) + + ;; CHECK: (elem declare func $target-keep $target-keep-2) + + ;; CHECK: (export "foo" (func $foo)) + + ;; CHECK: (func $foo (type $A) + ;; CHECK-NEXT: (call $call-without-effects + ;; CHECK-NEXT: (ref.null $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $target-keep) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $other-import + ;; CHECK-NEXT: (ref.func $target-keep-2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $foo (export "foo") + ;; Call the intrinsic without a RefFunc. All we infer here is the type, + ;; which means we must assume anything with type $A (and a reference) can be + ;; called, which will keep alive both $target-keep and $target-keep-2 + (call $call-without-effects + (ref.null $A) + ) + (drop + (ref.func $target-keep) + ) + (call $other-import + (ref.func $target-keep-2) + ) + ) + + ;; CHECK: (func $target-keep (type $A) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $target-keep (type $A) + ) + + ;; CHECK: (func $target-keep-2 (type $A) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $target-keep-2 (type $A) + ) +) |