-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Async-streams: allow pattern-based disposal in await using and foreach #32731
Conversation
f430844
to
ceddc75
Compare
ceddc75
to
d772212
Compare
321c873
to
f0e2f27
Compare
@@ -677,7 +677,7 @@ internal MethodSymbol TryFindDisposePatternMethod(BoundExpression expr, SyntaxNo | |||
{ | |||
Debug.Assert(!(expr is null)); | |||
Debug.Assert(!(expr.Type is null)); | |||
Debug.Assert(expr.Type.IsValueType && expr.Type.IsRefLikeType); // pattern dispose lookup is only valid on ref structs | |||
Debug.Assert((expr.Type.IsValueType && expr.Type.IsRefLikeType) || hasAwait); // pattern dispose lookup is only valid on ref structs or asynchronous usings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we say IsValueType && IsRefLikeType
? When can IsRefLikeType
be true
but IsValueType
is false
?
// Tracked by https://github.com/dotnet/roslyn/issues/32767 | ||
|
||
// extension methods do not contribute to pattern-based disposal | ||
disposeMethod = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disposeMethod
is discarded here but what about any diagnostics that resulted from calling PerformPatternMethodLookup
? Won't those still be in diagnostics
and hence passed back up to the caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the using
scenarios, we keep those diagnostics and add another error (didn't find a proper way of disposing). See UsingPatternScopedExtensionMethodTest
, which produces some unnecessary diagnostics. I'm thinking to leave that as-is for now, with tracking issue.
In the foreach
scenarios, the caller already uses a separate diagnostic bag. I'll add tests to demonstrate.
? this.GetWellKnownType(WellKnownType.System_Threading_Tasks_ValueTask, diagnostics, this._syntax) | ||
: builder.DisposeMethod.ReturnType.TypeSymbol; | ||
|
||
BoundExpression placeholder = new BoundAwaitableValuePlaceholder(_syntax.Expression, awaitableType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Why does the local change the type here of the new
expression? It's not passed by ref
hence couldn't see an obvious reason for this
var comp = CreateCompilationWithTasksExtensions(new[] { source, s_IAsyncEnumerable }, options: TestOptions.DebugExe); | ||
comp.VerifyDiagnostics(); | ||
CompileAndVerify(comp, expectedOutput: "MoveNextAsync DisposeAsync Done"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a test where the enumerator is a ref struct
and verify the IL to ensure we don't accidentally box the value. Possibly add one for normal struct
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No ref structs allowed in async scenarios (can't be captured into a state machine)
Done with review pass (iteration 2) |
f0e2f27
to
e3622a4
Compare
// We won't need to try and bind a second time if it fails, as async dispose can't be pattern based (ref structs are not allowed in async methods) | ||
if (!(type is null) && type.IsValueType && type.IsRefLikeType) | ||
bool isRefStruct = !(type is null) && type.IsValueType && type.IsRefLikeType; | ||
if (!(type is null) && (isRefStruct || hasAwait)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!(type is null) [](start = 20, length = 15)
For readability, consider avoiding the duplicate !(type is null)
check here and above. Perhaps inline isRefStruct
in the expression here. Or perhaps use nested if
blocks:
if (!(type is null))
{
bool isRefStruct = ...;
if (isRefStruct || hasAwait)) { ... }
}
``` #Resolved
{ | ||
return new MyAsyncEnumerator<T>(); | ||
} | ||
public System.Threading.Tasks.ValueTask DisposeAsync() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DisposeAsync [](start = 44, length = 12)
Should this DisposeAsync
be defined on MyAsyncEnumerator<T>
rather than here? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Thanks
} | ||
} | ||
}"; | ||
// DisposeAsync on derived type is ignored, since we don't do runtime check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
derived type is ignored [](start = 31, length = 23)
implementing type ...
? #Resolved
} | ||
public static class Extension | ||
{ | ||
public static System.Threading.Tasks.ValueTask DisposeAsync(this C c) => throw null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C c [](start = 69, length = 3)
Enumerator e
#Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. Thanks
{ | ||
get => throw null; | ||
} | ||
public Awaitable DisposeAsync() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awaitable [](start = 15, length = 9)
Consider testing a Task DisposeAsync()
as well since that might be a common case. #Resolved
} | ||
public async System.Threading.Tasks.ValueTask DisposeAsync(params int[] x) | ||
{ | ||
System.Console.Write($""dispose_start ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dispose_start [](start = 32, length = 13)
Consider including $"x.Length = {x.Length}"
so it's clear DisposeAsync
was called with an array instance rather than null
.
|
||
return 1; | ||
} | ||
public int DisposeAsync() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int [](start = 11, length = 3)
Consider testing Task DisposeAsync()
or Task<bool> DisposeAsync()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latter is a great case. Currently it is accepted, because you can indeed do await enumerator.DisposeAsync();
.
Do you think it should be rejected?
|
||
BoundExpression placeholder = new BoundAwaitableValuePlaceholder(syntax, taskType).MakeCompilerGenerated(); | ||
awaitOpt = originalBinder.BindAwaitInfo(placeholder, syntax, awaitKeyword.GetLocation(), diagnostics, ref hasErrors); | ||
// even if we don't have a proper value to await, we'll still report bad usages of `await` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 an IDE test caught a regression. Even if there is no disposable type, we should report bad usage of await
. Added TestInRegularMethod
to illustrate in compiler tests.
Consider adding comment for #32767 here. Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/UsingStatementTests.cs:445 in e3622a4. [](commit_id = e3622a4, deletion_comment = False) |
Consider adding comment for #32767 here. Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/UsingStatementTests.cs:679 in e3622a4. [](commit_id = e3622a4, deletion_comment = False) |
|
||
[ConditionalFact(typeof(WindowsDesktopOnly))] | ||
[WorkItem(32316, "https://github.com/dotnet/roslyn/issues/32316")] | ||
public void PatternBasedDisposal_NoExtensions_TwoExtensions() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NoExtensions_TwoExtensions [](start = 41, length = 26)
Is this test necessary? Do we look up extension methods for the collection type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is useful. See the tracking issue #32767
In this PR, I reject a binding if it is an extension, but we should really do the binding without considering extensions in the first place. So I added tests to show whether this has an impact or not. It does have an impact on using
(we'll produce an ambiguity diagnostic), but does not impact foreach
(shown here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extension methods take the collection type not the enumerator type though, and would not be considered even if extension methods were supported for pattern-based dispose.
In reply to: 251030838 [](ancestors = 251030838)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, sorry, same testing bug as above. Should be extensions on the enumerator
|
||
[ConditionalFact(typeof(WindowsDesktopOnly))] | ||
[WorkItem(32316, "https://github.com/dotnet/roslyn/issues/32316")] | ||
public void TestPatternBasedDisposal_ReturnsTaskOfInt() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReturnsTaskOfInt [](start = 45, length = 16)
Testing one of Task<int>
or Task<bool>
should be sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the bool
one
3c09cb2
to
898aa41
Compare
} | ||
public sealed class Enumerator | ||
{ | ||
public async System.Threading.Tasks.Task<bool> MoveNextAsync() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.Threading.Tasks. [](start = 21, length = 23)
It looks like the namespace qualifier can be removed in all these tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed unnecessary qualifications
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! |
Customer scenario
Use new BCL helpers
WithCancellation
andConfigureAwait
on anIAsyncEnumerable
.If you
await foreach
over the resulting async collection, but break out of the loop before the end of the collection, the enumerator will not be disposed (but it should).Bugs this fixes
Fixes #32316 (async using and foreach should allow pattern-based disposal)
Fixes #32722 (fix diagnostic message)
Workarounds, if any
None
Risk
Performance impact
Low. We are extending the recently added logic that allows
foreach
to bind to a pattern-basedDispose
method so thatawait foreach
can find aDisposeAsync
method.Is this a regression from a previous update?
No
Root cause analysis
This was supposed to fall out of the feature to bind pattern-based
Dispose
methods. But that feature was implemented later than expected, and it had some restrictions (only allowed for ref structs, due to last minute LDM decision).How was the bug found?
While reviewing the BCL API.
Async-streams umbrella: #24037
Filed https://devdiv.visualstudio.com/DevDiv/_workitems/edit/783198 for shiproom