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

Add draft of runtime async ECMA-335 changes #104063

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
71 changes: 71 additions & 0 deletions docs/design/specs/runtime-async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

This document is a draft of changes to ECMA-335 for the "runtime async" feature. When the feature is officially supported, it can be merged into the final ECMA-335 augments document.

# Runtime-async

Async is currently a feature implemented by various .NET languages as a compiler rewrite to support methods that can "yield" control back to their caller at specific "suspension" points. While effective, it's believed that implementation directly in the .NET runtime can provide improvements, especially in performance.

## Spec modifications

These are proposed modifications to the ECMA-335 specification for runtime-async.

### I.8.4.5 Sync and Async Methods

Methods may be either 'sync' or 'async'. Async methods have a special signature encoding, described in [### I.8.6.1.5 Method signatures].

Sync methods are all other methods.

Unlike sync methods, async methods support suspension. Suspension allows async methods to yield control flow back to their caller at certain well-defined suspension points, and resume execution of the remaining method at a later time or location, potentially on another thread.

Async methods support the following suspension points:

* Calling another async method. No special instructions need to be provided. If the callee suspends, the caller will suspend as well.
* Using a library in the .NET corelib to "await" an "INotifyCompletion" type. The signatures of these methods shall be:
agocke marked this conversation as resolved.
Show resolved Hide resolved
```C#
// public static async2 Task AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion
Copy link
Member

Choose a reason for hiding this comment

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

async2? will this temporary keyword for the prototype be part of our ecma spec?

Copy link
Member Author

Choose a reason for hiding this comment

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

Temporary 😄 Long-term I think we can use “runtime async” and “compiler async”

Copy link
Member

@jaredpar jaredpar Jul 9, 2024

Choose a reason for hiding this comment

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

await GetFinalSyntax("async2").ConfigureAwait(FileNotFound);

public static void modopt([System.Runtime]System.Threading.Tasks.Task) AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion
// public static async2 Task UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion
public static void modopt([System.Runtime]System.Threading.Tasks.Task) UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion
```

Each of the above methods will have semantics analogous to the current AsyncTaskMethodBuilder.AwaitOnCompleted/AwaitUnsafeOnCompleted methods. After calling this method, in can be presumed that the task has completed.

Async methods have the following restrictions:
Copy link
Member

Choose a reason for hiding this comment

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

We should split these restrictions into fundamental (hard/impossible to ever remove them) and non-fundamental ones that just exist to make the initial implementation easier.

Copy link
Member Author

Choose a reason for hiding this comment

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

Are there any truly fundamental restrictions? I think, with enough effort, we could make almost anything work.

Copy link
Member

Choose a reason for hiding this comment

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

I think escaping ref and byref locals are fundamental restriction category. I agree that we can make them work in principle, but it would come with tough performance trade-offs.

agocke marked this conversation as resolved.
Show resolved Hide resolved
* Usage of the `localloc` and instruction is forbidden
agocke marked this conversation as resolved.
Show resolved Hide resolved
* The `lodloca` and `ldarga` instructions are redefined to return managed pointers instead of pointers.
agocke marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Note that ldloca and ldarga already return managed pointers in the spec. This should rather be a modification around the note we have on transient pointers here: https://github.com/dotnet/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#transient-pointers

There is a question of what level of behavior we want to specify. We can either specify

Arguments / local variables of async methods are stored in unmanaged memory with an address that is guaranteed to be static in between suspension points, but that may change across suspension points.

or

Arguments / local variables of async methods are stored in managed memory.

The latter precludes code like unsafe { <unsafe code taking addresses of locals without any suspension points> } in C#, but Roslyn already warns on that today.

I think in either case we also need to document that values of managed pointers and structs containing managed pointers are not preserved across suspension points (at least for an initial version).

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 picked the last one because it's the most restrictive. We can relax it if necessary.

* Pinning locals may not be created
agocke marked this conversation as resolved.
Show resolved Hide resolved
* The `tail` prefix is forbidden
agocke marked this conversation as resolved.
Show resolved Hide resolved

Suspension points may not appear in exception handling blocks.
agocke marked this conversation as resolved.
Show resolved Hide resolved

All async methods effectively have two entry points, or signatures. The first signature is the one present in the above code: a modopt before the return type. The second signature is a "Task-equivalent signature", described in further detail in [I.8.6.1.5 Method signatures].
agocke marked this conversation as resolved.
Show resolved Hide resolved

Async methods have a special calling convention and may not be called directly outside of other async methods. To call an async method from a sync method, callers must use the second "Task-equivalent signature".
Copy link
Member

Choose a reason for hiding this comment

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

This sounds like it would impact aspect of Ref.Emit and invoke. Has Ref.Emit been considered with respect to async2 methods?


Callers may retrieve a Task-equivalent return type from an async method via calling the "Task-equivalent signature". This functionality is available in both sync and async methods.

### I.8.6.1.5 Method signatures

The list of relevant components is augmented to include sync vs. async method types. Async methods have some additions to normal signature compatibility.

Async MethodDef entries implicitly create two member definitions: one explicit, primary definiton, and a second implicit, runtime-generated definition.
agocke marked this conversation as resolved.
Show resolved Hide resolved

The primary, mandatory definition must be present in metadata as a MethodDef. This signature must have a required custom modifier as the last custom modifier before the return type. The custom modifier must fit the following requirements:
agocke marked this conversation as resolved.
Show resolved Hide resolved
* If the return type is void, the custom modifier must be either `System.Threading.Task` or `System.Threading.ValueTask`.
* If the return type is not `void`, the modifier must be to either `System.Threading.Task<T>` or `System.Threading.ValueTask<T>`. The return type must be a valid substitution for the type parameter of the custom modifier type.

_[Note: async methods have the same return type conventions as sync methods. If the async method produces a System.Int32, the return type must be System.Int32.]_

The second async signature is implicit and runtime-generated and is hereafter referred to as the "Task-equivalent" signature. It is generated based on the primary signature. The transformation is as follows:
Copy link
Member

Choose a reason for hiding this comment

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

The second async signature is implicit and runtime-generated

Not sure if it's obvious but this means that if you want to use the generated signature you have to emit a call, callvirt, ldftn or ldvirtfn with a MethodRef or MethodSpec token, not MethodDef, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good question. I think that's correct, but I have to review the full ECMA-335 spec to make sure.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, reviewed, you're correct. I don't have time right now to find every spot in the spec that would need to be adjusted here, but the intent is that basically the calling convention for implicit definitions of both sync and async methods would be to use MethodRef just like a VARARG call. I think every spot in the spec that mentions special casing for VARARG should be adjusted to also mention implicit definitions.


If the async return type is void, the return type of the Task-equivalent signature is the type of the async custom modifier.

Otherwise, the Task-equivalent return type is the custom modifier type (either Task`1 or ValueTask`1), substituted with the async return type.
agocke marked this conversation as resolved.
Show resolved Hide resolved

### I.8.10.2 Method inheritance

For the purposes of inheritance and hiding, both async signatures ([### I.8.6.1.5 Method signatures]) are used for hiding and overriding, but cannot be configured separately.

### II.10.3.2 The .override directive

Async methods participate in overrides through both definitions (both signatures). An async method with a .override overrides the target method signature, as well as the secondary "Task-equivalent" signature if applicable. An async method may also override only the "Task-equivalent" signature, if an async signature is not present on the base class. A sync method may not override an async method, even if it matches the async method's "Task-equivalent" definition.
333fred marked this conversation as resolved.
Show resolved Hide resolved
lambdageek marked this conversation as resolved.
Show resolved Hide resolved