-
Notifications
You must be signed in to change notification settings - Fork 827
Update effects.h to handle return calls correctly #6470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
@kripken, any suggestions you have about how to easily test this would be much appreciated. |
|
How does a non-exception effect get noticed here? That is, when we discussed this before we talked about stashing a thrown exception on the side, and not all the effects. This does more work to stash them all, and I'm not sure why yet. (I get that technically the call executes after the return, so effects happen "later" - but it seems simpler and sufficient to track only exceptions IIANM.) For testing throwing, you need global effects, see |
|
My hope is that more precisely analyzing return call effects would lead to more optimization opportunities, but maybe that's not the case if control flow transfers cannot be reordered with any other side effect? |
|
Do you mean the specific meaning of |
668f7ec to
c6497ac
Compare
503afcf to
de817ec
Compare
|
Ok, I implemented the simplified version that only stashes the Even if it doesn't make a functional difference now, being more precise by stashing all the effects can only possibly produce better results in the face of future optimizations for which it might make a difference. It also means we won't have to redo any of this work in the future if/when we add separate |
src/ir/effects.h
Outdated
| // code, but the effects of the return-called functions are visible to | ||
| // callers. Collect effects of return-called functions here and merge them in | ||
| // only when analyzing an entire function. | ||
| EffectSet returnCallEffects; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may double the size of EffectAnalyzer. Making it a unique_ptr might be better?
Though even that is worse than a bool.
|
I see your point that stashing effects could in theory help in the future. But do you see a practical way that could end up happening? That's what I am finding hard to see. From my perspective, a return call is like a call except with a slight reordering, which has the quirk of causing the call to happen outside of current scoping rules. But that is all. And we do not track a CFG here, so tracking where the branch goes to is beyond the scope of this class - for that reason I am skeptical it will ever benefit from the theoretical additional precision. |
|
Yeah, I guess I'm convinced that stashing will only ever be useful for effects that interact with scopes like throwing. Ok, back to the single |
src/ir/effects.h
Outdated
|
|
||
| parent.calls = true; | ||
| if (parent.features.hasExceptionHandling() && | ||
| (parent.tryDepth == 0 || curr->isReturn)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| (parent.tryDepth == 0 || curr->isReturn)) { | |
| (parent.tryDepth == 0 && !curr->isReturn)) { |
|
Fuzzer seems happy with this. 20k+ iterations passed (without the last commit, which I'm fuzzing now). |
|
Two other possible tests. It seems the code folding pass could be improved to deal better with tail calls. I had to add a (func $code-folding (param $x i32)
(try
(do
(drop (i32.const 1))
(drop (i32.const 1))
(return_call $throw)
(return)
)
(catch_all
(drop (i32.const 1))
(drop (i32.const 1))
(return_call $throw)
(return)
)
)
)
(func $simplify-locals (param $x i32)
(local $y i32)
(local.set $y
(if (result i32)
(local.get $x)
(then
(local.get $x)
)
(else
(return_call $throw)
)
)
)
(try
(do
(drop
(local.get $y)
)
)
(catch_all)
)
) |
|
Thanks for the suggestions, @vouillon! |
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 effects of return-callees on the side and only merge them in at the end when analyzing the effects of a full function body.
This reverts commit 40f26ee.
c6497ac to
36a6b21
Compare
b4affd2 to
5ff3513
Compare
kripken
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm % comments
src/ir/effects.h
Outdated
| // 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 `hasReturnCallThrow`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // captured by `hasReturnCallThrow`. | |
| // captured by `branchesOut`. That is, return calls add`branchesOut` which | |
| // represents the fact that we are branching to another place that may | |
| // have more effects, which can include throwing or anything else that | |
| // happens when the call executes. |
src/ir/effects.h
Outdated
| 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 | ||
| // `hasReturnCallThrow`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // `hasReturnCallThrow`. | |
| // `branchesOut`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
branchesOut isn't related to the throw effect, though. It's the fact that we set hasReturnCallThrow that ensures the throw is eventually reported correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, but the context here is removing the throws effect on the next line. It is only valid to remove the throws effect because branchesOut indicates "there is a branch from here to some other effects". Those other effects are hasReturnCallThrow, but those do not come into play normally - not unless you walk the entire function. So it's the combination of the two, but branchesOut is the more immediate thing that "replaces" the throws we are removing, it kind of points to where the effects are at.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider that we still apply branchesOut even if the callee does not throw, though.
I'll update the comment to mention both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do, yes. But in the return_call case that has the additional important meaning of "linking" us to the effects we are removing here. So it explains why the removal of throws is valid in that case. That is, branchesOut becomes even more significant for return calls given this optimization here, and if we some day refactor branchesOut we'd need to consider this place.
| (catch_all) | ||
| ) | ||
| ;; This cannot be optimized out at all. | ||
| (call $return-call-throw-and-catch) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please test return_call_indirect/ref as well, so we have coverage for them adding the hasReturnCallThrow field. (IIANM the tests below test the other property, that they do not have throws_ set and so they can be optimized better.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests, but I'm not sure they test what you're looking for because the callees also have the calls effect, which is enough to inhibit optimizations on its own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess calls masks this. Still, those tests look useful - maybe some day we'll make calls more refined somehow, and these would become more important.
Co-authored-by: Alon Zakai <azakai@google.com>
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.
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.
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.

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 effects of return-callees on the side and
only merge them in at the end when analyzing the effects of a full function
body.