Implement async using statement#23961
Conversation
39b405c to
82eefc4
Compare
|
@dotnet/roslyn-compiler for review. Thanks #Resolved |
1 similar comment
|
@dotnet/roslyn-compiler for review. Thanks #Resolved |
| BoundMultipleLocalDeclarations declarationsOpt = null; | ||
| BoundExpression expressionOpt = null; | ||
| BoundAwaitExpression boundAwaitOpt = null; | ||
| if (!hasErrors) |
There was a problem hiding this comment.
Is it necessary to skip binding the expression if IAsyncDisposable has use-site errors? #Resolved
| { | ||
| var syntaxForAwait = (SyntaxNode)expressionSyntax ?? declarationSyntax; | ||
| BoundExpression placeholder = new BoundAwaitableValuePlaceholder(syntaxForAwait, taskType); | ||
| boundAwaitOpt = BindAwait(placeholder, syntaxForAwait, diagnostics).MakeCompilerGenerated(); |
There was a problem hiding this comment.
We're marking the BoundAwaitExpression as compiler generated here, but not in existing await scenarios. Why the difference? #Resolved
There was a problem hiding this comment.
We're marking the BoundAwaitExpression as compiler generated here, but not in existing await scenarios. Why the difference?
I guess because there is no corresponding AwaitExpressionSyntax.
In reply to: 160518475 [](ancestors = 160518475)
There was a problem hiding this comment.
Because in a case of Using, the await expression is not isomorphic to any expression in the syntax.
We are using a bound node just as a storage for method/property symbols found in binding. The node has no other structure or nesting (like in deconstruct).
I think AwaitableInfo, which is not a bound node, would make things simpler. #Resolved
There was a problem hiding this comment.
The main reason why I marked it as compiler-generated is that AsyncMethodToStateMachineRewriter.VisitAwaitExpression expects that a BoundAwaitExpression is attached to some await syntax.
For async-using, we have no await syntax, so I modified the assertion as shown below.
Also, I think it makes sense into terms of bound tree having the same shape as the syntax tree.
private BoundBlock VisitAwaitExpression(BoundAwaitExpression node, BoundExpression resultPlace)
{
...
Debug.Assert(node.Syntax.IsKind(SyntaxKind.AwaitExpression) || node.WasCompilerGenerated);In reply to: 160567590 [](ancestors = 160567590)
|
|
||
| <!-- | ||
| This node is used to represent an awaitable expression of a certain type, when binding an using-await statement. | ||
| It will be replaced by an actual bound node during lowering. |
There was a problem hiding this comment.
Minor point: an async using or a using-await. #Resolved
| @@ -311,7 +312,7 @@ protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByS | |||
|
|
|||
| /// <summary> | |||
| /// A pending branch. There are created for a return, break, continue, goto statement, | |||
| /// <summary> | ||
| /// A pending branch. There are created for a return, break, continue, goto statement, | ||
| /// yield return, yield break, await expression, and if PreciseAbstractFlowPass.trackExceptions | ||
| /// yield return, yield break, await expression, foreach/using await, and if PreciseAbstractFlowPass.trackExceptions |
There was a problem hiding this comment.
Are there pending branches for foreach? #Resolved
There was a problem hiding this comment.
Not yet, but that will be coming shortly with foreach-await. I updated the comment pro-actively, as I feared I'd forget to update it in the foreach-await PR. I can replace with PROTOTYPE if you prefer.
In reply to: 160547396 [](ancestors = 160547396)
| using Xunit; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen | ||
| { |
There was a problem hiding this comment.
Are there tests for using and using await for types that implement IDisposable and IAsyncDisposable? #Resolved
There was a problem hiding this comment.
Consider testing using await (null) { }, using await (MethodName) { }. #Resolved
| IDS_FeatureAttributesOnBackingFields = MessageBase + 12731, | ||
|
|
||
| IDS_FeatureAsyncStreams = MessageBase + 12777, // PROTOTYPE(async-streams) | ||
| IDS_FeatureAsyncStreams = MessageBase + 12777, // PROTOTYPE(async-streams) Compat IDs |
There was a problem hiding this comment.
Compat [](start = 83, length = 6)
Did you mean "Compact"? #Closed
| if (hasAwait) | ||
| { | ||
| iDisposable = this.Compilation.GetWellKnownType(WellKnownType.System_IAsyncDisposable); | ||
| hasErrors |= ReportUseSiteDiagnostics(iDisposable, diagnostics, _syntax.AwaitKeyword.GetLocation()); |
There was a problem hiding this comment.
hasErrors |= ReportUseSiteDiagnostics(iDisposable, diagnostics, _syntax.AwaitKeyword.GetLocation()); [](start = 16, length = 100)
Is there a reason to differ from regular IDisposable in regard to reporting use-site diagnostics? #Closed
There was a problem hiding this comment.
The comment made me think it was unnecessary. But it looks like there was a bug for IDisposable case.
In reply to: 160562446 [](ancestors = 160562446)
| BoundMultipleLocalDeclarations declarationsOpt = null; | ||
| BoundExpression expressionOpt = null; | ||
| BoundAwaitExpression boundAwaitOpt = null; | ||
| if (!hasErrors) |
There was a problem hiding this comment.
if (!hasErrors) [](start = 12, length = 15)
We should keep binding expression/declarations, if not for error reporting, then for SemanticModel/IOperation benefits. #Closed
| hasErrors = true; | ||
| if (!hasErrors && hasAwait) | ||
| { | ||
| TypeSymbol taskType = this.Compilation.GetWellKnownType(WellKnownType.System_Threading_Tasks_Task); |
There was a problem hiding this comment.
GetWellKnownType [](start = 59, length = 16)
I think Binder has a helper that gets the type and reports use-site diagnostics. #Closed
There was a problem hiding this comment.
They don't accept a SyntaxToken or a Location. I don't want to multiple overloads, so I just added one to ReportUseSiteDiagnostics.
In reply to: 160563783 [](ancestors = 160563783)
| if (!hasErrors) | ||
| { | ||
| var syntaxForAwait = (SyntaxNode)expressionSyntax ?? declarationSyntax; | ||
| BoundExpression placeholder = new BoundAwaitableValuePlaceholder(syntaxForAwait, taskType); |
There was a problem hiding this comment.
BoundAwaitableValuePlaceholder [](start = 58, length = 30)
MakeCompilerGenerated()? #Closed
| <Field Name="IDisposableConversion" Type="Conversion" /> | ||
| <Field Name="Body" Type="BoundStatement"/> | ||
| <!-- A bound await, but with a placeholder instead of an awaited expression --> | ||
| <Field Name="AwaitOpt" Type="BoundAwaitExpression" Null="allow"/> |
There was a problem hiding this comment.
I wonder if it is better to factor out {getAwaiter, isCompleted, getResult} as an AwaitableInfo and use it in both BoundAwait and in BoundUsing?
GetAwaitableExpressionInfo is basically returning such info (although via bunch of out params).
Such approach might scale better at representing implicit awaitability than carrying around await nodes on a side. #Resolved
There was a problem hiding this comment.
I'm taking a look at this and will get back to you.
In reply to: 160565220 [](ancestors = 160565220)
There was a problem hiding this comment.
Pushed a commit that refactors BoundAwaitExpression and extracts an AwaitableInfo.
In reply to: 161033778 [](ancestors = 161033778,160565220)
| /// <summary> | ||
| /// Lower "using (ResourceType resource = expression) statement" to a try-finally block. | ||
| /// Lower "using (ResourceType resource = expression) statement" and | ||
| /// "using await (ResourceType resuorce = expression) statement" to a try-finally block. |
There was a problem hiding this comment.
Perhaps short the comment to Lower "using [await] (ResourceType resource = expression) statement" to a try-finally block. Similar comment for previous method. #Resolved
| // local.Dispose() | ||
| disposeCall = BoundCall.Synthesized(syntax, disposedExpression, disposeMethodSymbol); | ||
| } | ||
| else if (awaitOpt != null && TryGetWellKnownTypeMember(syntax, WellKnownMember.System_IAsyncDisposable__DisposeAsync, out MethodSymbol disposeAsyncMethodSymbol)) |
There was a problem hiding this comment.
If we're reporting use-site diagnostics here, could we skip reporting the use-site diagnostic for IAsyncDisposable in binding? #Resolved
There was a problem hiding this comment.
The checks here don't handle missing IDisposable, only missing IDisposable.Dipose().
I agree with you that it's weird to split the use-site diagnostics between here and initial binding. I'll look into this more.
In reply to: 160593496 [](ancestors = 160593496)
There was a problem hiding this comment.
Doesn't TryGetWellKnownTypeMember report a diagnostic if the type is missing? #Resolved
There was a problem hiding this comment.
If I remove the reporting of use-site diagnostics in binding (for missing IDisposable), then execution does not reach lowering, even when I ask for emit diagnostics.
I think that's because there are errors reported from binding anyways. Namely:
// (6,16): error CS1674: 'C': type used in a using statement must be implicitly convertible to 'System.IDisposable'
// using (new C())
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "new C()").WithArguments("C").WithLocation(6, 16),
// (9,16): error CS1674: 'C': type used in a using statement must be implicitly convertible to 'System.IDisposable'
// using (var x = new C())
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "var x = new C()").WithArguments("C").WithLocation(9, 16)
In reply to: 160863521 [](ancestors = 160863521)
There was a problem hiding this comment.
I believe all error reporting for missing members and such should be done in binding. IIRC, the local rewriter can assume that these exist here. Example in: https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StackAlloc.cs when getting the span type
In reply to: 161024454 [](ancestors = 161024454,160863521)
There was a problem hiding this comment.
Initial binding doesn't currently check for existing of IDisposable.Dispose().
There are other examples of well-known types and members which are only checked during rewriting, such as AsyncRewriter.VerifyPresenceOfRequiredAPIs().
I don't see anything wrong with that, especially since getting in a situation where IDisposable.Dispose() is missing, but IDisposable exists wouldn't seem common. The same applies to async counterpart APIs. #Resolved
| { | ||
| System.Console.Write(""body ""); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Consider testing S? s = null; using await (s) { ... }. #Resolved
There was a problem hiding this comment.
Thanks for the test suggestions (good ideas). Added them
In reply to: 160596740 [](ancestors = 160596740)
|
@AlekseyTs @cston Any other feedback? Thanks #Resolved |
|
|
||
| internal AwaitableInfo Update(MethodSymbol newGetAwaiter, PropertySymbol newIsCompleted, MethodSymbol newGetResult) | ||
| { | ||
| if (ReferenceEquals(GetAwaiter, newGetAwaiter) && ReferenceEquals(IsCompleted, newIsCompleted) && ReferenceEquals(GetResult, newGetResult)) |
There was a problem hiding this comment.
ReferenceEquals [](start = 16, length = 15)
ReferenceEquals is fine, but we usually use cast to object and regular == operator to compare references #Closed
There was a problem hiding this comment.
| return null; | ||
| } | ||
|
|
||
| public abstract bool AsyncUsingAddsPendingBranch { get; } |
There was a problem hiding this comment.
AsyncUsingAddsPendingBranch [](start = 29, length = 27)
I am not sure why do we need this for using, but regular await expressions don't use similar mechanism. #Closed
There was a problem hiding this comment.
The reason is that ControlFlowPass doesn't dig into expressions, so it doesn't add pending branches for awaits (implemented in base class).
I want to get the same behavior for async-using, but without cloning the implementation of VisitUsingStatement.
// from ControlFlowPass
public override BoundNode Visit(BoundNode node)
{
// there is no need to scan the contents of an expression, as expressions
// do not contribute to reachability analysis (except for constants, which
// are handled by the caller).
if (!(node is BoundExpression))
{
return base.Visit(node);
}
return null;
}// from PreciseAbstractFlowPass<LocalState>
public override BoundNode VisitAwaitExpression(BoundAwaitExpression node)
{
VisitRvalue(node.Expression);
_pendingBranches.Add(new PendingBranch(node, this.State));
if (_trackExceptions) NotePossibleException(node);
return null;
}
``` #Resolved| } | ||
|
|
||
| #if DEBUG | ||
| LocalRewritingValidator.Validate(loweredStatement); |
There was a problem hiding this comment.
LocalRewritingValidator.Validate(loweredStatement); [](start = 16, length = 51)
I am concerned that this introduces a new source of stack overflow exception and only for debug. Some test will be testing different code path debug vs. release. Perhaps we should catch and swallow the internal stack overflow exception thrown by this validator. #Closed
There was a problem hiding this comment.
| comp.VerifyDiagnostics( | ||
| // (6,22): error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. | ||
| // using await (var x = new C()) | ||
| Diagnostic(ErrorCode.ERR_BadAwaitWithoutVoidAsyncMethod, "var x = new C()").WithLocation(6, 22) |
There was a problem hiding this comment.
var x = new C() [](start = 74, length = 15)
I think it would be better if the location for the error was the await keyword. It would point precisely to the problem and the part between parenthesis could be quite long, event could span multiple lines. #Closed
| "; | ||
| var comp = CreateCompilationWithMscorlib46(source + s_interfaces, options: TestOptions.DebugExe); | ||
| comp.VerifyDiagnostics(); | ||
| CompileAndVerify(comp, expectedOutput: "C body DisposeAsync"); |
There was a problem hiding this comment.
"C body DisposeAsync" [](start = 51, length = 21)
It would be good to test that the task is actually awaited, which I believe this test doesn't confirm. #Closed
| comp.VerifyDiagnostics( | ||
| // (8,26): error CS4004: Cannot await in an unsafe context | ||
| // using await (var x = new C()) | ||
| Diagnostic(ErrorCode.ERR_AwaitInUnsafeContext, "var x = new C()").WithLocation(8, 26) |
There was a problem hiding this comment.
var x = new C() [](start = 64, length = 15)
Same comment for location #Closed
| comp.VerifyDiagnostics( | ||
| // (8,26): error CS1996: Cannot await in the body of a lock statement | ||
| // using await (var x = new C()) | ||
| Diagnostic(ErrorCode.ERR_BadAwaitInLock, "var x = new C()").WithLocation(8, 26) |
There was a problem hiding this comment.
var x = new C() [](start = 58, length = 15)
Same comment for location #Closed
| { | ||
| System.Action a = () => | ||
| { | ||
| System.Action b = async () => { await local(); }; // this await doesn't count towards Main method |
There was a problem hiding this comment.
await local() [](start = 44, length = 13)
Shouldn't we be testing using await here? #Closed
|
|
||
| async System.Threading.Tasks.Task local() | ||
| { | ||
| await local(); // neither does this one |
There was a problem hiding this comment.
await local(); [](start = 12, length = 14)
Shouldn't we be testing using await here? #Closed
| { | ||
| System.Action lambda1 = async () => | ||
| { | ||
| System.Action b = async () => { await local(); }; // this await doesn't count towards lambda |
There was a problem hiding this comment.
await local() [](start = 44, length = 13)
Shouldn't we be testing using await here? #Closed
| System.Action b = async () => { await local(); }; // this await doesn't count towards lambda | ||
| }; | ||
|
|
||
| System.Action lambda2 = async () => await local2(); // this await counts towards lambda |
There was a problem hiding this comment.
await local2() [](start = 44, length = 14)
Shouldn't we be testing using await here? #Closed
There was a problem hiding this comment.
We can't here. The purpose for this lambda is to have an expression body, but async using is a statement.
In reply to: 162705223 [](ancestors = 162705223)
|
|
||
| async System.Threading.Tasks.Task innerLocal() | ||
| { | ||
| await local(); // this await doesn't count towards local function |
There was a problem hiding this comment.
await local() [](start = 16, length = 13)
Shouldn't we be testing using await here? #Closed
| finally | ||
| { | ||
| System.Console.Write(""before ""); | ||
| await System.Threading.Tasks.Task.CompletedTask; |
There was a problem hiding this comment.
await System.Threading.Tasks.Task.CompletedTask [](start = 12, length = 47)
Does completed task actually suspend execution? #Closed
| comp.VerifyEmitDiagnostics( | ||
| // (13,9): error CS0656: Missing compiler required member 'System.IAsyncDisposable.DisposeAsync' | ||
| // using await (new C()) { } | ||
| Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "using await (new C()) { }").WithArguments("System.IAsyncDisposable", "DisposeAsync").WithLocation(13, 9), |
There was a problem hiding this comment.
using await (new C()) { } [](start = 67, length = 25)
It feels like we should narrow location of this error. #Closed
There was a problem hiding this comment.
| } | ||
|
|
||
| [Fact] | ||
| public void TestWithMultipleResources() |
There was a problem hiding this comment.
TestWithMultipleResources [](start = 20, length = 25)
Is this a dupe of TestWithMultipleDeclarations? #Closed
There was a problem hiding this comment.
Close enough. I'll remove TestWithMultipleDeclarations which doesn't check order of evaluation of resources.
Thanks
In reply to: 162709276 [](ancestors = 162709276)
|
Done with review pass (iteration 11) #Closed |
| AwaitableInfo info = node.AwaitableInfo; | ||
| return node.Update( | ||
| expression, | ||
| node.AwaitableInfo.Update(VisitMethodSymbol(info.GetAwaiter), VisitPropertySymbol(info.IsCompleted), VisitMethodSymbol(info.GetResult)), |
There was a problem hiding this comment.
info.Update(...) #Resolved
| // (11,9): error CS0165: Use of unassigned local variable 'z' | ||
| // z++; | ||
| Diagnostic(ErrorCode.ERR_UseDefViolation, "z").WithArguments("z").WithLocation(11, 9) | ||
| ); |
There was a problem hiding this comment.
Do we report an error for await L1(); ...; y++;?
#Resolved
| "; | ||
| var comp = CreateCompilationWithMscorlib46(source + s_interfaces, options: TestOptions.DebugExe); | ||
| comp.VerifyDiagnostics(); | ||
| var verifier = CompileAndVerify(comp, expectedOutput: "ctor1 ctor2 body dispose2 dispose1 caught"); |
There was a problem hiding this comment.
Are we disposing of s1 if an exception is thrown in new S(2)? #Resolved
| public Task DisposeAsync() | ||
| { | ||
| System.Console.Write(""DisposeAsync ""); | ||
| return Task.Delay(10); |
There was a problem hiding this comment.
return Task.Delay(10); [](start = 8, length = 22)
How do we know that this task is awaited after this method is called? #Closed
There was a problem hiding this comment.
Here and in all other similar tests we should await for something inside DisposeAsync and then print something after that. This will confirm that we aren't just calling the method, but also awaiting the task that we are getting from it.
In reply to: 162766376 [](ancestors = 162766376)
There was a problem hiding this comment.
Thanks for the clarification. #Resolved
|
Done with review pass (iteration 12) #Closed |
|
@AlekseyTs Could you take another look when you get the chance? Thanks Btw, I added a work item to umbrella issue (#24037) to deal with IOperation too. |
|
Awesome. Thanks! |
The approach here is to re-use all the binding and lowering logic for
using, but extend it to supportusing await. TheBoundUsingStatementgets a new field to hold an optionalBoundAwaitExpressionwhich will be used during lowering to doawait <expr>.DisposeAsync()instead of<expr>Dispose().All the elements of the bound tree that relate to the
IDisposableinterface become overloaded to deal withIDisposableAsyncin the async scenario.I've done a little bit of IDE testing, which prompted me to fix some control flow analysis.