Skip to content

Emit async-iterators with runtime-async when possible#81314

Merged
jcouv merged 5 commits intodotnet:features/runtime-async-streamsfrom
jcouv:async-iterator2
Feb 11, 2026
Merged

Emit async-iterators with runtime-async when possible#81314
jcouv merged 5 commits intodotnet:features/runtime-async-streamsfrom
jcouv:async-iterator2

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Nov 18, 2025

Relates to test plan #75960

@jcouv jcouv self-assigned this Nov 18, 2025
@jcouv jcouv force-pushed the async-iterator2 branch 4 times, most recently from 2765f0d to 8542bed Compare November 24, 2025 23:33
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:
Copy link
Member

@stephentoub stephentoub Nov 26, 2025

Choose a reason for hiding this comment

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

Nit:

Suggested change
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.
Copy link
Member

@stephentoub stephentoub Nov 26, 2025

Choose a reason for hiding this comment

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

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

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, 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?
Copy link
Member

@stephentoub stephentoub Nov 26, 2025

Choose a reason for hiding this comment

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

Why would this need to differ from what we do today? #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'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 ;-)

@jcouv jcouv marked this pull request as ready for review December 1, 2025 17:08
@jcouv jcouv requested a review from a team as a code owner December 1, 2025 17:08
@jcouv jcouv mentioned this pull request Dec 1, 2025
46 tasks
Copy link
Member

@333fred 333fred 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 (commit 1). Tests are not looked at yet.


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.
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

Suggested change
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

@jcouv jcouv requested a review from 333fred December 3, 2025 21:47
Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Mostly LGTM. Just a couple of small comments.

@jcouv jcouv requested a review from RikkiGibson December 4, 2025 17:19
@RikkiGibson
Copy link
Member

putting on the queue.

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.

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))
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

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)
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

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)
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

non-blocking: consider inlining this local function. #ByDesign

{
return rewriter.Rewrite();
}
catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex)
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

Does reaching this catch(), indicate a check is missing in VerifyPresenceOfRequiredAPIs above? #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.

Yes, we'd have to have missed some API during VerifyPresenceOfRequiredAPIs

MethodSymbol IAsyncEnumeratorOfElementType_MoveNextAsync = GetMoveNextAsyncMethod();
OpenMoveNextMethodImplementation(IAsyncEnumeratorOfElementType_MoveNextAsync, runtimeAsync: true);

var rewritter = new MoveNextAsyncRewriter(
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

nit: rewriter #Pending

// _current = default;
blockBuilder.Add(GenerateClearCurrent());

// throw null;
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

nit: This comment feels slightly misleading. A "throw with no expression" is being used rather than a "throw with null expression", right?

Suggested change
// 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>: ;
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

nit:

Suggested change
// <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));
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

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

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 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"/>
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

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)",
Copy link
Member

@RikkiGibson RikkiGibson Feb 6, 2026

Choose a reason for hiding this comment

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

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

@jcouv jcouv enabled auto-merge (squash) February 11, 2026 18:07
@jcouv jcouv merged commit 5127522 into dotnet:features/runtime-async-streams Feb 11, 2026
23 of 24 checks passed
@jcouv jcouv deleted the async-iterator2 branch February 11, 2026 21:02
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.

5 participants