Skip to content

Conversation

@333fred
Copy link
Member

@333fred 333fred commented May 30, 2025

Our current approach to rewriting await expressions in finally blocks is technically unsound: the rewrite can result in a new exit block from the original method being created. As a simple example, here is an original method, and what it gets rewritten to by AsyncExceptionHandlerRewriter (slightly simplified):

static async Task<int> M()
{
    int x = 0;

    try
    {
        x = await F();
        return x;
    }
    finally
    {
        x += await F();
    }
}
async Task<int> M()
{
    int x = 0;
    object ex = null;
    int retTaken = 0;
    int retTemp = 0;
    try
    {
        x = await F();
        goto @finally;
    }
    catch (object o)
    {
        ex = o;
        goto @finally;
    }

@finally:
    x += await F();
    if (ex != null)
    {
        if (ex is Exception e) ExceptionDispatchInfo.Capture(e).Throw();
        throw ex;
    }

    switch(retTaken)
    {
        case 2:
            return retTemp;
        default:
            break; // This is the problem
    }
}

While we can statically walk the method and prove via flow analysis that the default case of the switch isn't reachable, the generalized version of this problem is the halting problem. To date, this has never been a problem because the code that the async rewriter produces is then wrapped and rewritten again into the async state machine, and the missing return becomes completely unobservable. However, for runtime async, we're going to be emitting this general structure, essentially unchanged: that break;, then, becomes a branch to an invalid IL location and the method ends without a ret. We're taking a conservative approach to fixing this by appending a throw null; at the end of every method that has an await in a finally. Most of the time, this is provably never reachable, and the basic block reducer will eliminate it. However, sometimes a standard control flow analysis will not be able to prove that the throw null; is not reachable, and it will remain in the final IL. It is not reachable, but its presence will ensure that the method is always valid.

async Task<int> M()
{
    int x = 0;
    object ex = null;
    int retTaken = 0;
    int retTemp = 0;
    try
    {
        x = await F();
        goto @finally;
    }
    catch (object o)
    {
        ex = o;
        goto @finally;
    }

@finally:
    x += await F();
    if (ex != null)
    {
        if (ex is Exception e) ExceptionDispatchInfo.Capture(e).Throw();
        throw ex;
    }

    switch(retTaken)
    {
        case 2:
            return retTemp;
        default:
            break;
    }

    throw null; // Unreachable, but not easily provable statically
}

Relates to test plan #75960

@333fred 333fred requested a review from a team as a code owner May 30, 2025 20:53
@333fred 333fred marked this pull request as draft May 30, 2025 20:53
@333fred 333fred requested review from RikkiGibson and jcouv May 30, 2025 20:53
@333fred

This comment was marked as outdated.

@333fred 333fred force-pushed the rewrite-try-catch branch 2 times, most recently from 35ef3fa to 7a32ac8 Compare June 6, 2025 21:23
@333fred 333fred force-pushed the rewrite-try-catch branch from 7a32ac8 to b0ec3c2 Compare June 18, 2025 03:52
@333fred
Copy link
Member Author

333fred commented Jun 18, 2025

@jcouv @RikkiGibson this is ready for review. I'll likely have another commit sometime tomorrow that also adds runtime async verification for await foreach tests, but I don't anticipate that to change any of the core implementation changes so I think you should be good to proceed with the initial set.

