Skip to content

Move runtime async method validation into initial binding #78310

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

Open
wants to merge 10 commits into
base: features/runtime-async
Choose a base branch
from

Conversation

333fred
Copy link
Member

@333fred 333fred commented Apr 24, 2025

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

@333fred 333fred requested a review from a team as a code owner April 24, 2025 23:39
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Apr 24, 2025
@333fred 333fred requested review from RikkiGibson and jcouv April 24, 2025 23:39
@@ -58,11 +59,12 @@ internal BoundAwaitableInfo BindAwaitInfo(BoundAwaitableValuePlaceholder placeho
out PropertySymbol? isCompleted,
out MethodSymbol? getResult,
getAwaiterGetResultCall: out _,
out MethodSymbol? runtimeAsyncAwaitCall,
Copy link
Member

Choose a reason for hiding this comment

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

runtimeAsyncAwaitCall

"runtimeAsyncAwaitMethod"? ("method" instead of "call") Also applies elsewhere

&& GetGetResultMethod(getAwaiter, node, expression.Type!, diagnostics, out getResult, out getAwaiterGetResultCall)
&& (!isRuntimeAsyncEnabled || getRuntimeAwaitAwaiter(awaiterType, out runtimeAsyncAwaitCall));

bool tryGetRuntimeAwaitHelper(out MethodSymbol? runtimeAwaitHelper)
Copy link
Member

Choose a reason for hiding this comment

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

  1. There are captures. This reduces readability and causes allocations. Consider reducing to only capture this. Same for getRuntimeAwaitAwaiter below
  2. I'd suggest keeping the isRuntimeAsyncEnabled check at the call site (if (isRuntimeAsyncEnabled && tryGetRuntimeAwaitHelper(...)) for clarity


var exprOriginalType = expression.Type!.OriginalDefinition;
SpecialMember awaitCall;
TypeWithAnnotations? maybeNestedType = null;
Copy link
Member

Choose a reason for hiding this comment

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

nit: "nestedType" seems strange. Consider "resultType"

runtimeAwaitHelper = runtimeAwaitHelper.Construct([nestedType]);
checkMethodGenericConstraints(runtimeAwaitHelper, diagnostics, expression.Syntax.Location);
}
#if DEBUG
Copy link
Member

Choose a reason for hiding this comment

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

nit: do we need this directive?

return true;
}

void checkMethodGenericConstraints(MethodSymbol method, BindingDiagnosticBag diagnostics, Location location)
Copy link
Member

Choose a reason for hiding this comment

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

Would this helper from ConstraintHelper do the job? public static bool CheckConstraints(this MethodSymbol method, in CheckConstraintsArgs args)

@@ -693,6 +693,10 @@
<Field Name="GetAwaiter" Type="BoundExpression?" Null="allow"/>
<Field Name="IsCompleted" Type="PropertySymbol?" Null="allow"/>
<Field Name="GetResult" Type="MethodSymbol?" Null="allow"/>
<!-- Refers to the runtime async helper we call for awaiting. Either this is an instance of an AsyncHelpers.Await call, and
Copy link
Member

Choose a reason for hiding this comment

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

an instance of an AsyncHelpers.Await call

  1. we're not storing a call
  2. consider also commenting on the null case

Copy link
Member

Choose a reason for hiding this comment

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

  1. Consider adding a Validate method with HasValidate="true"

Copy link
Member

@jcouv jcouv Apr 25, 2025

Choose a reason for hiding this comment

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

  1. Let's take a note to revisit the GetAwaitExpressionInfo API which exposes various bits of BoundAwaitableInfo. We may also want to update NullableWalker.VisitAwaitExpression. [update:] On second thought, maybe we don't need to, as these methods are from corlib (we can afford some assumptions)
  2. It would also be good to review the various consumers of BoundAwaitableInfo and check if some can assert that RuntimeAsyncAwaitMethod is null (for example, AsyncMethodToStateMachineRewriter.VisitAwaitExpression)

// Keep in sync with VB's AssemblySymbol.RuntimeSupportsAsyncMethods
internal bool RuntimeSupportsAsyncMethods
=> RuntimeSupportsFeature(SpecialMember.System_Runtime_CompilerServices_RuntimeFeature__Async)
|| _overrideRuntimeSupportsAsyncMethods;
=> GetSpecialType(InternalSpecialType.System_Runtime_CompilerServices_AsyncHelpers) is { TypeKind: TypeKind.Class, IsStatic: true };
Copy link
Member

Choose a reason for hiding this comment

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

Should we remove the runtime feature entry from the design doc?

_factory.Compilation.GetSpecialType(InternalSpecialType.System_Runtime_CompilerServices_AsyncHelpers)));
Debug.Assert(runtimeAsyncAwaitMethod.Name is "Await" or "UnsafeAwaitAwaiter" or "AwaitAwaiter");

if (runtimeAsyncAwaitMethod.Name == "Await")
Copy link
Member

Choose a reason for hiding this comment

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

nit: this feels a bit weird. Would it be better to split this into two fields in BoundAwaitableInfo (one for Await case and one for AwaitAwait case)?

// await default(System.Threading.Tasks.ValueTask<int>);
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "default(System.Threading.Tasks.ValueTask<int>)").WithArguments("System.Runtime.CompilerServices.AsyncHelpers.Await<T>(System.Threading.Tasks.ValueTask<T>)", "T", "int").WithLocation(1, 7)
);
}
Copy link
Member

@jcouv jcouv Apr 25, 2025

Choose a reason for hiding this comment

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

  • Consider adding a test for void-returning method showing we'll still generate a state machine.
  • could be in a later PR: consider adding a test for await using and await foreach in a runtime-async method
  • nit: should we include a test where the types are present but not from corlib?

&& !ReferenceEquals(methodReturn, GetSpecialType(InternalSpecialType.System_Threading_Tasks_ValueTask))
&& !ReferenceEquals(methodReturn, GetSpecialType(InternalSpecialType.System_Threading_Tasks_ValueTask_T)))
{
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Is there a corresponding update to the design doc? Or should we have a follow-up comment to make the void-returning method scenario work at some point?

@jcouv jcouv self-assigned this Apr 25, 2025
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 (iteration 10)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Feature - Runtime Async untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants