Skip to content

Commit 7de7718

Browse files
authored
Support using a simple overload resolution for finding Await helpers from the BCL (#79135)
* Support using a simple overload resolution for finding Await helpers from the BCL This PR removes special knowledge of what `Await` helpers correspond to what types, and instead implements a very simple form of overload resolution. We immediately bail on any conflict or error and fall back to attempting to use `AwaitAwaiter` or `UnsafeAwaitAwaiter` when such scenarios are detected. I've also updated the rules to better reflect what is actually implementable. * Create the full BoundCall in initial binding. * PR feedback.
1 parent c2fff44 commit 7de7718

File tree

19 files changed

+674
-189
lines changed

19 files changed

+674
-189
lines changed

docs/compilers/CSharp/Runtime Async Design.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,11 @@ For any `await expr` with where `expr` has type `E`, the compiler will attempt t
135135
2. There is an identity or implicit reference conversion from `E` to the type of `P`.
136136
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:
137137
1. The return type is `Tm`
138-
2. There is an identity or implicit reference conversion from `E`'s unsubstituted definition to `P`
139-
3. `E`'s type argument, `Te`, is valid to substitute for `Tm`
140-
6. If only one `Mi` remains, that method is used for the following rewrites. Otherwise, we instead move to [await any other type].
138+
2. The generic parameter of `E` is `Te`
139+
3. `Ti` satisfies any constraints on `Tm`
140+
4. `Mie` is `Mi` with `Te` substituted for `Tm`, and `Pe` is the resulting parameter of `Mie`
141+
5. There is an identity or implicit reference conversion from `E` to the type of `Pe`
142+
5. If only one `Mi` remains, that method is used for the following rewrites. Otherwise, we instead move to [await any other type].
141143

142144
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
143145
main interesting deviations are when `struct` rvalues need to be hoisted across an `await`, and exception handling rewriting.

src/Compilers/CSharp/Portable/Binder/Binder_Await.cs

Lines changed: 185 additions & 52 deletions
Large diffs are not rendered by default.

src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno
263263
var placeholder = new BoundAwaitableValuePlaceholder(expr, builder.MoveNextInfo?.Method.ReturnType ?? CreateErrorType());
264264
awaitInfo = BindAwaitInfo(placeholder, expr, diagnostics, ref hasErrors);
265265

266-
if (!hasErrors && (awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod)?.ReturnType.SpecialType != SpecialType.System_Boolean)
266+
if (!hasErrors && (awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitCall?.Method)?.ReturnType.SpecialType != SpecialType.System_Boolean)
267267
{
268268
diagnostics.Add(ErrorCode.ERR_BadGetAsyncEnumerator, expr.Location, getEnumeratorMethod.ReturnTypeWithAnnotations, getEnumeratorMethod);
269269
hasErrors = true;

src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,11 @@ private void GetAwaitableInstancePlaceholders(ArrayBuilder<(BoundValuePlaceholde
958958
{
959959
placeholders.Add((placeholder, valEscapeScope));
960960
}
961+
962+
if (awaitableInfo.RuntimeAsyncAwaitCallPlaceholder is { } runtimePlaceholder)
963+
{
964+
placeholders.Add((runtimePlaceholder, valEscapeScope));
965+
}
961966
}
962967

963968
public override BoundNode? VisitImplicitIndexerAccess(BoundImplicitIndexerAccess node)

src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo
150150

151151
if (awaitableTypeOpt is null)
152152
{
153-
awaitOpt = new BoundAwaitableInfo(syntax, awaitableInstancePlaceholder: null, isDynamic: true, getAwaiter: null, isCompleted: null, getResult: null, runtimeAsyncAwaitMethod: null) { WasCompilerGenerated = true };
153+
awaitOpt = new BoundAwaitableInfo(syntax, awaitableInstancePlaceholder: null, isDynamic: true, getAwaiter: null, isCompleted: null, getResult: null, runtimeAsyncAwaitCall: null, runtimeAsyncAwaitCallPlaceholder: null) { WasCompilerGenerated = true };
154154
}
155155
else
156156
{

src/Compilers/CSharp/Portable/BoundTree/BoundAwaitableInfo.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ partial class BoundAwaitableInfo
1010
{
1111
private partial void Validate()
1212
{
13-
if (RuntimeAsyncAwaitMethod is not null)
13+
if (RuntimeAsyncAwaitCall is not null)
1414
{
15-
Debug.Assert(RuntimeAsyncAwaitMethod.ContainingType.ExtendedSpecialType == InternalSpecialType.System_Runtime_CompilerServices_AsyncHelpers);
15+
Debug.Assert(RuntimeAsyncAwaitCall.Method.ContainingType.ExtendedSpecialType == InternalSpecialType.System_Runtime_CompilerServices_AsyncHelpers);
16+
Debug.Assert(RuntimeAsyncAwaitCallPlaceholder is not null);
1617

17-
switch (RuntimeAsyncAwaitMethod.Name)
18+
switch (RuntimeAsyncAwaitCall.Method.Name)
1819
{
1920
case "Await":
2021
Debug.Assert(GetAwaiter is null);
@@ -30,11 +31,11 @@ private partial void Validate()
3031
break;
3132

3233
default:
33-
Debug.Fail($"Unexpected RuntimeAsyncAwaitMethod: {RuntimeAsyncAwaitMethod.Name}");
34+
Debug.Fail($"Unexpected RuntimeAsyncAwaitCall: {RuntimeAsyncAwaitCall.Method.Name}");
3435
break;
3536
}
3637
}
3738

38-
Debug.Assert(GetAwaiter is not null || RuntimeAsyncAwaitMethod is not null || IsDynamic || HasErrors);
39+
Debug.Assert(GetAwaiter is not null || RuntimeAsyncAwaitCall is not null || IsDynamic || HasErrors);
3940
}
4041
}

src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,11 +702,13 @@
702702
<Field Name="GetAwaiter" Type="BoundExpression?" Null="allow"/>
703703
<Field Name="IsCompleted" Type="PropertySymbol?" Null="allow"/>
704704
<Field Name="GetResult" Type="MethodSymbol?" Null="allow"/>
705-
<!-- Refers to the runtime async helper method we use for awaiting. Either this is an instance of an AsyncHelpers.Await method symbol, and
706-
GetAwaiter, IsCompleted, and GetResult are null, or this is AsyncHelpers.AwaitAwaiter/UnsafeAwaitAwaiter, and the other
705+
<!-- Refers to the runtime async helper call we use for awaiting. Either this is an instance of a BoundCall of an AsyncHelpers.Await method, and
706+
GetAwaiter, IsCompleted, and GetResult are null, or this is an instance of a AsyncHelpers.AwaitAwaiter/UnsafeAwaitAwaiter BoundCall, and the other
707707
fields are not null. -->
708708
<!-- PROTOTYPE: Look at consumers of this API and see if we can assert that this is null when it should be -->
709-
<Field Name="RuntimeAsyncAwaitMethod" Type="MethodSymbol?" Null="allow"/>
709+
<Field Name="RuntimeAsyncAwaitCall" Type="BoundCall?" Null="allow"/>
710+
<!-- Never null when RuntimeAsyncAwaitCall is not null -->
711+
<Field Name="RuntimeAsyncAwaitCallPlaceholder" Type="BoundAwaitableValuePlaceholder?" Null="allow"/>
710712
</Node>
711713

712714
<Node Name="BoundAwaitExpression" Base="BoundExpression">

src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,16 +2249,16 @@ internal bool ReturnsAwaitableToVoidOrInt(MethodSymbol method, BindingDiagnostic
22492249
var syntax = method.ExtractReturnTypeSyntax();
22502250
var dumbInstance = new BoundLiteral(syntax, ConstantValue.Null, namedType);
22512251
var binder = GetBinder(syntax);
2252-
var success = binder.GetAwaitableExpressionInfo(dumbInstance, out BoundExpression? result, out MethodSymbol? runtimeAwaitMethod, syntax, diagnostics);
2252+
var success = binder.GetAwaitableExpressionInfo(dumbInstance, out BoundExpression? result, out BoundCall? runtimeAwaitCall, syntax, diagnostics);
22532253

22542254
RoslynDebug.Assert(!namedType.IsDynamic());
22552255
if (!success)
22562256
{
22572257
return false;
22582258
}
22592259

2260-
Debug.Assert(result is { Type: not null } || runtimeAwaitMethod is { ReturnType: not null });
2261-
var returnType = result?.Type ?? runtimeAwaitMethod!.ReturnType;
2260+
Debug.Assert(result is { Type: not null } || runtimeAwaitCall is { Type: not null });
2261+
var returnType = result?.Type ?? runtimeAwaitCall!.Type;
22622262
return returnType.IsVoidType() || returnType.SpecialType == SpecialType.System_Int32;
22632263
}
22642264

src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs

Lines changed: 19 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ private RuntimeAsyncRewriter(SyntheticBoundNodeFactory factory)
5050
Debug.Assert(nodeType is not null);
5151

5252
var awaitableInfo = node.AwaitableInfo;
53-
var runtimeAsyncAwaitMethod = awaitableInfo.RuntimeAsyncAwaitMethod;
53+
var runtimeAsyncAwaitCall = awaitableInfo.RuntimeAsyncAwaitCall;
54+
Debug.Assert(runtimeAsyncAwaitCall is not null);
55+
Debug.Assert(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder is not null);
56+
var runtimeAsyncAwaitMethod = runtimeAsyncAwaitCall.Method;
5457
Debug.Assert(runtimeAsyncAwaitMethod is not null);
5558
Debug.Assert(ReferenceEquals(
5659
runtimeAsyncAwaitMethod.ContainingType.OriginalDefinition,
@@ -61,7 +64,11 @@ private RuntimeAsyncRewriter(SyntheticBoundNodeFactory factory)
6164
{
6265
// This is the direct await case, with no need for the full pattern.
6366
// System.Runtime.CompilerServices.RuntimeHelpers.Await(awaitedExpression)
64-
return _factory.Call(receiver: null, runtimeAsyncAwaitMethod, VisitExpression(node.Expression));
67+
var expr = VisitExpression(node.Expression);
68+
_placeholderMap.Add(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder, expr);
69+
var call = Visit(awaitableInfo.RuntimeAsyncAwaitCall);
70+
_placeholderMap.Remove(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder);
71+
return call;
6572
}
6673
else
6774
{
@@ -108,11 +115,11 @@ private BoundExpression RewriteCustomAwaiterAwait(BoundAwaitExpression node)
108115
var isCompletedCall = _factory.Call(tmp, isCompletedMethod);
109116

110117
// UnsafeAwaitAwaiter(_tmp) OR AwaitAwaiter(_tmp)
111-
Debug.Assert(awaitableInfo.RuntimeAsyncAwaitMethod is not null);
112-
var awaitCall = _factory.Call(
113-
receiver: null,
114-
awaitableInfo.RuntimeAsyncAwaitMethod,
115-
tmp);
118+
Debug.Assert(awaitableInfo.RuntimeAsyncAwaitCall is not null);
119+
Debug.Assert(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder is not null);
120+
_placeholderMap.Add(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder, tmp);
121+
var awaitCall = (BoundCall)Visit(awaitableInfo.RuntimeAsyncAwaitCall);
122+
_placeholderMap.Remove(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder);
116123

117124
// if (!_tmp.IsCompleted) awaitCall
118125
var ifNotCompleted = _factory.If(_factory.Not(isCompletedCall), _factory.ExpressionStatement(awaitCall));

0 commit comments

Comments
 (0)