-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Record-structs: Add equality members #51900
Conversation
@dotnet/roslyn-compiler for review. Thanks |
@@ -3934,7 +3945,8 @@ MethodSymbol addThisEquals(PropertySymbol equalityContract) | |||
|
|||
void reportStaticOrNotOverridableAPIInRecord(Symbol symbol, BindingDiagnosticBag diagnostics) | |||
{ | |||
if (!IsSealed && | |||
if (symbol.ContainingType.IsReferenceType && |
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.
symbol.ContainingType.IsReferenceType [](start = 20, length = 37)
isRecordClass? #Closed
BoundExpression objectEqual = F.ObjectEqual(r1, r2); | ||
BoundExpression recordEquals = F.LogicalAnd(F.ObjectNotEqual(r1, F.Null(F.SpecialType(SpecialType.System_Object))), | ||
BoundExpression expression; | ||
if (ContainingType.IsReferenceType) |
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.
ContainingType.IsReferenceType [](start = 20, length = 30)
I think we should use TypeKind or the special property instead. #Closed
@@ -57,10 +64,10 @@ protected sealed override (TypeWithAnnotations ReturnType, ImmutableArray<Parame | |||
return (ReturnType: TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Boolean, location, diagnostics)), | |||
Parameters: ImmutableArray.Create<ParameterSymbol>( | |||
new SourceSimpleParameterSymbol(owner: this, | |||
TypeWithAnnotations.Create(ContainingType, NullableAnnotation.Annotated), | |||
TypeWithAnnotations.Create(ContainingType, ContainingType.IsReferenceType ? NullableAnnotation.Annotated : NullableAnnotation.Oblivious), |
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.
ContainingType.IsReferenceType [](start = 111, length = 30)
I think we should use TypeKind or the special property instead. #Closed
ordinal: 0, RefKind.None, "r1", isDiscard: false, Locations), | ||
new SourceSimpleParameterSymbol(owner: this, | ||
TypeWithAnnotations.Create(ContainingType, NullableAnnotation.Annotated), | ||
TypeWithAnnotations.Create(ContainingType, ContainingType.IsReferenceType ? NullableAnnotation.Annotated : NullableAnnotation.Oblivious), |
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.
ContainingType.IsReferenceType [](start = 111, length = 30)
I think we should use TypeKind or the special property instead. #Closed
: base(containingType, WellKnownMemberNames.ObjectEquals, hasBody: true, memberOffset, diagnostics) | ||
{ | ||
Debug.Assert(equalityContract is null == containingType.IsStructType()); |
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.
containingType.IsStructType() [](start = 53, length = 29)
I think we should be consistent in the way we check for record class vs. record struct. It feels like relying on a special property would be preferred, that way we will be able to quikly locate all the placess that perform the check with the purpose to distinguish kinds of records. #Closed
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'm planning to merge IsRecord
and IsRecordStruct
. So I'll align on TypeKind check for class vs. struct.
In reply to: 597069643 [](ancestors = 597069643)
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.
We could keep an IsRecordStruct => IsRecord && TypeKind == Struct
, but then the IsRecord
check is redundant. Is that what you were thinking?
In reply to: 597866787 [](ancestors = 597866787,597069643)
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.
We could keep an IsRecordStruct => IsRecord && TypeKind == Struct, but then the IsRecord check is redundant. Is that what you were thinking?
I would leave implementation of IsRecordStruct as is, it is correct and efficient.
In reply to: 597888108 [](ancestors = 597888108,597866787,597069643)
{ | ||
var compilation = DeclaringCompilation; | ||
var location = ReturnTypeLocation; | ||
return (ReturnType: TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Boolean, location, diagnostics)), | ||
Parameters: ImmutableArray.Create<ParameterSymbol>( | ||
new SourceSimpleParameterSymbol(owner: this, | ||
TypeWithAnnotations.Create(ContainingType, NullableAnnotation.Annotated), | ||
TypeWithAnnotations.Create(ContainingType, ContainingType.IsReferenceType ? NullableAnnotation.Annotated : NullableAnnotation.Oblivious), |
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.
ContainingType.IsReferenceType [](start = 111, length = 30)
I think we should use TypeKind or the special property instead. #Closed
@@ -62,8 +60,17 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, | |||
// This method is the strongly-typed Equals method where the parameter type is | |||
// the containing type. | |||
|
|||
if (ContainingType.BaseTypeNoUseSiteDiagnostics.IsObjectType()) | |||
bool isRecordStruct = ContainingType.IsStructType(); |
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.
ContainingType.IsStructType() [](start = 38, length = 29)
Same comment as for the constructor. #Closed
{ | ||
// We'll produce: | ||
// virtual bool Equals(T other) => |
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.
virtual [](start = 23, length = 7)
Why would we produce a virtual method in a struct? #Closed
// We'll produce: | ||
// virtual bool Equals(T other) => | ||
// field1 == other.field1 && ... && fieldN == other.fieldN; | ||
retExpr = F.Literal(true); |
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.
F.Literal(true); [](start = 30, length = 16)
This doesn't look usefull. I think we can use null value instead and avoid generating unnecessary nodes here and below. #Closed
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.
We need a true one way or another for the case with no fields. I'll move it to only that case
In reply to: 597075436 [](ancestors = 597075436)
@@ -155,7 +163,10 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, | |||
} | |||
|
|||
fields.Free(); | |||
retExpr = F.LogicalOr(F.ObjectEqual(F.This(), other), retExpr); | |||
if (!isRecordStruct) |
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.
if (!isRecordStruct) [](start = 16, length = 20)
Consider adding empty lines around this if
#Closed
: base(containingType, WellKnownMemberNames.ObjectGetHashCode, memberOffset, diagnostics) | ||
{ | ||
Debug.Assert(containingType.IsReferenceType == equalityContract is not 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.
containingType.IsReferenceType [](start = 25, length = 30)
There is a lack of consistency in terms of how we distinguish kinds of records. #Closed
|
||
if (ContainingType.BaseTypeNoUseSiteDiagnostics.IsObjectType()) | ||
if (ContainingType.IsStructType()) |
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.
ContainingType.IsStructType() [](start = 20, length = 29)
There is a lack of consistency in terms of how we distinguish kinds of records. #Closed
{ | ||
currentHashValue = 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.
currentHashValue [](start = 20, length = 16)
Should we add hash code for the type in order to add variance across different types of record structs? #Closed
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'm not sure I follow. You mean if we did struct inheritance?
In reply to: 597084774 [](ancestors = 597084774)
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'm not sure I follow. You mean if we did struct inheritance?
No, inheritance isn't part of this. This is about instances of record struct S1;
and instances of record struct S2;
having distict hash codes. The same way we do this for record classes.
In reply to: 597882249 [](ancestors = 597882249,597084774)
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.
Current spec doesn't include that.
I'll add an open question: dotnet/csharplang#4564
In reply to: 597887909 [](ancestors = 597887909,597882249,597084774)
{ | ||
var compilation = DeclaringCompilation; | ||
var location = ReturnTypeLocation; | ||
return (ReturnType: TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Boolean, location, diagnostics)), | ||
Parameters: ImmutableArray.Create<ParameterSymbol>( | ||
new SourceSimpleParameterSymbol(owner: this, | ||
TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Object, location, diagnostics), NullableAnnotation.Annotated), | ||
TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Object, location, diagnostics), | ||
ContainingType.IsReferenceType ? NullableAnnotation.Annotated : NullableAnnotation.Oblivious), |
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.
ContainingType.IsReferenceType [](start = 72, length = 30)
There is a lack of consistency in terms of how we distinguish kinds of records. #Closed
F.CloseMethod(F.ThrowNull()); | ||
return; | ||
} | ||
|
||
var paramAccess = F.Parameter(Parameters[0]); | ||
|
||
BoundExpression expression; | ||
if (ContainingType.IsStructType()) |
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.
ContainingType.IsStructType() [](start = 20, length = 29)
There is a lack of consistency in terms of how we distinguish kinds of records. #Closed
var paramAccess = F.Parameter(Parameters[0]); | ||
|
||
BoundExpression expression; | ||
if (ContainingType.IsStructType()) | ||
{ | ||
throw ExceptionUtilities.Unreachable; | ||
// For record structs: | ||
// return other is R && Equals((R)other) |
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.
other is R && Equals((R)other) [](start = 35, length = 30)
The spec suggests a different code - other is R temp && Equals(temp)
#Closed
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 underlying IL is probably going to be equivalent though.
In reply to: 597087812 [](ancestors = 597087812)
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.
Yes. We don't need to store the temp. I can update the spec if you feel the distinction is meaningful.
In reply to: 597090857 [](ancestors = 597090857,597087812)
[Fact] | ||
public void RecordEquals_01() | ||
{ | ||
// PROTOTYPE(record-structs): ported |
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.
// PROTOTYPE(record-structs): ported [](start = 12, length = 36)
Is this comment meaningful? #Closed
IL_0001: ret | ||
}"); | ||
|
||
var recordEquals = comp.GetMembers("A.Equals").OfType<SynthesizedRecordEquals>().Single(); |
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.
Single [](start = 93, length = 6)
It looks like there are two Equals methods in the type. We should check the other one too, I think. #Closed
Diagnostic(ErrorCode.WRN_NoRuntimeMetadataVersion).WithLocation(1, 1) | ||
); | ||
|
||
var recordEquals = comp.GetMembers("A.Equals").OfType<SynthesizedRecordEquals>().Single(); |
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.
Single [](start = 93, length = 6)
I am confused, why Single succeeds here? #Closed
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.
Because of .OfType<SynthesizedRecordEquals>()
. There is only one "Equals(R). The other one is an
Equals(object)which is
SynthesizedRecordObjEquals`.
In reply to: 597114534 [](ancestors = 597114534)
Do we have tests for modifiers on all new methods we generate in a record struct? #Closed Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs:15 in 38c9ec2. [](commit_id = 38c9ec2, deletion_comment = False) |
Done with review pass (commit 2) #Closed |
Added coverage for GetHashCode In reply to: 802171871 [](ancestors = 802171871) Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs:15 in 38c9ec2. [](commit_id = 38c9ec2, deletion_comment = False) |
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.
LGTM (commit 3)
@dotnet/roslyn-compiler for second review. Thanks |
@@ -54,13 +61,14 @@ protected sealed override (TypeWithAnnotations ReturnType, ImmutableArray<Parame | |||
{ | |||
var compilation = DeclaringCompilation; | |||
var location = ReturnTypeLocation; | |||
var annotation = ContainingType.IsRecordStruct ? NullableAnnotation.Oblivious : NullableAnnotation.Annotated; |
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.
What's the effect of using Oblivious here vs NotAnnotated?
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.
There's no effect because TypeWithAnnotation
already has special case for structs, but it seemed misleading to call the API with NotAnnotated unconditionally.
record struct C(bool X) | ||
{ | ||
var src = @" | ||
readonly record struct C(bool X) |
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.
Were adjacent tests combined 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.
Add
IEquatable<T>
interface,Equals
andGetHashCode
methods and equality/inequality operators.Implements this section of the spec (copied below for convenience).
Test plan: #51199