JIT: Fix edge cases around async call definitions with exceptional flow#121583
Merged
Conversation
We had special logic to optimize the case where an async call defines a
local, avoiding capturing the local as part of the live state since its
value would be overridden on resumption anyway.
This does not handle the case where the callee throws an exception and
we continue internally in the function. In that case we may still have
needed to capture the local state. Consider the following example:
```csharp
int x = GetValue();
try
{
x = await IntThrows();
}
catch
{
}
return x;
```
Here we need to capture the value of `x` in the continuation as we need
it if `IntThrows` throws. The previous logic optimized away this
capture.
For cases where we have liveness for the defined local we can use
liveness for precision when we have exceptional flow. Liveness will tell
us that `x` is live on the `IntThrows` call here, precisely because of
the exceptional flow. If there was no exceptional flow, `x` would not be
live. We can still use the previous optimization for address exposed or
tier0 cases.
Also fix when we update the live set around the async calls we are
transforming. Before this change we were updating it before the
transformation. This handling was imprecise for return buffer
definitions, but it would not usually cause problems beyond
overestimating the live set.
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
This reverts commit e6fa1b5.
jakobbotsch
commented
Nov 13, 2025
3 tasks
Removed nested try-finally and simplified exception handling.
Contributor
There was a problem hiding this comment.
Pull Request Overview
This PR fixes edge cases in the JIT's async call handling where exceptional flow (try/catch/finally blocks) could lead to incorrect optimization of local variable capture. Previously, locals fully defined by an async call were optimized away from the continuation state, but this was incorrect when the callee threw an exception that was caught internally, as the original value of the local was still needed.
Key changes:
- Introduced liveness-aware handling of exceptional flow for async call definitions
- Added new helper method
HasNonContextRestoreExceptionalFlowto detect when exceptional flow requires preserving local state - Moved liveness updates to occur at the correct time during async call transformation
- Enabled and added comprehensive test cases covering both value types and struct types with exceptional flow
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/async/simple-eh/simple-eh.csproj | Removes DisableProjectBuild flag to enable the test execution |
| src/tests/async/simple-eh/simple-eh.cs | Adds comprehensive test cases for async calls that define locals but throw exceptions, covering both primitive int types and struct types |
| src/coreclr/jit/importercalls.cpp | Renames function from impSetupAndSpillForAsyncCall to impSetupAsyncCall to reflect the simplified functionality |
| src/coreclr/jit/compiler.h | Updates function declaration to match the renamed function |
| src/coreclr/jit/async.h | Adds declaration for new helper method HasNonContextRestoreExceptionalFlow |
| src/coreclr/jit/async.cpp | Implements the core fix: adds exceptional flow detection, updates liveness tracking timing, and improves canonicalization logic to preserve locals when needed for exception handling |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Member
Author
|
cc @dotnet/jit-contrib PTAL @AndyAyersMS |
AndyAyersMS
approved these changes
Nov 14, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
We had special logic to optimize the case where an async call defines a local, avoiding capturing the local as part of the live state since its value would be overridden on resumption anyway.
This does not handle the case where the callee throws an exception and we continue internally in the function. In that case we may still have needed to capture the local state. Consider the following example:
Here we need to capture the value of
xin the continuation as we need it ifIntThrowsthrows. The previous logic optimized away this capture.For cases where we have liveness for the defined local we can use liveness for precision when we have exceptional flow. Liveness will tell us that
xis live on theIntThrowscall here, precisely because of the exceptional flow. If there was no exceptional flow,xwould not be live. We can still use the previous optimization for address exposed or tier0 cases.Also fix when we update the live set around the async calls we are transforming. Before this change we were updating it before the transformation. This handling was imprecise for return buffer definitions, but it would not usually cause problems beyond overestimating the live set.
Fix #121564