summaryrefslogtreecommitdiff
path: root/src/ir/effects.h
diff options
context:
space:
mode:
authorThomas Lively <tlively@google.com>2024-04-08 10:50:35 -0700
committerGitHub <noreply@github.com>2024-04-08 17:50:35 +0000
commitad097394dff569afb42bc4c1c4d961faad04fc81 (patch)
treee67ebefa77d27b29140160cce4111c2f34690b4a /src/ir/effects.h
parentf0dd9941de2df62e0a29f2faeadf007e37a425a9 (diff)
downloadbinaryen-ad097394dff569afb42bc4c1c4d961faad04fc81.tar.gz
binaryen-ad097394dff569afb42bc4c1c4d961faad04fc81.tar.bz2
binaryen-ad097394dff569afb42bc4c1c4d961faad04fc81.zip
Handle return calls correctly
This is a combined commit covering multiple PRs fixing the handling of return calls in different areas. The PRs are all landed as a single commit to ensure internal consistency and avoid problems with bisection. Original PR descriptions follow: * Fix inlining of `return_call*` (#6448) Previously we transformed return calls in inlined function bodies into normal calls followed by branches out to the caller code. Similarly, when inlining a `return_call` callsite, we simply added a `return` after the body inlined at the callsite. These transformations would have been correct if the semantics of return calls were to call and then return, but they are not correct for the actual semantics of returning and then calling. The previous implementation is observably incorrect for return calls inside try blocks, where the previous implementation would run the inlined body within the try block, but the proper semantics would be to run the inlined body outside the try block. Fix the problem by transforming inlined return calls to branches followed by calls rather than as calls followed by branches. For the case of inlined return call callsites, insert branches out of the original body of the caller and inline the body of the callee as a sibling of the original caller body. For the other case of return calls appearing in inlined bodies, translate the return calls to branches out to calls inserted as siblings of the original inlined body. In both cases, it would have been convenient to use multivalue block return to send call parameters along the branches to the calls, but unfortunately in our IR that would have required tuple-typed scratch locals to unpack the tuple of operands at the call sites. It is simpler to just use locals to propagate the operands in the first place. * Fix interpretation of `return_call*` (#6451) We previously interpreted return calls as calls followed by returns, but that is not correct both because it grows the size of the execution stack and because it runs the called functions in the wrong context, which can be observable in the case of exception handling. Update the interpreter to handle return calls correctly by adding a new `RETURN_CALL_FLOW` that behaves like a return, but carries the arguments and reference to the return-callee rather than normal return values. `callFunctionInternal` is updated to intercept this flow and call return-called functions in a loop until a function returns with some other kind of flow. Pull in the upstream spec tests return_call.wast, return_call_indirect.wast, and return_call_ref.wast with light editing so that we parse and validate them successfully. * Handle return calls in wasm-ctor-eval (#6464) When an evaluated export ends in a return call, continue evaluating the return-called function. This requires propagating the parameters, handling the case that the return-called function might be an import, and fixing up local indices in case the final function has different parameters than the original function. * Update effects.h to handle return calls correctly (#6470) As far as their surrounding code is concerned return calls are no different from normal returns. It's only from a caller's perspective that a function containing a return call also has the effects of the return-callee. To model this more precisely in EffectAnalyzer, stash the throw effect of return-callees on the side and only merge it in at the end when analyzing the effects of a full function body.
Diffstat (limited to 'src/ir/effects.h')
-rw-r--r--src/ir/effects.h113
1 files changed, 84 insertions, 29 deletions
diff --git a/src/ir/effects.h b/src/ir/effects.h
index 6901f99de..3ecb54641 100644
--- a/src/ir/effects.h
+++ b/src/ir/effects.h
@@ -67,9 +67,23 @@ public:
// noticeable from the perspective of the caller, that is, effects that are
// only noticeable during the call, but "vanish" when the call stack is
// unwound.
+ //
+ // Unlike walking just the body, walking the function will also
+ // include the effects of any return calls the function makes. For that
+ // reason, it is a bug if a user of this code calls walk(Expression*) and not
+ // walk(Function*) if their intention is to scan an entire function body.
+ // Putting it another way, a return_call is syntax sugar for a return and a
+ // call, where the call executes at the function scope, so there is a
+ // meaningful difference between scanning an expression and scanning
+ // the entire function body.
void walk(Function* func) {
walk(func->body);
+ // Effects of return-called functions will be visible to the caller.
+ if (hasReturnCallThrow) {
+ throws_ = true;
+ }
+
// We can ignore branching out of the function body - this can only be
// a return, and that is only noticeable in the function, not outside.
branchesOut = false;
@@ -143,6 +157,22 @@ public:
// or a continuation that is never continued, are examples of that.
bool mayNotReturn = false;
+ // Since return calls return out of the body of the function before performing
+ // their call, they are indistinguishable from normal returns from the
+ // perspective of their surrounding code, and the return-callee's effects only
+ // become visible when considering the effects of the whole function
+ // containing the return call. To model this correctly, stash the callee's
+ // effects on the side and only merge them in after walking a full function
+ // body.
+ //
+ // We currently do this stashing only for the throw effect, but in principle
+ // we could do it for all effects if it made a difference. (Only throw is
+ // noticeable now because the only thing that can change between doing the
+ // call here and doing it outside at the function exit is the scoping of
+ // try-catch blocks. If future wasm scoping additions are added, we may need
+ // more here.)
+ bool hasReturnCallThrow = false;
+
// Helper functions to check for various effect types
bool accessesLocal() const {
@@ -466,43 +496,63 @@ private:
return;
}
+ const EffectAnalyzer* targetEffects = nullptr;
+ if (parent.funcEffectsMap) {
+ auto iter = parent.funcEffectsMap->find(curr->target);
+ if (iter != parent.funcEffectsMap->end()) {
+ targetEffects = &iter->second;
+ }
+ }
+
if (curr->isReturn) {
parent.branchesOut = true;
+ // When EH is enabled, any call can throw.
+ if (parent.features.hasExceptionHandling() &&
+ (!targetEffects || targetEffects->throws())) {
+ parent.hasReturnCallThrow = true;
+ }
}
- if (parent.funcEffectsMap) {
- auto iter = parent.funcEffectsMap->find(curr->target);
- if (iter != parent.funcEffectsMap->end()) {
- // We have effect information for this call target, and can just use
- // that. The one change we may want to make is to remove throws_, if
- // the target function throws and we know that will be caught anyhow,
- // the same as the code below for the general path.
- const auto& targetEffects = iter->second;
- if (targetEffects.throws_ && parent.tryDepth > 0) {
- auto filteredEffects = targetEffects;
- filteredEffects.throws_ = false;
- parent.mergeIn(filteredEffects);
- } else {
- // Just merge in all the effects.
- parent.mergeIn(targetEffects);
- }
- return;
+ if (targetEffects) {
+ // We have effect information for this call target, and can just use
+ // that. The one change we may want to make is to remove throws_, if the
+ // target function throws and we know that will be caught anyhow, the
+ // same as the code below for the general path. We can always filter out
+ // throws for return calls because they are already more precisely
+ // captured by `branchesOut`, which models the return, and
+ // `hasReturnCallThrow`, which models the throw that will happen after
+ // the return.
+ if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
+ auto filteredEffects = *targetEffects;
+ filteredEffects.throws_ = false;
+ parent.mergeIn(filteredEffects);
+ } else {
+ // Just merge in all the effects.
+ parent.mergeIn(*targetEffects);
}
+ return;
}
parent.calls = true;
- // When EH is enabled, any call can throw.
- if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
+ // When EH is enabled, any call can throw. Skip this for return calls
+ // because the throw is already more precisely captured by the combination
+ // of `hasReturnCallThrow` and `branchesOut`.
+ if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 &&
+ !curr->isReturn) {
parent.throws_ = true;
}
}
void visitCallIndirect(CallIndirect* curr) {
parent.calls = true;
- if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
- parent.throws_ = true;
- }
if (curr->isReturn) {
parent.branchesOut = true;
+ if (parent.features.hasExceptionHandling()) {
+ parent.hasReturnCallThrow = true;
+ }
+ }
+ if (parent.features.hasExceptionHandling() &&
+ (parent.tryDepth == 0 && !curr->isReturn)) {
+ parent.throws_ = true;
}
}
void visitLocalGet(LocalGet* curr) {
@@ -745,21 +795,26 @@ private:
}
}
void visitCallRef(CallRef* curr) {
+ if (curr->isReturn) {
+ parent.branchesOut = true;
+ if (parent.features.hasExceptionHandling()) {
+ parent.hasReturnCallThrow = true;
+ }
+ }
if (curr->target->type.isNull()) {
parent.trap = true;
return;
}
- parent.calls = true;
- if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
- parent.throws_ = true;
- }
- if (curr->isReturn) {
- parent.branchesOut = true;
- }
// traps when the call target is null
if (curr->target->type.isNullable()) {
parent.implicitTrap = true;
}
+
+ parent.calls = true;
+ if (parent.features.hasExceptionHandling() &&
+ (parent.tryDepth == 0 && !curr->isReturn)) {
+ parent.throws_ = true;
+ }
}
void visitRefTest(RefTest* curr) {}
void visitRefCast(RefCast* curr) {