Skip to content
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

Fix interpretation of return_call* #6451

Merged
merged 4 commits into from
Apr 8, 2024

Conversation

tlively
Copy link
Member

@tlively tlively commented Mar 28, 2024

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.

@tlively tlively requested a review from kripken March 28, 2024 19:13
@tlively
Copy link
Member Author

tlively commented Mar 28, 2024

@kripken
Copy link
Member

kripken commented Mar 28, 2024

I'll try to find time to review this later today, but I suggest that we merge this PR with the Inlining PR. Landing them as a single commit will avoid future bisection problems. (Otherwise, imagine someone bisects a testcase that involves return_call and inlining as incidental details, then when bisection includes only the inlining fix the testcase could break because of the inlining/interpreter discrepancy.)

@tlively
Copy link
Member Author

tlively commented Mar 28, 2024

Sounds good. We can merge this PR into the previous PR first once they're both ready to go.

Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code lgtm, but where in the tests is the difference noticeable? I was expecting to see a thrown exception somewhere.

@tlively
Copy link
Member Author

tlively commented Mar 29, 2024

The only noticeable difference in the tests is that we can recursively tail-call functions like even and count without encountering the function depth limit.

I can add an explicit test for the EH case if you want.

@tlively tlively force-pushed the interpret-return-call branch 2 times, most recently from 761e417 to e4f8a1d Compare March 29, 2024 19:46
@kripken
Copy link
Member

kripken commented Mar 29, 2024

Yes, I think an explicit test is worth it here.

@tlively
Copy link
Member Author

tlively commented Mar 29, 2024

Done, PTAL.

@tlively
Copy link
Member Author

tlively commented Mar 29, 2024

Heh, looks like that test uncovered a bug in the optimizer. Will investigate.

@tlively
Copy link
Member Author

tlively commented Mar 30, 2024

It turns out I was wrong about that cast to block, so I replaced that with a blockify() in the last commit. Meanwhile the fuzzer has found that wasm-ctor-eval needs updating to handling the new RETURN_CALL_FLOW correctly, so I'm taking a look at adding proper support for tail calls there.

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.
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.
@tlively tlively merged commit dce7c35 into return-call-inlining Apr 8, 2024
0 of 13 checks passed
@tlively tlively deleted the interpret-return-call branch April 8, 2024 17:04
tlively added a commit that referenced this pull request Apr 8, 2024
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants