Emit async-iterators with runtime-async when possible#81314
Emit async-iterators with runtime-async when possible#81314jcouv merged 5 commits intodotnet:features/runtime-async-streamsfrom
Conversation
2765f0d to
8542bed
Compare
8542bed to
cfbdbfb
Compare
| The `GetAsyncEnumerator` method either returns the current instance if it can be reused, | ||
| or creates a new instance of the state machine class. | ||
|
|
||
| Assuming that the unspeakble state machine class is named `Unspeakable`, `GetAsyncEnumerator` is emitted as: |
There was a problem hiding this comment.
Nit:
| Assuming that the unspeakble state machine class is named `Unspeakable`, `GetAsyncEnumerator` is emitted as: | |
| Assuming that the unspeakable state machine class is named `Unspeakable`, `GetAsyncEnumerator` is emitted as: | |
| ``` #Resolved |
|
|
||
| #### Lowering of `yield return` | ||
|
|
||
| `yield return` is disallowed in finally, in try with catch and in catch. |
There was a problem hiding this comment.
Unrelated to this PR, we were going to look at removing some of these restrictions, in particular for yields inside of a try with a catch, right? #Resolved
There was a problem hiding this comment.
Yes, there's a proposal for that, tracked independently: dotnet/csharplang#8414
It's not clear whether this would make it into C# 15 yet
|
|
||
| ## Open issues | ||
|
|
||
| Question: AsyncIteratorStateMachineAttribute, or IteratorStateMachineAttribute, or other attribute on kickoff method? |
There was a problem hiding this comment.
Why would this need to differ from what we do today? #Resolved
There was a problem hiding this comment.
I'm not sure yet. We introduced a different attribute for each state machine design so far.
Edit&Continue looks at this attribute and also relies on Symreader which also looks at the attribute. Symreader uses it to find the MoveNext method, but here the naming convention is different (it's "MoveNextAsync").
I didn't investigate the debug symbols or EnC yet. Need to talk to Tomas ;-)
src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/AsyncRewriter.AsyncIteratorRewriter.cs
Show resolved
Hide resolved
333fred
left a comment
There was a problem hiding this comment.
Done review pass (commit 1). Tests are not looked at yet.
src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs
Show resolved
Hide resolved
.../Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.MoveNextAsyncRewriter.cs
Outdated
Show resolved
Hide resolved
.../Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.MoveNextAsyncRewriter.cs
Outdated
Show resolved
Hide resolved
.../Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.MoveNextAsyncRewriter.cs
Outdated
Show resolved
Hide resolved
...pilers/CSharp/Portable/Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.cs
Show resolved
Hide resolved
...pilers/CSharp/Portable/Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.cs
Show resolved
Hide resolved
...rs/CSharp/Portable/Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorStateMachine.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/SynthesizedStateMachineMethod.cs
Show resolved
Hide resolved
|
|
||
| Async methods that return `IAsyncEnumerable<T>` or `IAsyncEnumerator<T>` are transformed by the compiler into state machines. | ||
| States are created for each `await` and `yield`. | ||
| Runtime-async support was added in .NET 10 as a preview feature and reduces the overhead of async methods by letting the runtime handling `await` suspensions. |
There was a problem hiding this comment.
| Runtime-async support was added in .NET 10 as a preview feature and reduces the overhead of async methods by letting the runtime handling `await` suspensions. | |
| Runtime-async support was added in .NET 10 as a preview feature and reduces the overhead of async methods by letting the runtime handle `await` suspensions. | |
| ``` #Resolved |
333fred
left a comment
There was a problem hiding this comment.
Mostly LGTM. Just a couple of small comments.
...pilers/CSharp/Portable/Lowering/RuntimeAsyncIteratorRewriter/RuntimeAsyncIteratorRewriter.cs
Outdated
Show resolved
Hide resolved
|
putting on the queue. |
RikkiGibson
left a comment
There was a problem hiding this comment.
Finished reviewing impl. Overall LGTM. Will take a break then get thru tests today.
| } | ||
|
|
||
| var getAsyncEnumerator = (MethodSymbol?)GetWellKnownTypeMember(WellKnownMember.System_Collections_Generic_IAsyncEnumerable_T__GetAsyncEnumerator); | ||
| if (getAsyncEnumerator is null || !iAsyncEnumerable.GetMembers(WellKnownMemberNames.GetAsyncEnumeratorMethodName).Contains(getAsyncEnumerator)) |
There was a problem hiding this comment.
Does the iAsyncEnumerable.GetMembers().Contains() check do something here? I thought a well-known member has a strong association with a well-known containing type, and, GetWellKnownTypeMember() won't return a symbol which is "disjunct" from that. #Pending
There was a problem hiding this comment.
You're right, that was unnecessarily paranoid :-P
| /// <param name="locals">The set of locals declared in the original version of this statement</param> | ||
| /// <param name="wrapped">A delegate to return the translation of the body of this statement</param> | ||
| private BoundStatement PossibleIteratorScope(ImmutableArray<LocalSymbol> locals, Func<BoundStatement> wrapped) | ||
| private BoundStatement PossibleIteratorScope(ImmutableArray<LocalSymbol> locals, Func<MethodToStateMachineRewriter, BoundBlock, BoundStatement> wrapped, BoundBlock node) |
There was a problem hiding this comment.
non-blocking: consider adjusting the doc comment to reflect the change in signature #Pending
|
|
||
| ensureSpecialMember(SpecialMember.System_Runtime_CompilerServices_AsyncHelpers__Await_T, bag); | ||
|
|
||
| Symbol ensureSpecialMember(SpecialMember member, BindingDiagnosticBag bag) |
There was a problem hiding this comment.
non-blocking: consider inlining this local function. #ByDesign
| { | ||
| return rewriter.Rewrite(); | ||
| } | ||
| catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) |
There was a problem hiding this comment.
Does reaching this catch(), indicate a check is missing in VerifyPresenceOfRequiredAPIs above? #Resolved
There was a problem hiding this comment.
Yes, we'd have to have missed some API during VerifyPresenceOfRequiredAPIs
| MethodSymbol IAsyncEnumeratorOfElementType_MoveNextAsync = GetMoveNextAsyncMethod(); | ||
| OpenMoveNextMethodImplementation(IAsyncEnumeratorOfElementType_MoveNextAsync, runtimeAsync: true); | ||
|
|
||
| var rewritter = new MoveNextAsyncRewriter( |
| // _current = default; | ||
| blockBuilder.Add(GenerateClearCurrent()); | ||
|
|
||
| // throw null; |
There was a problem hiding this comment.
nit: This comment feels slightly misleading. A "throw with no expression" is being used rather than a "throw with null expression", right?
| // throw null; | |
| // throw; | |
| ``` #Pending |
| var resultTrue = new BoundReturnStatement(F.Syntax, RefKind.None, F.Literal(true), @checked: false) { WasCompilerGenerated = true }; | ||
| blockBuilder.Add(resultTrue); | ||
|
|
||
| // <next_state_label>: ; |
There was a problem hiding this comment.
nit:
| // <next_state_label>: ; | |
| // <next_state_resume_label>: ; | |
| ``` #Pending |
| public override BoundNode? VisitReturnStatement(BoundReturnStatement node) | ||
| { | ||
| Debug.Assert(_currentDisposalLabel is not null); | ||
| return F.Block(SetDisposeMode(), F.Goto(_currentDisposalLabel)); |
There was a problem hiding this comment.
I was slightly confused by this. User cannot write return; statements in an async iterator, right? So is this for handling return statements which were inserted by compiler in an earlier rewriting stage? #Resolved
There was a problem hiding this comment.
I was confused by this too. Yes, it's for implicitly inserted return statement. This could probably be improved, but would affect async and (non runtime-async) async-iterator scenarios, so I kept the same approach
| return F.Block(SetDisposeMode(), F.Goto(_currentDisposalLabel)); | ||
| } | ||
|
|
||
| /// <inheritdoc cref="AsyncIteratorMethodToStateMachineRewriter.VisitTryStatement"/> |
There was a problem hiding this comment.
The reuse issue seems tricky. Seems we are in disjunct part of the class hierarchy here, so, simply sharing "we override certain methods" without sharing other stuff, requires a whole process of extracting a static method and calling it from two overrides. It looks like you probably made a judgment call in each case on whether to extract a static method or just copy the override to this type.
I would slightly prefer extracting this 'VisitTryStatement' to static method, but, don't feel strongly. #WontFix
There was a problem hiding this comment.
Gave it a shot and I didn't find a good way to make it work. The two challenges are the base call to VisitTryStatement and the call to AppendConditionalJumpToCurrentDisposalLabel (which is used in a couple of places, so didn't want to inline)
| "void C.<M>d__0.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.OnCompleted(System.Action<System.Object> continuation, System.Object state, System.Int16 token, System.Threading.Tasks.Sources.ValueTaskSourceOnCompletedFlags flags)", | ||
| "void C.<M>d__0.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(System.Int16 token)", |
There was a problem hiding this comment.
nit: In general it is good to manually reorder to preserve the diff within reason. Alternatively, we could add an ordering step prior to ToTestDisplayStrings() #Pending
5127522
into
dotnet:features/runtime-async-streams
Relates to test plan #75960