diff --git a/docs/compilers/CSharp/Runtime Async Design.md b/docs/compilers/CSharp/Runtime Async Design.md index f7dc760b13a78..077f233a9a924 100644 --- a/docs/compilers/CSharp/Runtime Async Design.md +++ b/docs/compilers/CSharp/Runtime Async Design.md @@ -27,26 +27,23 @@ We use the following helper APIs to indicate suspension points to the runtime, i ```cs namespace System.Runtime.CompilerServices; -// TODO: Clarify which of these should be preferred? Should we always emit the `Unsafe` version when awaiting something that implements `ICriticalNotifyCompletion`? namespace System.Runtime.CompilerServices; -public static class RuntimeHelpers + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] +public static partial class AsyncHelpers { - // These methods are used to await things that cannot use the Await helpers below - [MethodImpl(MethodImplOptions.Async)] - public static void AwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : INotifyCompletion; - [MethodImpl(MethodImplOptions.Async)] - public static void UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion; + public static void UnsafeAwaitAwaiter(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { } + public static void AwaitAwaiter(TAwaiter awaiter) where TAwaiter : INotifyCompletion { } // These methods are used to directly await method calls - [MethodImpl(MethodImplOptions.Async)] - public static void Await(Task task); - [MethodImpl(MethodImplOptions.Async)] - public static T Await(Task task); - [MethodImpl(MethodImplOptions.Async)] - public static void Await(ValueTask task); - [MethodImpl(MethodImplOptions.Async)] - public static T Await(ValueTask task); + public static void Await(System.Threading.Tasks.Task task) { } + public static T Await(System.Threading.Tasks.Task task) { } + public static void Await(System.Threading.Tasks.ValueTask task) { } + public static T Await(System.Threading.Tasks.ValueTask task) { } + public static void Await(System.Runtime.CompilerServices.ConfiguredTaskAwaitable configuredAwaitable) { } + public static T Await(System.Runtime.CompilerServices.ConfiguredTaskAwaitable configuredAwaitable) { } + public static void Await(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable configuredAwaitable) { } + public static T Await(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable configuredAwaitable) { } } ``` @@ -64,17 +61,6 @@ public enum MethodImplOptions } ``` -Additionally, we use the following helper type to indicate information to the runtime. If this type is not present in the reference assemblies, we will generate it; the runtime matches by full -name, not by type identity, so we do not need to care about using the "canonical" version. - -```cs -namespace System.Runtime.CompilerServices; - -// Used to mark locals that should be hoisted to the generated async closure. Note that the runtime does not guarantee that all locals marked with this modreq will be hoisted; if it can prove that it -// doesn't need to hoist a variable, it may avoid doing so. -public class HoistedLocal(); -``` - For experimentation purposes, we recognize an attribute that can be used to force the compiler to generate the runtime async code, or to force the compiler to generate a full state machine. This attribute is not defined in the BCL, and exists as an escape hatch for experimentation. It may be removed when the feature ships in stable. @@ -90,7 +76,7 @@ public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribut As mentioned previously, we try to expose as little of this to initial binding as possible. The one major exception to this is our handling of the `MethodImplOption.Async`; we do not let this be applied to user code, and will issue an error if a user tries to do this by hand. -TODO: We may need special handling for the implementation of the `RuntimeHelpers.Await` methods in corelib to permit usage of `MethodImplOptions.Async` directly, as they will not be `async` as we think of it in C#. +TODO: We may need special handling for the implementation of the `AsyncHelpers.Await` methods in corelib to permit usage of `MethodImplOptions.Async` directly, as they will not be `async` as we think of it in C#. Compiler generated async state machines and runtime generated async share some of the same building blocks. Both need to have `await`s with in `catch` and `finally` blocks rewritten to pend the exceptions, perform the `await` outside of the `catch`/`finally` region, and then have the exceptions restored as necessary. @@ -98,9 +84,7 @@ perform the `await` outside of the `catch`/`finally` region, and then have the e TODO: Go over `IAsyncEnumerable` and confirm that the initial rewrite to a `Task`-based method produces code that can then be implemented with runtime async, rather than a full compiler state machine. TODO: Clarify with the debugger team where NOPs need to be inserted for debugging/ENC scenarios. - We will likely need to insert AwaitYieldPoint and AwaitResumePoints for the scenarios where we emit calls to `RuntimeHelpers` async helpers, but can we avoid them for calls in runtime async form? - -TODO: Do we need to implement clearing of locals marked with `Hoisted`, or will the runtime handle that? + We will likely need to insert AwaitYieldPoint and AwaitResumePoints for the scenarios where we emit calls to `AsyncHelpers` async helpers, but can we avoid them for calls in runtime async form? ### Example transformations @@ -129,18 +113,47 @@ Task M() The same holds for methods that return `Task`, `ValueTask`, and `ValueTask`. Any method returning a different `Task`-like type is not transformed to runtime async form and uses a C#-generated state machine. -`await`s within the body will either be transformed to Runtime-Async call format (as detailed in the runtime specification), or we will use one of the `RuntimeHelpers` methods to do the `await`. Specifics +`await`s within the body will either be transformed to Runtime-Async call format (as detailed in the runtime specification), or we will use one of the `AsyncHelpers` methods to do the `await`. Specifics for given scenarios are elaborated in more detail below. `Experimental` will be removed when the full feature is ready to ship, likely not before .NET 11. TODO: Async iterators (returning `IAsyncEnumerable`) -#### `Task`, `Task`, `ValueTask`, `ValueTask` Scenarios - -For any lvalue of one of these types, we'll generally rewrite `await expr` into `System.Runtime.CompilerServices.RuntimeHelpers.Await(expr)`. A number of different example scenarios for this are covered below. The +#### `AsyncHelpers.Await` Scenarios + +For any `await expr` with where `expr` has type `E`, the compiler will attempt to match it to a helper method in `System.Runtime.CompilerServices.AsyncHelpers`. The following algorithm is used: + +1. If `E` has generic arity greater than 1, no match is found and instead move to [await any other type]. +2. `System.Runtime.CompilerServices.AsyncHelpers` from corelib (the library that defines `System.Object` and has no references) is fetched. +3. All methods named `Await` are put into a group called `M`. +4. For every `Mi` in `M`: + 1. If `Mi`'s generic arity does not match `E`, it is removed. + 2. If `Mi` takes more than 1 parameter (named `P`), it is removed. + 3. If `Mi` has a generic arity of 0, all of the following must be true, or `Mi` is removed: + 1. The return type is `System.Void` + 2. There is an identity or implicit reference conversion from `E` to the type of `P`. + 4. Otherwise, if `Mi` has a generic arity of 1 with type param `Tm`, all of the following must be true, or `Mi` is removed: + 1. The return type is `Tm` + 2. There is an identity or implicit reference conversion from `E`'s unsubstituted definition to `P` + 3. `E`'s type argument, `Te`, is valid to substitute for `Tm` +6. If only one `Mi` remains, that method is used for the following rewrites. Otherwise, we instead move to [await any other type]. + +We'll generally rewrite `await expr` into `System.Runtime.CompilerServices.AsyncHelpers.Await(expr)`. A number of different example scenarios for this are covered below. The main interesting deviations are when `struct` rvalues need to be hoisted across an `await`, and exception handling rewriting. +These rules are intended to cover the following types: + +* `Task`, or any subtypes of `Task` +* `Task`, or any subtypes of `Task` +* `ValueTask` +* `ValueTask` +* `ConfiguredTaskAwaitable` +* `ConfiguredTaskAwaitable` +* `ConfiguredValueTaskAwaitable` +* `ConfiguredValueTaskAwaitable` +* Any future `Task`-like types the runtime would like to intrinsify + ##### Await `Task`-returning method ```cs @@ -155,12 +168,12 @@ await C.M(); Translated C#: ```cs -System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); ``` ```il call [System.Runtime]System.Threading.Tasks.Task C::M() -call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) +call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) ``` --------------------------- @@ -179,13 +192,13 @@ Translated C#: ```cs var c = new C(); -System.Runtime.CompilerServices.RuntimeHelpers.Await(c.M()); +System.Runtime.CompilerServices.AsyncHelpers.Await(c.M()); ``` ```il newobj instance void C::.ctor() callvirt instance class [System.Runtime]System.Threading.Tasks.Task C::M() -call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) +call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) ```
@@ -205,12 +218,12 @@ class C Translated C#: ```cs -int i = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +int i = System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); ``` ```il call class [System.Runtime]System.Threading.Tasks.Task`1 C::M() -call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) +call int32 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) stloc.0 ``` @@ -230,13 +243,13 @@ Translated C#: ```cs var c = new C(); -int i = System.Runtime.CompilerServices.RuntimeHelpers.Await(c.M()); +int i = System.Runtime.CompilerServices.AsyncHelpers.Await(c.M()); ``` ```il newobj instance void C::.ctor() callvirt instance class [System.Runtime]System.Threading.Tasks.Task`1 C::M() -call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) +call int32 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) stloc.0 ``` @@ -256,7 +269,7 @@ Translated C#: ```cs var local = C.M(); -System.Runtime.CompilerServices.RuntimeHelpers.Await(local); +System.Runtime.CompilerServices.AsyncHelpers.Await(local); ``` ```il @@ -268,7 +281,7 @@ System.Runtime.CompilerServices.RuntimeHelpers.Await(local); IL_0000: call class [System.Runtime]System.Threading.Tasks.Task C::M() IL_0005: stloc.0 IL_0006: ldloc.0 - IL_0007: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0007: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_000c: ret } ``` @@ -289,7 +302,7 @@ Translated C#: ```cs var local = C.M(); -var i = System.Runtime.CompilerServices.RuntimeHelpers.Await(local); +var i = System.Runtime.CompilerServices.AsyncHelpers.Await(local); ``` ```il @@ -302,7 +315,7 @@ var i = System.Runtime.CompilerServices.RuntimeHelpers.Await(local); IL_0000: call class [System.Runtime]System.Threading.Tasks.Task`1 C::M() IL_0005: stloc.0 IL_0006: ldloc.0 - IL_0007: call !!0 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) + IL_0007: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) IL_000c: stloc.1 IL_000d: ret } @@ -322,13 +335,13 @@ class C Translated C#: ```cs -System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); ``` ```il { IL_0000: call !!0 C::M() - IL_0005: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0005: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_000a: ret } ``` @@ -347,13 +360,13 @@ class C Translated C#: ```cs -int i = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +int i = System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); ``` ```il { IL_0000: call class [System.Runtime]System.Threading.Tasks.Task`1 C::M() - IL_0005: call !!0 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) + IL_0005: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) IL_000a: stloc.0 IL_000b: ret } @@ -377,7 +390,7 @@ Translated C# ```cs AsyncDelegate d = C.M; -System.Runtime.CompilerServices.RuntimeHelpers.Await(d()); +System.Runtime.CompilerServices.AsyncHelpers.Await(d()); ``` ```il @@ -394,7 +407,7 @@ System.Runtime.CompilerServices.RuntimeHelpers.Await(d()); IL_0016: stsfld class AsyncDelegate Program/'<>O'::'<0>__M' IL_001b: callvirt instance class [System.Runtime]System.Threading.Tasks.Task AsyncDelegate::Invoke() - IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_0025: ret } ``` @@ -415,7 +428,7 @@ Translated C#: ```cs Func d = C.M; -System.Runtime.CompilerServices.RuntimeHelpers.Await(d()); +System.Runtime.CompilerServices.AsyncHelpers.Await(d()); ``` ```il @@ -432,7 +445,7 @@ System.Runtime.CompilerServices.RuntimeHelpers.Await(d()); IL_0016: stsfld class [System.Runtime]System.Func`1 Program/'<>O'::'<0>__M' IL_001b: callvirt instance !0 class [System.Runtime]System.Func`1::Invoke() - IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_0025: ret } ``` @@ -475,7 +488,7 @@ catch (Exception e) if (pendingCatch == 1) { - System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); + System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); throw pendingException; } ``` @@ -507,7 +520,7 @@ if (pendingCatch == 1) IL_000f: bne.un.s IL_001d IL_0011: call class [System.Runtime]System.Threading.Tasks.Task C::M() - IL_0016: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0016: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_001b: ldloc.1 IL_001c: throw @@ -546,7 +559,7 @@ catch (Exception e) pendingException = e; } -System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +System.Runtime.CompilerServices.AsyncHelpers.Await(C.M()); if (pendingException != null) { @@ -572,7 +585,7 @@ if (pendingException != null) } // end handler IL_0009: call class [System.Runtime]System.Threading.Tasks.Task C::M() - IL_000e: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_000e: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) IL_0013: ldloc.0 IL_0014: brfalse.s IL_0018 @@ -602,7 +615,7 @@ Translated C#: int[] a = new int[] { }; int _tmp1 = C.M2(); int _tmp2 = a[_tmp1]; -int _tmp3 = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M1()); +int _tmp3 = System.Runtime.CompilerServices.AsyncHelpers.Await(C.M1()); a[_tmp1] = _tmp2 + _tmp3; ``` @@ -623,7 +636,7 @@ a[_tmp1] = _tmp2 + _tmp3; IL_000e: ldelem.i4 IL_000f: stloc.1 IL_0010: call class [System.Runtime]System.Threading.Tasks.Task`1 C::M1() - IL_0015: call !!0 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) + IL_0015: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) IL_001a: stloc.2 IL_001b: ldloc.0 IL_001c: ldloc.1 @@ -634,10 +647,11 @@ a[_tmp1] = _tmp2 + _tmp3; } ``` -#### Await a non-Task/ValueTask +#### Await any other type +[await any other type]: #await-any-other-type -For anything that isn't a `Task`, `Task`, `ValueTask`, and `ValueTask`, we instead use `System.Runtime.CompilerServices.RuntimeHelpers.AwaitAwaiterFromRuntimeAsync` or -`System.Runtime.CompilerServices.RuntimeHelpers.UnsafeAwaitAwaiterFromRuntimeAsync`. These are covered below. +For anything that isn't a `Task`, `Task`, `ValueTask`, and `ValueTask`, we instead use `System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiterFromRuntimeAsync` or +`System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiterFromRuntimeAsync`. These are covered below. ##### Implementor of ICriticalNotifyCompletion @@ -669,7 +683,7 @@ _ = { var awaiter = c.GetAwaiter(); if (!awaiter.IsCompleted) { - System.Runtime.CompilerServices.RuntimeHelpers.UnsafeAwaitAwaiterFromRuntimeAsync(awaiter); + System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiterFromRuntimeAsync(awaiter); } awaiter.GetResult() }; @@ -689,7 +703,7 @@ _ = { IL_0011: brtrue.s IL_0019 IL_0013: ldloc.0 - IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::UnsafeAwaitAwaiterFromRuntimeAsync(!!0) + IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::UnsafeAwaitAwaiterFromRuntimeAsync(!!0) IL_0019: ldloc.0 IL_001a: callvirt instance void C/Awaiter::GetResult() @@ -724,7 +738,7 @@ _ = { var awaiter = c.GetAwaiter(); if (!awaiter.IsCompleted) { - System.Runtime.CompilerServices.RuntimeHelpers.AwaitAwaiterFromRuntimeAsync(awaiter); + System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiterFromRuntimeAsync(awaiter); } awaiter.GetResult() }; @@ -744,7 +758,7 @@ _ = { IL_0011: brtrue.s IL_0019 IL_0013: ldloc.0 - IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::AwaitAwaiterFromRuntimeAsync(!!0) + IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::AwaitAwaiterFromRuntimeAsync(!!0) IL_0019: ldloc.0 IL_001a: callvirt instance void C/Awaiter::GetResult()