Conversation
- Defer the equality of method/field symbol to the substituted symbol when compared - Make the hashcode of substituted method/field defer to the original definition
|
@dotnet/roslyn-compiler for review |
| return new PublicModel.FieldSymbol(this); | ||
| } | ||
|
|
||
| public override bool Equals(Symbol other, TypeCompareKind compareKind) |
There was a problem hiding this comment.
Equals [](start = 29, length = 6)
If we override Equals, I would expect us to override GetHashCode as well. #Closed
There was a problem hiding this comment.
It will just call down into the base though, so it seems redundant? #Resolved
There was a problem hiding this comment.
It will just call down into the base though, so it seems redundant?
That will be informative for the reader and then future changes to Equals might require changes to GetHashCode too, which is easy to forget unless the methods next to each other.
In reply to: 370320715 [](ancestors = 370320715)
| return new PublicModel.MethodSymbol(this); | ||
| } | ||
|
|
||
| public override bool Equals(Symbol other, TypeCompareKind compareKind) |
There was a problem hiding this comment.
Equals [](start = 29, length = 6)
If we override Equals, I would expect us to override GetHashCode as well. #Closed
| // the substituted type is considered equal (e.g. when ignoring nullability). | ||
| // We defer the hashcode to the original definition so it will match in those scenarios. | ||
| // See https://github.com/dotnet/roslyn/issues/38195 | ||
| return this.OriginalDefinition.GetHashCode(); |
There was a problem hiding this comment.
this.OriginalDefinition.GetHashCode() [](start = 19, length = 37)
I think it is still valid to return Hash.Combine(_containingType, OriginalDefinition.GetHashCode());, unless TypeSymbol.Equals(_containingType, other.ContainingType, ALL_IGNORE_OPTIONS) is true #Closed
There was a problem hiding this comment.
For performance, we can replace this condition for containing types with comparison of hash codes
In reply to: 370326121 [](ancestors = 370326121)
There was a problem hiding this comment.
But how would we know that in the hashcode function? We wouldn't have an other to compare against? If we put a substituted method symbol into a dictionary, then did a .Contains() with a regular symbol that would compare equal when ignoring nullability we would fail, even with a comparer that accounted for it. #Resolved
There was a problem hiding this comment.
To clarify, the other in the condition is this.OriginalDefinition
In reply to: 370326741 [](ancestors = 370326741,370326121)
| // the substituted type is considered equal (e.g. when ignoring nullability). | ||
| // We defer the hashcode to the original definition so it will match in those scenarios. | ||
| // See https://github.com/dotnet/roslyn/issues/38195 | ||
| return this.OriginalDefinition.GetHashCode(); |
There was a problem hiding this comment.
return this.OriginalDefinition.GetHashCode(); [](start = 12, length = 45)
Same comment as for substituted fields, we could keep original calculation under some conditions. #Closed
|
|
||
| [Fact] | ||
| [WorkItem(8195, "https://github.com/dotnet/roslyn/issues/38195")] | ||
| public void SemanticModel_SubstitutedEvent_Equality() |
There was a problem hiding this comment.
SemanticModel_SubstitutedEvent_Equality [](start = 20, length = 39)
It doesn't look like we made changes for events. Why does this test succeed? #Closed
There was a problem hiding this comment.
EventSymbol already has a custom equality/hashcode implementation and SubsititutedEventSymbol doesn't override it.
(note: my previous PR attempted to go this route for method + field symbol, but it proved annoyingly complicated to get right in all cases) #Resolved
|
Do we need to make similar changes to VB, it doesn't have nullable annotations, but has custom modifiers. See similar change for types #35613. |
|
Done with review pass (iteration 2) #Closed |
|
Yes, it turns out we're not correctly handling the custom modifier case for VB type comparisons. I took a look at this, but it turned out to be more complicated than expected: we're not actually respecting the type compare kind anywhere in VB symbol comparison, and it's not a simple change to fix it. Given that this isn't directly related to the issue we're trying to fix, I think we should create an issue and fix that in a separate PR. In reply to: 577867650 [](ancestors = 577867650) |
Agree. I really want this fix to get into 16.5 sooner vs. later, as with all our other nullable changes, so we get customer validation on the change. Can take up the VB modopt one separately. |
| // (e.g, ignoring nullability) and want to retain the same hashcode. As such only make | ||
| // the containing type part of the hashcode when we know equality isn't possible | ||
| var containingHashCode = _containingType.GetHashCode(); | ||
| if (containingHashCode != this.OriginalDefinition.ContainingType.GetHashCode()) |
There was a problem hiding this comment.
if (containingHashCode != this.OriginalDefinition.ContainingType.GetHashCode()) [](start = 12, length = 79)
I think we should reverse the condition and return what we have. Otherwise, do what we did before. The if about type arguments below shouldn't be executed if this condition is false. #Closed
|
Done with review pass (iteration 3) #Closed |
- Make SMS hashcode an optional int, rather than using zero to signify not initialized - Invert hashcode condition and exit early
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| # Visual Studio 15 | ||
| VisualStudioVersion = 15.0.27102.0 | ||
| # Visual Studio Version 16 |
There was a problem hiding this comment.
Are changes in this file intentional? #Closed
| private OverriddenOrHiddenMembersResult _lazyOverriddenOrHiddenMembers; | ||
|
|
||
| private int _hashCode; // computed on demand | ||
| private int? _hashCode; // computed on demand |
There was a problem hiding this comment.
int? [](start = 16, length = 4)
This probably makes the code that initializes this field not thread-safe. The assignment is no longer guaranteed to be atomic. I am not sure if the change is warranted, it is unlikely that definition's hash code is 0. We can even assert that in the ComputeHashCode. #Closed
| int code = _hashCode; | ||
|
|
||
| if (code == 0) | ||
| if (!_hashCode.HasValue) |
There was a problem hiding this comment.
!_hashCode.HasValue [](start = 16, length = 19)
The access to properties is probably not thread safe. #Closed
| } | ||
|
|
||
| _hashCode = code; | ||
| _hashCode = ComputeHashCode(); |
There was a problem hiding this comment.
_hashCode = ComputeHashCode(); [](start = 16, length = 30)
This assignment is no longer atomic. I recommend reverting the type change and moving increment into the ComputeHashCode. #Closed
|
Done with review pass (iteration 4) #Closed |
AlekseyTs
left a comment
There was a problem hiding this comment.
LGTM (iteration 7), assuming the tests are passing.
|
Created #41319 to track VB custom modifiers comparison issue |
| // If the containing type of the of the original definition is the same as our containing type | ||
| // it's possible that we will compare equal to the original definition under certain conditions | ||
| // (e.g, ignoring nullability) and want to retain the same hashcode. As such, consider only | ||
| // the the original definition for the hashcode when we know equality is possible |
There was a problem hiding this comment.
the the [](start = 15, length = 7)
Typo.
| var containingHashCode = _containingType.GetHashCode(); | ||
| if (containingHashCode == this.OriginalDefinition.ContainingType.GetHashCode()) | ||
| { | ||
| return code; |
There was a problem hiding this comment.
return code [](start = 16, length = 11)
Any chance this returns 0?
The tweak to the hash code should probably be left in the caller.
There was a problem hiding this comment.
But then we won't match the original definition hash code, which we have to do. If in the rare case it is 0 we'll just call CalculateHashCode each time rather than caching it, but it's pretty cheap as we are just going to call into the original definition and return.
| code = Hash.Combine(containingHashCode, code); | ||
|
|
||
| // Unconstructed method may contain alpha-renamed type parameters while | ||
| // may still be considered equal, we do not want to give different hashcode to such types. |
| { | ||
| if (other is SubstitutedFieldSymbol sfs) | ||
| { | ||
| return sfs.Equals(this, compareKind); |
There was a problem hiding this comment.
Why did we swap the ordering of the Equals call here?
There was a problem hiding this comment.
Because the substituted ones have special logic to compare against the non-substited. We've been through various iterations of solving this and this is sadly the simplest / easiest to reason about.
| return Hash.Combine(_containingType, OriginalDefinition.GetHashCode()); | ||
| var code = this.OriginalDefinition.GetHashCode(); | ||
|
|
||
| // If the containing type of the of the original definition is the same as our containing type |
There was a problem hiding this comment.
| // If the containing type of the of the original definition is the same as our containing type | |
| // If the containing type of the original definition is the same as our containing type |
| // | ||
| // In short - we are not interested in the type arguments of unconstructed methods. | ||
|
|
||
| // If the containing type of the of the original definition is the same as our containing type |
There was a problem hiding this comment.
| // If the containing type of the of the original definition is the same as our containing type | |
| // If the containing type of the original definition is the same as our containing type |
| } | ||
|
|
||
| // 0 means that hashcode is not initialized. | ||
| // in a case we really get 0 for the hashcode, tweak it by +1 |
* dotnet/master: (918 commits) Pure null test on unconstrained type (dotnet#41384) Fix file headers, bootsrap build issues. Make `elementLocations` accept an array of nullable locations in the public api. Learn from non-null tests on as operator (dotnet#41419) Use Microsoft.NET.Sdk.WindowsDesktop for XAML projects (dotnet#40234) Address feedback. Introduce `GetRequiredBinder`. Add missing `NotNullWhen` annotations. Annotate more of the binder Add version of zlib library to deterministic build inputs (dotnet#41385) [master] Update dependencies from dotnet/arcade (dotnet#41354) Simplify Simplify Do not simplify interpolation expressions on implicit receivers. Fix local function crash + add tests Substituted symbol equality (dotnet#41123) Fix test failures Rename from IncludeNonNullableReferenceTypeModifier to IncludeNotNullableReferenceTypeModifier (dotnet#41332) Set TEMP environment variable to $TempDir on CI machines (dotnet#41361) Document placeholders ...
This reverts commit 2512d80.
The changes made in dotnet#41123 fix the GetHashCode for SubstitutedMethodSymbol for more nullability scenarios, but end up hashing all generic methods with a non-generic containing type into the same bucket. This can cause large performance regressions for the compiler. This change ensures that substituted symbols which have substitutions equivalent to their original methods' type parameter continue to be equal, but substituted methods with different type substitutions are not considered equal.
The changes made in #41123 fix the GetHashCode for SubstitutedMethodSymbol for more nullability scenarios, but end up hashing all generic methods with a non-generic containing type into the same bucket. This can cause large performance regressions for the compiler. This change ensures that substituted symbols which have substitutions equivalent to their original methods' type parameter continue to be equal, but substituted methods with different type substitutions are not considered equal.
Third attempt at a sane way of doing this: (see #40939, #38408)
We simply defer to the substituted field + method equal implementations if we're comparing them, and defer back to the non-substituted versions for the hash code.