summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/DeadArgumentElimination.cpp84
-rw-r--r--test/lit/passes/dae-gc.wast250
2 files changed, 332 insertions, 2 deletions
diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp
index a2231469d..ddba3bee8 100644
--- a/src/passes/DeadArgumentElimination.cpp
+++ b/src/passes/DeadArgumentElimination.cpp
@@ -43,6 +43,7 @@
#include "ir/find_all.h"
#include "ir/module-utils.h"
#include "ir/type-updating.h"
+#include "ir/utils.h"
#include "pass.h"
#include "passes/opt-utils.h"
#include "support/sorted_vector.h"
@@ -308,6 +309,9 @@ struct DAE : public Pass {
allDroppedCalls[pair.first] = pair.second;
}
}
+ // If we refine return types then we will need to do more type updating
+ // at the end.
+ bool refinedReturnTypes = false;
// We now have a mapping of all call sites for each function, and can look
// for optimization opportunities.
for (auto& pair : allCalls) {
@@ -323,6 +327,10 @@ struct DAE : public Pass {
// affect whether an argument is used or not, it just refines the type
// where possible.
refineArgumentTypes(func, calls, module);
+ // Refine return types as well.
+ if (refineReturnTypes(func, calls, module)) {
+ refinedReturnTypes = true;
+ }
// Check if all calls pass the same constant for a particular argument.
for (Index i = 0; i < numParams; i++) {
Literal value;
@@ -357,6 +365,12 @@ struct DAE : public Pass {
}
}
}
+ if (refinedReturnTypes) {
+ // Changing a call expression's return type can propagate out to its
+ // parents, and so we must refinalize.
+ // TODO: We could track in which functions we actually make changes.
+ ReFinalize().run(runner, module);
+ }
// Track which functions we changed, and optimize them later if necessary.
std::unordered_set<Function*> changed;
// We now know which parameters are unused, and can potentially remove them.
@@ -444,7 +458,7 @@ struct DAE : public Pass {
if (optimize && !changed.empty()) {
OptUtils::optimizeAfterInlining(changed, module, runner);
}
- return !changed.empty();
+ return !changed.empty() || refinedReturnTypes;
}
private:
@@ -591,6 +605,74 @@ private:
}
}
}
+
+ // See if the types returned from a function allow us to define a more refined
+ // return type for it. If so, we can update it and all calls going to it.
+ //
+ // This assumes that the function has no calls aside from |calls|, that is, it
+ // is not exported or called from the table or by reference. Exports should be
+ // fine, as should indirect calls in principle, but VMs will need to support
+ // function subtyping in indirect calls. TODO: relax this when possible
+ //
+ // Returns whether we optimized.
+ //
+ // TODO: We may be missing a global optimum here, as e.g. if a function calls
+ // itself and returns that value, then we would not do any change here,
+ // as one of the return values is exactly what it already is. Similar
+ // unoptimality can happen with multiple functions, more local code in
+ // the middle, etc.
+ bool refineReturnTypes(Function* func,
+ const std::vector<Call*>& calls,
+ Module* module) {
+ if (!module->features.hasGC()) {
+ return false;
+ }
+
+ Type originalType = func->getResults();
+ if (!originalType.hasRef()) {
+ // Nothing to refine.
+ return false;
+ }
+
+ // Before we do anything, we must refinalize the function, because otherwise
+ // its body may contain a block with a forced type,
+ //
+ // (func (result X)
+ // (block (result X)
+ // (..content with more specific type Y..)
+ // )
+ ReFinalize().walkFunctionInModule(func, module);
+
+ Type refinedType = func->body->type;
+ if (refinedType == originalType) {
+ return false;
+ }
+
+ // Scan the body and look at the returns.
+ for (auto* ret : FindAll<Return>(func->body).list) {
+ refinedType = Type::getLeastUpperBound(refinedType, ret->value->type);
+ if (refinedType == originalType) {
+ return false;
+ }
+ }
+ assert(refinedType != originalType);
+
+ // If the refined type is unreachable then nothing actually returns from
+ // this function.
+ // TODO: We can propagate that to the outside, and not just for GC.
+ if (refinedType == Type::unreachable) {
+ return false;
+ }
+
+ // Success. Update the type, and the calls.
+ func->setResults(refinedType);
+ for (auto* call : calls) {
+ if (call->type != Type::unreachable) {
+ call->type = refinedType;
+ }
+ }
+ return true;
+ }
};
Pass* createDAEPass() { return new DAE(); }
diff --git a/test/lit/passes/dae-gc.wast b/test/lit/passes/dae-gc.wast
index b3f8eddd2..f80673cf2 100644
--- a/test/lit/passes/dae-gc.wast
+++ b/test/lit/passes/dae-gc.wast
@@ -9,10 +9,11 @@
(type ${i32} (struct (field i32)))
;; CHECK: (type ${i32_i64} (struct (field i32) (field i64)))
+ ;; CHECK: (type ${i32_f32} (struct (field i32) (field f32)))
+
;; CHECK: (type ${f64} (struct (field f64)))
(type ${f64} (struct (field f64)))
(type ${i32_i64} (struct (field i32) (field i64)))
- ;; CHECK: (type ${i32_f32} (struct (field i32) (field f32)))
(type ${i32_f32} (struct (field i32) (field f32)))
;; CHECK: (func $foo
@@ -293,4 +294,251 @@
;; "Use" the local to avoid other optimizations kicking in.
(drop (local.get $x))
)
+
+ ;; We cannot refine the return type if nothing is actually returned.
+ ;; CHECK: (func $refine-return-no-return (result anyref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-no-return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (unreachable)
+ ;; CHECK-NEXT: )
+ (func $refine-return-no-return (result anyref)
+ ;; Call this function, so that we attempt to optimize it. Note that we do not
+ ;; just drop the result, as that would cause the drop optimizations to kick
+ ;; in.
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-no-return))
+
+ (unreachable)
+ )
+
+ ;; We cannot refine the return type if it is already the best it can be.
+ ;; CHECK: (func $refine-return-no-refining (result anyref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-no-refining)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null any)
+ ;; CHECK-NEXT: )
+ (func $refine-return-no-refining (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-no-refining))
+
+ (ref.null any)
+ )
+
+ ;; Refine the return type based on the value flowing out.
+ ;; CHECK: (func $refine-return-flow (result funcref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-flow)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ (func $refine-return-flow (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-flow))
+
+ (ref.null func)
+ )
+ ;; CHECK: (func $call-refine-return-flow (result funcref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $call-refine-return-flow)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if (result funcref)
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $refine-return-flow)
+ ;; CHECK-NEXT: (call $refine-return-flow)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $call-refine-return-flow (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $call-refine-return-flow))
+
+ ;; After refining the return value of the above function, refinalize will
+ ;; update types here, which will lead to updating the if, and then the entire
+ ;; function's return value.
+ (if (result anyref)
+ (i32.const 1)
+ (call $refine-return-flow)
+ (call $refine-return-flow)
+ )
+ )
+
+ ;; Refine the return type based on a return.
+ ;; CHECK: (func $refine-return-return (result funcref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $refine-return-return (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-return))
+
+ (return (ref.null func))
+ )
+
+ ;; Refine the return type based on multiple values.
+ ;; CHECK: (func $refine-return-many (result funcref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-many)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ (func $refine-return-many (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-many))
+
+ (if
+ (i32.const 1)
+ (return (ref.null func))
+ )
+ (if
+ (i32.const 2)
+ (return (ref.null func))
+ )
+ (ref.null func)
+ )
+
+ ;; CHECK: (func $refine-return-many-blocked (result anyref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-many-blocked)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null data)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ (func $refine-return-many-blocked (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-many-blocked))
+
+ (if
+ (i32.const 1)
+ (return (ref.null func))
+ )
+ (if
+ (i32.const 2)
+ ;; The refined return value is blocked by this return.
+ (return (ref.null data))
+ )
+ (ref.null func)
+ )
+
+ ;; CHECK: (func $refine-return-many-blocked-2 (result anyref)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-many-blocked-2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null data)
+ ;; CHECK-NEXT: )
+ (func $refine-return-many-blocked-2 (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-many-blocked-2))
+
+ (if
+ (i32.const 1)
+ (return (ref.null func))
+ )
+ (if
+ (i32.const 2)
+ (return (ref.null func))
+ )
+ ;; The refined return value is blocked by this value.
+ (ref.null data)
+ )
+
+ ;; CHECK: (func $refine-return-many-middle (result (ref null ${i32}))
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (call $refine-return-many-middle)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (ref.null ${i32_i64})
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (ref.null ${i32_f32})
+ ;; CHECK-NEXT: )
+ (func $refine-return-many-middle (result anyref)
+ (local $temp anyref)
+ (local.set $temp (call $refine-return-many-middle))
+
+ ;; Return two different struct types, with an LUB that is not equal to either
+ ;; of them.
+ (if
+ (i32.const 1)
+ (return (ref.null ${i32_i64}))
+ )
+ (ref.null ${i32_f32})
+ )
+
+ ;; We can refine the return types of tuples.
+ ;; CHECK: (func $refine-return-tuple (result funcref i32)
+ ;; CHECK-NEXT: (local $temp anyref)
+ ;; CHECK-NEXT: (local.set $temp
+ ;; CHECK-NEXT: (tuple.extract 0
+ ;; CHECK-NEXT: (call $refine-return-tuple)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (tuple.make
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $refine-return-tuple (result anyref i32)
+ (local $temp anyref)
+ (local.set $temp
+ (tuple.extract 0
+ (call $refine-return-tuple)
+ )
+ )
+
+ (tuple.make
+ (ref.null func)
+ (i32.const 1)
+ )
+ )
)