summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/RemoveUnusedModuleElements.cpp44
-rw-r--r--test/lit/passes/remove-unused-module-elements-refs.wast112
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)
+ )
+)