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

Correctly record the dispose method to be invoked in operations #49481

Merged
merged 26 commits into from
Dec 8, 2020

Conversation

chsienki
Copy link
Contributor

  • Correctly record the dispose method in operations that will be used when not interface based
  • Fix CFG to correctly handle struct based disposal and list the correct method
  • Update and add tests

Fixes #49267

@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler for review

Note: this is targeting @333fred's IOperation refactoring branch rather than master to reduce merge issues

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Done review pass (commit 2)

@@ -485,6 +485,11 @@
</summary>
</Comments>
</Property>
<Property Name="DisposeMethod" Type="IMethodSymbol?">
Copy link
Member

@333fred 333fred Nov 19, 2020

Choose a reason for hiding this comment

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

It doesn't feel like making this only be populated if it's not IDisposable.Dispose is particularly useful, it feels more like an implementation detail bleeding through. I think we should just always populate this, and if it's IDiposable then we populate it with IDisposable.Dispose. #Closed

@@ -3862,7 +3862,7 @@ void processResource(IOperation resource, ArrayBuilder<(IVariableDeclarationOper

LeaveRegion();

AddDisposingFinally(resource, knownToImplementIDisposable: true, iDisposable, isAsynchronous);
AddDisposingFinally(resource, knownToImplementIDisposable: true, iDisposable, disposeMethod, isAsynchronous);
Copy link
Member

@333fred 333fred Nov 19, 2020

Choose a reason for hiding this comment

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

Well, it's certainly not know to implement IDisposable if it's a ref struct. Please update the comments above and the parameter name. #Closed

@chsienki chsienki marked this pull request as draft November 19, 2020 19:38
- Always populate the dispose method in binding, even if its for I{Async}Disposable
- Remove knownToImplementIDisposable
- Just use the precalcualted dispose method in IOperation rather than calculating it
- Use the presence of the DisposeMethod to calculate the expected output in CFG
- Correctly report invalid operations in CFG
- Update tests
@chsienki chsienki marked this pull request as ready for review November 20, 2020 19:24
@chsienki
Copy link
Contributor Author

@333fred / @AlekseyTs assuming tests are good, this should be good to review now.

By always populating the dispose method in binding I was able to remove a bunch of logic in the operation and lowering to just use what we found, instead of calculating it again.

@chsienki
Copy link
Contributor Author

Note to self: run VB tests locally too. 🤦‍♂️

@333fred
Copy link
Member

333fred commented Nov 20, 2020

Note on the determinism test: I haven't merged Aleksey's fix from master into this branch yet, so it's flaky.

AppendNewBlock(endOfFinally);
var isPatternDispose = !disposeMethod.ContainingType.Equals(iDisposable, SymbolEqualityComparer.Default);
var isNull = resource.GetConstantValue() == ConstantValue.Null;
var requiresConversion = !isPatternDispose && !isAsynchronous && !isNull && !_compilation.HasImplicitConversion(resource.Type, iDisposable) && !isNonNullableValueType(resource.Type);
Copy link
Member

Choose a reason for hiding this comment

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

!_compilation.HasImplicitConversion(resource.Type, iDisposable) [](start = 92, length = 63)

If it has an implicit conversion, we probably still need to represent that implicit conversion, right? Or am I misunderstanding the expansion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it has an implicit conversion we'll represent that later on. This checks if we're doing a runtime conversion check. That only happens in foreach on enumerators. The simplest example is with a dynamic enumerator: we try and convert it to IDisposable first then check it for null. I'll give the var a more descriptive name.

@333fred
Copy link
Member

333fred commented Nov 20, 2020

Done review pass (commit 9)

@@ -125,7 +125,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo
if (declarationTypeOpt.IsDynamic())
{
iDisposableConversion = Conversion.ImplicitDynamic;
disposeMethodOpt = null;
disposeMethodOpt = originalBinder.Compilation.GetWellKnownDisposeMethod(isAsync: hasAwait);
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

originalBinder.Compilation.GetWellKnownDisposeMethod(isAsync: hasAwait); [](start = 39, length = 72)

Same comment as for other call-sites #Closed

@@ -185,6 +185,7 @@ bool populateDisposableConversionOrDisposeMethod(bool fromExpression, out Conver

if (iDisposableConversion.IsImplicit)
{
disposeMethodOpt = originalBinder.Compilation.GetWellKnownDisposeMethod(hasAwait);
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

originalBinder.Compilation.GetWellKnownDisposeMethod(hasAwait); [](start = 39, length = 63)

Same comment #Closed

: compilation.GetSpecialType(SpecialType.System_IDisposable);

var enumeratorImplementsDisposable = enumeratorInfoOpt.NeedsDisposal
&& compilation.Conversions.ClassifyImplicitConversionFromType(enumeratorInfoOpt.GetEnumeratorMethod.ReturnType,
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

enumeratorInfoOpt.GetEnumeratorMethod [](start = 115, length = 37)

It looks like we dropped a null check for this value. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was never supposed to be able to contain a null value. The enumeratorInfo has nullable enabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

It was never supposed to be able to contain a null value. The enumeratorInfo has nullable enabled.

It looks to me that definition of ForEachEnumeratorInfo is in nullable disabled region.


In reply to: 533648164 [](ancestors = 533648164)

@@ -1008,7 +1008,7 @@ IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null) (S

<CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)>
<Fact()>
Public Sub UsingFlow_14()
Public Sub UsingFlow_01()
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

UsingFlow_01 [](start = 19, length = 12)

Why was that renamed? Is it possible that it corresponded to C# test with the same name? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Long ago there were UsingFlow_{1-13} in C# and then {14,15} in VB, but C# has since added significantly more tests with {14...} so it makes no sense to have a random set of numbered VB tests. We can move these to the end of the C# tests, but the likelihood is just that they'll clash again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally we'd give them descriptive names tbh

Copy link
Contributor

Choose a reason for hiding this comment

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

We can move these to the end of the C# tests, but the likelihood is just that they'll clash again.

There is no requirement for test methods to be sorted in any way. The test was not affected by the change as well. I find the rename unnecessary.


In reply to: 533652564 [](ancestors = 533652564)

@@ -1152,7 +1152,7 @@ Block[B8] - Exit

<CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)>
<Fact()>
Public Sub UsingFlow_15()
Public Sub UsingFlow_02()
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

UsingFlow_02 [](start = 19, length = 12)

Why was this renamed? #Closed


<CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)>
<Fact()>
Public Sub UsingFlow_04()
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

UsingFlow_04 [](start = 19, length = 12)

Is there a similar C# test? Consider using the same name. #Closed

@@ -1236,5 +1236,171 @@ Block[B6] - Exit
VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics)
End Sub

<CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)>
<Fact()>
Public Sub UsingFlow_03()
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

UsingFlow_03 [](start = 19, length = 12)

Is there a similar C# test? Consider using the same name. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Nov 26, 2020

Done with review pass (commit 20). Overall, it feels to me that a lot of changes could be avoided. Let's discuss offline in more details. #Closed

return new AwaitOperation(invocation, semanticModel: null, value.Syntax, _compilation.GetSpecialType(SpecialType.System_Void), isImplicit: true);
}
// resource.Dispose();
resource = new InvocationOperation(disposeMethod, resource, isVirtual: true,
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 26, 2020

Choose a reason for hiding this comment

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

new InvocationOperation(disposeMethod, resource, isVirtual: true, [](start = 27, length = 65)

Can pattern-based expose use an extension method? In that scenario, the resource should be the first parameter instead. Do we need to handle omitted optional and param array parameters? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, pattern disposal does not support extensions methods. We should report the params and optional arguments, however

Copy link
Contributor

Choose a reason for hiding this comment

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

We should report the params and optional arguments, however

See how we deal with arguments for GetEnumerator and MoveNext in VisitForEachLoop


In reply to: 533774968 [](ancestors = 533774968)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@AlekseyTs I have a fix for this, but its a little involved, as we now need to pass the binder down through the BoundUsingStatement like we do for Foreach. Given the mild complexity, do you want me to add it to this PR, or wait and open a separate one?

Copy link
Contributor

Choose a reason for hiding this comment

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

Given the mild complexity, do you want me to add it to this PR, or wait and open a separate one?

I think a separate PR is fine.


In reply to: 534633638 [](ancestors = 534633638)

// {
// ResourceType resource = expr;
// try { statement; }
// finally { if (resource != null) resource.Dispose(); }
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 2, 2020

Choose a reason for hiding this comment

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

resource [](start = 53, length = 8)

Is this accurate for a nullable value type? We are not unwrapping the Nullable<T>? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the comment to show what we do in lowering.

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated the comment to show what we do in lowering.

Why would you do this? Is this comment for lowering? Is this comment for lowering of a specific compiler?


In reply to: 534592569 [](ancestors = 534592569)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understood your comment to mean that we should be showing the unwrapping of the nullable in CFG, but clearly I misunderstood you.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understood your comment to mean that we should be showing the unwrapping of the nullable in CFG, but clearly I misunderstood you.

I asked you about behavior that you are describing in a new comment specifically for a pattern-based case. I couldn't find a test for it, hence the question. Clearly the comment looked strange to me, but you started looking at completely different scenario.


In reply to: 534620372 [](ancestors = 534620372)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apologies, as I said, I misunderstood what you were asking.

@@ -963,7 +964,7 @@ private MethodSymbol GetGetEnumeratorMethod(NamedTypeSymbol collectionType, Diag
return getEnumeratorMethod;
}

private ForEachEnumeratorInfo.Builder GetDefaultEnumeratorInfo(ForEachEnumeratorInfo.Builder builder, DiagnosticBag diagnostics, TypeSymbol collectionExprType)
private ForEachEnumeratorInfo.Builder GetDefaultEnumeratorInfo(ForEachEnumeratorInfo.Builder builder, DiagnosticBag diagnostics, TypeSymbol collectionExprType, bool isAsync)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 2, 2020

Choose a reason for hiding this comment

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

, bool isAsync [](start = 166, length = 14)

It looks like this parameter is not used. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 2, 2020

Done with review pass (commit 24) #Closed

//
// {
// ResourceType resource = expr;
// try { statement; }
// finally { ((IDisposable)resource).Dispose(); }
// }
//
// Otherwise, when ResourceType is a non-nullable value type that implements
// disposal via pattern, the expansion is:
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 2, 2020

Choose a reason for hiding this comment

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

Are we testing this scenario? #Closed

Copy link
Contributor Author

@chsienki chsienki Dec 3, 2020

Choose a reason for hiding this comment

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

Yes in UsingDeclaration_Flow_20 #Closed

//
// {
// ResourceType resource = expr;
// try { statement; }
// finally { if (resource != null) ((IDisposable)resource).Dispose(); }
// }
//
// Otherwise, when Resource type is a nullable value type or
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 2, 2020

Choose a reason for hiding this comment

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

when Resource type is a nullable value type [](start = 30, length = 43)

Are we testing this scenario? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in UsingFlow_05 but actually, it doesn't appear to be correctly reporting what we do. It matches the comments in the Builder, but that isnt how we lower the code, as you pointed out we unwrap the nullable, which we're not representing here. https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQBMQGoA+ABATAAgAUBYAKAG8z9r9sAWfAWQApGBPAEQEsoAHAeygRgAGzgB+fFwB2vAK4wk+YP34jlASjIB6AFQAeYDJQA+Xdsqka12gAZ8zAOJwY3PoOFjmG/OMkz5GC0rG2pLUNDgfABefBgEOTgAbioIgF9U6jS9fW0jaVNzMkymTh4BIVEJfGdXco8q7xiTOIALBH4Ad3xpORERFNIM0jJYBIBjGFK3Cs84fBBaAEYAOgBJGYaxMnCabABmWgZNqDgm6JaYdq6evoGyNKA=

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, interesting. Actually we're representing in ControlFlowGraph what the spec says we should be doing:
http://sourceroslyn.io/#Microsoft.CodeAnalysis.CSharp/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs,277

Except in lowering we purposefully deviate from the spec to improve perf.

@AlekseyTs what do you think? Should the CFG represent the spirit of the code in the spec, or what we actually lower to? I guess I lean towards representing the spec? because like the async stuff we give a general representation of what it's lowered to, not the implementation specific details.

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks to me that UsingFlow_05 and the sharplab link is about non-pattern disposal and this change is not supposed to make any changes for these scenarios. I am fine with the current behavior of the builder for them. The question I have is about pattern-based disposal for nullable value types. This is what this comment is about. Are we testing the scenario?


In reply to: 534599322 [](ancestors = 534599322)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not currently possible to perform pattern disposal on a nullable value types: see #34701

I will add a test to show the error recovery scenario, and update the comment in CFG to correctly reflect that.

// {
// ResourceType resource = expr;
// try { statement; }
// finally { if (resource.HasValue()) ((IDisposable)resource.GetValueOrDefault()).Dispose(); }
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2020

Choose a reason for hiding this comment

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

finally { if (resource.HasValue()) ((IDisposable)resource.GetValueOrDefault()).Dispose(); } [](start = 21, length = 91)

Where do you see this in flow graph tests? #Closed

Copy link
Contributor Author

@chsienki chsienki Dec 3, 2020

Choose a reason for hiding this comment

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

Mentioned above, we don't. The test shows something different: do we want to make the CFG represent the actual lowered value or what the spec says?

Copy link
Contributor

Choose a reason for hiding this comment

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

Mentioned above, we don't. The test shows something different: do we want to make the CFG represent the actual lowered value or what the spec says?

Does the comment accurately reflect CFG builder's behavior? If so, we don't want to make any changes for the comment. We also do not want to make any changes for scenarios that are not about pattern-based disposal, which is supposed to be the focus of this PR.


In reply to: 534616619 [](ancestors = 534616619)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes apologies. I was confused by your comment, I thought you were saying that the comment, and thus the tests are incorrect for those scenarios.

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 26), assuming that we will deal with the arguments in a separate PR.

@chsienki
Copy link
Contributor Author

chsienki commented Dec 7, 2020

@dotnet/roslyn-compiler / @333fred for a second review please :)

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

LGTM (commit 26)

@chsienki chsienki merged commit e929e00 into dotnet:features/ioperation Dec 8, 2020
333fred added a commit to 333fred/roslyn that referenced this pull request Dec 9, 2020
…c-foreach-binding

* upstream/features/ioperation:
  Correctly record the dispose method to be invoked in operations (dotnet#49481)
@chsienki chsienki linked an issue Jan 14, 2021 that may be closed by this pull request
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.

CFG does not handle pattern dispose correctly
3 participants