summaryrefslogtreecommitdiff
path: root/src/passes/param-utils.cpp
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2022-08-18 09:13:47 -0700
committerGitHub <noreply@github.com>2022-08-18 09:13:47 -0700
commit0c9ffd618ba69128caa61583a7cd9a831fef1d98 (patch)
tree4e97b43c4d8660213cfd160354a78b4b8c3c8eef /src/passes/param-utils.cpp
parent8a92ab5f5f0529fec4b71223858fac853837884e (diff)
downloadbinaryen-0c9ffd618ba69128caa61583a7cd9a831fef1d98.tar.gz
binaryen-0c9ffd618ba69128caa61583a7cd9a831fef1d98.tar.bz2
binaryen-0c9ffd618ba69128caa61583a7cd9a831fef1d98.zip
Fix DeadArgumentElimination + TrapsNeverHappen to not leave stale types (#4910)
DAE will normally not remove an unreachable parameter, because it checks for effects there. But in TrapsNeverHappen mode, we assume that an unreachable is an effect we can remove, so we are willing to remove it: (func $foo (param $unused i32) ;; never use $unused ) (func $bar (call $foo (unreachable))) ;;=> dae+tnh (func $foo ) (func $bar (call $foo)) But that transformation is invalid: the call's type was unreachable before but no longer is. What went wrong here is that, yes, it is valid to remove an unreachable, but we may need to update types while doing so, which we were not doing. This wasn't noticed before due to a combination of unfortunate factors: The main reason is that this only happens in TrapsNeverHappens mode. We don't fuzz that, because it's difficult: that mode can assume a trap never happens, so a trap is undefined behavior really. On real-world code this is great, but in the fuzzer it means that the output can seem to change after optimizations. The validator happened to be missing an error for a call that has type unreachable but shouldn't: Validator: Validate unreachable calls more carefully #4909 . Without that, we'd only get an error if the bad type influenced a subsequent pass in a confusing way - which is possible, but difficult to achieve (what ended up happening in practice is that SignatureRefining on J2Wasm relied on the unreachable and refined a type too much). Even with that fix, for the problem to be detected we'd need for the validation error to happen in the final output, after running all the passes. In practice, though, that's not likely, since other passes tend to remove unreachables etc. Pass-debug mode is very useful for finding stuff like this, as it validates after every individual pass. Sadly it turns out that global validation was off there: Validator: Validate globally by default #4906 (so it was catching the 99% of validation errors that are local, but this particular error was in the remaining 1%...). As a fix, simply ignore this case. It's not really worth the effort to optimize it, since DCE will just remove unreachables like that anyhow. So if we run again after a DCE we'd get a chance to optimize. This updates some existing tests to avoid (unreachable). That was used as an example of something with effects, but after this change it is treated more carefully. Replace those things with something else that has effects (a call).
Diffstat (limited to 'src/passes/param-utils.cpp')
-rw-r--r--src/passes/param-utils.cpp30
1 files changed, 25 insertions, 5 deletions
diff --git a/src/passes/param-utils.cpp b/src/passes/param-utils.cpp
index cf7873dc0..3b0721173 100644
--- a/src/passes/param-utils.cpp
+++ b/src/passes/param-utils.cpp
@@ -62,20 +62,40 @@ bool removeParameter(const std::vector<Function*>& funcs,
// Check if none of the calls has a param with side effects that we cannot
// remove (as if we can remove them, we will simply do that when we remove the
// parameter). Note: flattening the IR beforehand can help here.
- auto hasBadEffects = [&](ExpressionList& operands) {
- return EffectAnalyzer(runner->options, *module, operands[index])
- .hasUnremovableSideEffects();
+ //
+ // It would also be bad if we remove a parameter that causes the type of the
+ // call to change, like this:
+ //
+ // (call $foo
+ // (unreachable))
+ //
+ // After removing the parameter the type should change from unreachable to
+ // something concrete. We could handle this by updating the type and then
+ // 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 &&
+ std::any_of(operands.begin(), operands.end(), [](Expression* operand) {
+ return operand->type == Type::unreachable;
+ });
+ return hasUnremovable || wouldChangeType;
};
bool callParamsAreValid =
std::none_of(calls.begin(), calls.end(), [&](Call* call) {
- return hasBadEffects(call->operands);
+ return hasBadEffects(call);
});
if (!callParamsAreValid) {
return false;
}
bool callRefParamsAreValid =
std::none_of(callRefs.begin(), callRefs.end(), [&](CallRef* call) {
- return hasBadEffects(call->operands);
+ return hasBadEffects(call);
});
if (!callRefParamsAreValid) {
return false;