{
result = _F.Block(
loweredStatement,
_F.Throw(_F.Null(_F.SpecialType(SpecialType.System_Object)))
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

_F.Throw(_F.Null(_F.SpecialType(SpecialType.System_Object)))

Is this going to work for async void methods? Will there be an explicit return after all user code in the method is exhausted? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I hope it will, since I expect the automatic implicit return code to not care about whether it's just void or async void, but it's a good test to add.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the automatic implicit return worked here. See AsyncInFinally006_AsyncVoid, newly added.

@RikkiGibson RikkiGibson self-assigned this Jun 20, 2025
Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

getting through tests going to take a little while.

Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Done review pass, had some comments on the tests.

awaitInfo = BindAwaitInfo(placeholder, expr, diagnostics, ref hasErrors);

if (!hasErrors && awaitInfo.GetResult?.ReturnType.SpecialType != SpecialType.System_Boolean)
if (!hasErrors && (awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod)?.ReturnType.SpecialType != SpecialType.System_Boolean)
Copy link
Member

Choose a reason for hiding this comment

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

(awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod

Can awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod be null when there's no errors?
I still think it's be a good idea to add a Validate method to BoundAwaitableInfo to codify expectations.

Copy link
Member Author

Choose a reason for hiding this comment

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

It can, when dynamic is involved.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, I made the following addition and ran build.cmd -testCompilerOnly -testCoreClr and go no hit:

+                if (!hasErrors)
+                {
+                    Debug.Assert((awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod) is not null);
+                }

Copy link
Member Author

Choose a reason for hiding this comment

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

It's possible dynamically-bound awaits do not go through here, but you absolutely can have both methods be null and not have errors.

@jcouv
Copy link
Member

jcouv commented Jun 24, 2025

            // (3,1): hidden CS8019: Unnecessary using directive.

Test AsyncWithEH, line 150: Do we understand what API we used for async that we no longer use for runtime-async?


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncEHTests.cs:150 in 92399e0. [](commit_id = 92399e0, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Jun 24, 2025

                    [G]: Unexpected type on the stack. { Offset = 0x104, Found = Int32, Expected = ref 'System.Threading.Tasks.Task`1<int32>' }

Test AsyncWithEHCodeQuality, line 587: Did we file an issue on ILVerify already? It'd be good to link here and other similar places


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncEHTests.cs:587 in 92399e0. [](commit_id = 92399e0, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Jun 24, 2025

        verifier.VerifyDiagnostics(

nit: Test AsyncWithException1, line 690: Consider also verifying diagnostics on the async baseline above, and possibly removing the async from F
Also applies to some other tests below (AsyncWithException2 and many more)


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncEHTests.cs:690 in 503c056. [](commit_id = 503c056, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Jun 24, 2025

                IL_0145:  ldnull

nit: Thanks for pointing out this test. Consider leaving a comment for what to look for in the IL (this is an async test where the new "throw null" is survives and is observed). Or maybe in AsyncInFinally003 since it already had an IL baseline #Closed


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncEHTests.cs:1749 in 503c056. [](commit_id = 503c056, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Jun 24, 2025

        verifier.VerifyIL("Test.G(System.Threading.SemaphoreSlim)", """

Test AsyncInFinally006_AsyncVoid_01, line 2059 (last VerifyIL): Was there anything in particular to observe here? This should not have been affected by the change in this PR, right?


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncEHTests.cs:2059 in 503c056. [](commit_id = 503c056, deletion_comment = False)

@333fred
Copy link
Member Author

333fred commented Jun 24, 2025

Was there anything in particular to observe here? This should not have been affected by the change in this PR, right?

Correct, I'm verifying that fact.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

Done with review pass (commit 14)

@333fred
Copy link
Member Author

333fred commented Jun 24, 2025

Do we understand what API we used for async that we no longer use for runtime-async?

None. The non-runtime async code does not verify diagnostics, this is present on both.

@333fred
Copy link
Member Author

333fred commented Jun 24, 2025

nit: Thanks for pointing out this test. Consider leaving a comment for what to look for in the IL (this is an async test where the new "throw null" is survives and is observed). Or maybe in AsyncInFinally003 since it already had an IL baseline

I'm not sure that comment would be especially useful after this review?

@333fred
Copy link
Member Author

333fred commented Jun 24, 2025

nit: Test AsyncWithException1, line 690: Consider also verifying diagnostics on the async baseline above, and possibly removing the async from F

For the former; to be honest, I don't think the effort is worth it here. For the latter, that would potentially change the behavior of the test unless I did more extensive rewrites to change to Task.FromException, which I also don't think is worth it.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (commit 14)

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (commit 16)

@333fred 333fred merged commit c2fff44 into dotnet:features/runtime-async Jun 25, 2025
24 checks passed
@333fred 333fred deleted the rewrite-try-catch branch June 25, 2025 16:52
333fred added a commit that referenced this pull request Aug 19, 2025
* Handle basic await scenarios (#76121)

Add initial handling of expressions that return `Task`, `Task<T>`, `ValueTask`, `ValueTask<T>`.

* Add RuntimeAsyncMethodGenerationAttribute (#77543)

Adds control for whether to use runtime async. The flowchart is as follows:

1. The flag `System.Runtime.CompilerServices.RuntimeFeature.Async` must be present.
2. Assuming that flag is present, we look for the presence of `System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute` on the method. If that attribute is present, we use the preference expressed in the attribute. The preference does not carry to nested contexts, such as local functions or lambdas.
3. If the attribute is not present, we look for `features:runtime-async=on` on the command line. If that is present, then the feature is on by default. Otherwise, the feature is off.

* Semantic search info

* Implement custom awaitable support (#78071)

This adds support for awaiting task-like types that are not natively supported by runtime async. Closes #77897.

* Move runtime async method validation into initial binding (#78310)

We now do method construction and validation for runtime async helpers up front in initial binding, rather than doing it in `RuntimeAsyncRewriter`. I've also renamed the APIs as per dotnet/runtime#114310 (comment) (though I haven't added ConfigureAwait support yet, that will be the next PR). We now validate:

* The helpers come from `System.Runtime.CompilerServices.AsyncHelpers`, defined in corelib. This means that I now need a fairly extensive corelib mock to be able to compile. When we have a testing runtime that defines these helpers, we can remove the giant mock and use the real one.
* We properly error when expected helpers aren't present.
* We properly check to make sure that constraints are satisfied when doing generic substitution in one of the runtime helpers.
* Runtime async is not turned on if the async method does not return `Task`, `Task<T>`, `ValueTask`, or `ValueTask<T>`.

Relates to test plan #75960

* React to main changes #78246 and #78231.

* Switch MethodImplAttributes.Async to 0x2000 (#78536)

It was changed in dotnet/runtime#114310 as 0x400 is a thing in framework.

* Ensure return local is the correct type for runtime async (#78603)

* Add test demonstrating current behavior

* Ensure return local is the correct type in async scenarios

* Ensure method is actually async when doing local rewrite

* Exception Handler support (#78773)

* Update EH tests to run with runtime async

* Handle non-null exception filter prologues in the spill sequencer

* Add more testing to show current incorrect behavior

* Unskip ConditionalFacts that do not need to be skipped.

* Handle ensuring that the method remains valid, even when there is an `await` in a finally section. Add signifcant testing of `await using`.

* Fix baselines

* Support `await foreach` and add runtime async verification to existing tests.

* Remove unnecessary generic

* Failing tests, add async void test suggestion

* CI failures

* Add additional test

* Test fixes

* Remove implemented PROTOTYPE, add assertion on behavior.

* Update to SpillSequenceSpiller after some more debugging and tightening the assertion

* Fix nullref

* Enable nullable for VisitCatchBlock

* Support using a simple overload resolution for finding Await helpers from the BCL (#79135)

* Support using a simple overload resolution for finding Await helpers from the BCL

This PR removes special knowledge of what `Await` helpers correspond to what types, and instead implements a very simple form of overload resolution. We immediately bail on any conflict or error and fall back to attempting to use `AwaitAwaiter` or `UnsafeAwaitAwaiter` when such scenarios are detected. I've also updated the rules to better reflect what is actually implementable.

* Create the full BoundCall in initial binding.

* PR feedback.

* Baseline struct lifting tests (#79505)

* Extract expectedOutput constants, minor touchups

* Rename expected -> expectedOutput

* Include new testing with placeholder baselines

* Progress

* First ILVerify pass

* Initial baseline IL run.

* Further baseline additions and skips based on missing corelib apis.

* Clone async void tests and have them use async Task, and validate existing code spit for these under runtime async

* Update baselines after .NET 10 intake

* Delete the stub

* Remove long dynamic baseline and leave a prototype.

* Feedback.

* BOM

* Remove unused references parameter

* Block `await dynamic`

* Block hoisted variables from runtime async for now

* Update test baselines for block

* Block arglist in runtime async

* Add IL baseline

* Handle an additional branch beyond the end of the method case.

* Move prototype comments to issues.

* Remove entry point prototypes

* Add assert and comment

* Add back assert

* Report obsolete/experimental diagnostics on await helpers.

* Fix ref safety analysis build error.

---------

Signed-off-by: Emmanuel Ferdman <[email protected]>
Co-authored-by: Jan Jones <[email protected]>
Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: Cyrus Najmabadi <[email protected]>
Co-authored-by: David Barbet <[email protected]>
Co-authored-by: Ankita Khera <[email protected]>
Co-authored-by: David Wengier <[email protected]>
Co-authored-by: Rikki Gibson <[email protected]>
Co-authored-by: Cyrus Najmabadi <[email protected]>
Co-authored-by: Rikki Gibson <[email protected]>
Co-authored-by: akhera99 <[email protected]>
Co-authored-by: Joey Robichaud <[email protected]>
Co-authored-by: Todd Grunke <[email protected]>
Co-authored-by: Youssef1313 <[email protected]>
Co-authored-by: Joey Robichaud <[email protected]>
Co-authored-by: Tomáš Matoušek <[email protected]>
Co-authored-by: Amadeusz Wieczorek <[email protected]>
Co-authored-by: Charles Stoner <[email protected]>
Co-authored-by: Jared Parsons <[email protected]>
Co-authored-by: Sam Harwell <[email protected]>
Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: Jason Malinowski <[email protected]>
Co-authored-by: Etienne Baudoux <[email protected]>
Co-authored-by: AlekseyTs <[email protected]>
Co-authored-by: Jan Jones <[email protected]>
Co-authored-by: Maryam Ariyan <[email protected]>
Co-authored-by: Andrew Hall <[email protected]>
Co-authored-by: Arun Chander <[email protected]>
Co-authored-by: Kauwai Lucchesi <[email protected]>
Co-authored-by: Bill Wagner <[email protected]>
Co-authored-by: PaddiM8 <[email protected]>
Co-authored-by: Matteo Prosperi <[email protected]>
Co-authored-by: Julien Couvreur <[email protected]>
Co-authored-by: Matteo Prosperi <[email protected]>
Co-authored-by: Carlos Sánchez López <[email protected]>
Co-authored-by: Tomas Matousek <[email protected]>
Co-authored-by: Deepak Rathore (ALLYIS INC) <[email protected]>
Co-authored-by: Emmanuel Ferdman <[email protected]>
Co-authored-by: Evgeny Tvorun <[email protected]>
Co-authored-by: Victor Pogor <[email protected]>
Co-authored-by: Ella Hathaway <[email protected]>
Co-authored-by: Viktor Hofer <[email protected]>
Co-authored-by: Jason Malinowski <[email protected]>
Co-authored-by: DoctorKrolic <[email protected]>
Co-authored-by: John Douglas Leitch <[email protected]>
Co-authored-by: Matt Thalman <[email protected]>
Co-authored-by: Bernd Baumanns <[email protected]>
Co-authored-by: Thomas Shephard <[email protected]>
Co-authored-by: DoctorKrolic <[email protected]>
Co-authored-by: David Barbet <[email protected]>
Co-authored-by: Chris Sienkiewicz <[email protected]>
Co-authored-by: tmat <[email protected]>
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Gen Lu <[email protected]>
Co-authored-by: Oleg Tkachenko <[email protected]>
Co-authored-by: Matt Mitchell <[email protected]>
Co-authored-by: Djuradj Kurepa <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Stuart Lang <[email protected]>
Co-authored-by: RaymondY <[email protected]>
Co-authored-by: Gobind Singh <[email protected]>
Co-authored-by: David Kean <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants