Skip to content
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

Merged
merged 6 commits into from
Jan 25, 2019

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Jan 24, 2019

Customer scenario

Use new BCL helpers WithCancellation and ConfigureAwait on an IAsyncEnumerable.
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-based Dispose method so that await foreach can find a DisposeAsync 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

@jcouv jcouv added this to the 16.0.P3 milestone Jan 24, 2019
@jcouv jcouv self-assigned this Jan 24, 2019
@jcouv jcouv requested a review from a team as a code owner January 24, 2019 06:18
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jan 24, 2019
@jcouv jcouv removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jan 25, 2019
@jcouv
Copy link
Member Author

jcouv commented Jan 25, 2019

@chsienki @cston @dotnet/roslyn-compiler for review for preview3 tomorrow/Friday. Thanks

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

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

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?

Copy link
Member Author

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

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

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.

Copy link
Member Author

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)

@jaredpar
Copy link
Member

Done with review pass (iteration 2)

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

@cston cston Jan 25, 2019

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

@cston cston Jan 25, 2019

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

Copy link
Member Author

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

@cston cston Jan 25, 2019

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

@cston cston Jan 25, 2019

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

Copy link
Member Author

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

@cston cston Jan 25, 2019

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

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

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().

Copy link
Member Author

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`
Copy link
Member Author

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.

@cston
Copy link
Member

cston commented Jan 25, 2019

            Diagnostic(ErrorCode.ERR_AmbigCall, "S1 s = new S1()").WithArguments("C2.Dispose(S1)", "C3.Dispose(S1)").WithLocation(20, 16),

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)

@cston
Copy link
Member

cston commented Jan 25, 2019

            Diagnostic(ErrorCode.ERR_AmbigCall, "S1 s = new S1()").WithArguments("C2.Dispose(S1, int)", "C3.Dispose(S1, int)").WithLocation(21, 15),

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

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?

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 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).

Copy link
Member

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)

Copy link
Member Author

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the bool one

}
public sealed class Enumerator
{
public async System.Threading.Tasks.Task<bool> MoveNextAsync()
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed unnecessary qualifications

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

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

:shipit:

@jcouv jcouv merged commit 91a119b into dotnet:master Jan 25, 2019
@jcouv jcouv deleted the pattern-disposal branch January 25, 2019 17:40
@jcouv
Copy link
Member Author

jcouv commented Jan 25, 2019

Thanks!

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.

Adjust wording for ERR_NoConvToIDisp Async-streams: await foreach doesn't dispose result from ConfigureAwait
3 participants