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

Implement auto-generated Equals for records #41401

Merged
merged 6 commits into from
Feb 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -5952,7 +5952,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<value>records</value>
</data>
<data name="ERR_BadRecordDeclaration" xml:space="preserve">
<value>Records must have both a 'data' modifier and parameter list</value>
<value>Records must have both a 'data' modifier and non-empty parameter list</value>
</data>
<data name="ERR_DuplicateRecordConstructor" xml:space="preserve">
<value>There cannot be a primary constructor and a member constructor with the same parameter types.</value>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Emit
{
internal static class EqualityMethodBodySynthesizer
{
public static BoundExpression GenerateEqualsComparisons<TList>(
NamedTypeSymbol containingType,
BoundExpression otherAccess,
TList componentFields,
SyntheticBoundNodeFactory F) where TList : IReadOnlyList<FieldSymbol>
{
Debug.Assert(componentFields.Count > 0);

// Expression:
//
// System.Collections.Generic.EqualityComparer<T_1>.Default.Equals(this.backingFld_1, value.backingFld_1)
// ...
// && System.Collections.Generic.EqualityComparer<T_N>.Default.Equals(this.backingFld_N, value.backingFld_N);

// prepare symbols
var equalityComparer_get_Default = F.WellKnownMethod(
WellKnownMember.System_Collections_Generic_EqualityComparer_T__get_Default);
var equalityComparer_Equals = F.WellKnownMethod(
WellKnownMember.System_Collections_Generic_EqualityComparer_T__Equals);

NamedTypeSymbol equalityComparerType = equalityComparer_Equals.ContainingType;

BoundExpression? retExpression = null;

// Compare fields
foreach (var field in componentFields)
{
// Prepare constructed symbols
var constructedEqualityComparer = equalityComparerType.Construct(field.Type);

// System.Collections.Generic.EqualityComparer<T_index>.
// Default.Equals(this.backingFld_index, local.backingFld_index)'
BoundExpression nextEquals = F.Call(
F.StaticCall(constructedEqualityComparer,
equalityComparer_get_Default.AsMember(constructedEqualityComparer)),
equalityComparer_Equals.AsMember(constructedEqualityComparer),
F.Field(F.This(), field),
F.Field(otherAccess, field));

// Generate 'retExpression' = 'retExpression && nextEquals'
retExpression = retExpression is null
? nextEquals
: F.LogicalAnd(retExpression, nextEquals);
}

RoslynDebug.AssertNotNull(retExpression);
agocke marked this conversation as resolved.
Show resolved Hide resolved

return retExpression;
}
}
}
4 changes: 4 additions & 0 deletions src/Compilers/CSharp/Portable/Symbols/LexicalSortKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public int Position

public static readonly LexicalSortKey NotInitialized = new LexicalSortKey() { _treeOrdinal = -1, _position = -1 };

// Put Record Equals right before synthesized constructors.
public static LexicalSortKey SynthesizedRecordEquals => new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 3 };
public static LexicalSortKey SynthesizedRecordObjEquals => new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 2 };

// Dev12 compiler adds synthetic constructors to the child list after adding all other members.
// Methods are emitted in the children order, but synthetic cctors would be deferred
// until later when it is known if they can be optimized or not.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2920,7 +2920,7 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde
return;
}

if (!_declModifiers.HasFlag(DeclarationModifiers.Data))
if (paramList.ParameterCount == 0 || !_declModifiers.HasFlag(DeclarationModifiers.Data))
{
diagnostics.Add(ErrorCode.ERR_BadRecordDeclaration, paramList.Location);
}
Expand All @@ -2930,24 +2930,69 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde

var memberSignatures = new HashSet<Symbol>(members, MemberSignatureComparer.DuplicateSourceComparer);

var ctor = new SynthesizedRecordConstructor(this, binder, paramList, diagnostics);
if (!memberSignatures.Contains(ctor))
var ctor = addCtor(paramList);
addProperties(ctor.Parameters);
addObjectEquals();
addHashCode();
addThisEquals();

return;

SynthesizedRecordConstructor addCtor(ParameterListSyntax paramList)
{
members.Add(ctor);
var ctor = new SynthesizedRecordConstructor(this, binder, paramList, diagnostics);
if (!memberSignatures.Contains(ctor))
{
members.Add(ctor);
}
else
{
diagnostics.Add(ErrorCode.ERR_DuplicateRecordConstructor, paramList.Location);
}

return ctor;
}
else

void addProperties(ImmutableArray<ParameterSymbol> recordParameters)
{
foreach (ParameterSymbol param in ctor.Parameters)
{
var property = new SynthesizedRecordPropertySymbol(this, param);
if (!memberSignatures.Contains(property))
{
members.Add(property);
members.Add(property.GetMethod);
members.Add(property.BackingField);
}
}
}

void addObjectEquals()
{
var objEquals = new SynthesizedRecordObjEquals(this);
if (!memberSignatures.Contains(objEquals))
{
// PROTOTYPE: Don't add if the overridden method is sealed
members.Add(objEquals);
}
}

void addHashCode()
{
diagnostics.Add(ErrorCode.ERR_DuplicateRecordConstructor, paramList.Location);
var hashCode = new SynthesizedRecordGetHashCode(this);
if (!memberSignatures.Contains(hashCode))
{
// PROTOTYPE: Don't add if the overridden method is sealed
members.Add(hashCode);
}
}

foreach (ParameterSymbol param in ctor.Parameters)
void addThisEquals()
{
var property = new SynthesizedRecordPropertySymbol(this, param);
if (!memberSignatures.Contains(property))
var thisEquals = new SynthesizedRecordEquals(this);
if (!memberSignatures.Contains(thisEquals))
{
members.Add(property);
members.Add(property.GetMethod);
members.Add(property.BackingField);
members.Add(thisEquals);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
agocke marked this conversation as resolved.
Show resolved Hide resolved


using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Microsoft.Cci;
using Microsoft.CodeAnalysis.CSharp.Emit;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
internal sealed class SynthesizedRecordEquals : SynthesizedInstanceMethodSymbol
{
public override NamedTypeSymbol ContainingType { get; }

public SynthesizedRecordEquals(NamedTypeSymbol containingType)
{
ContainingType = containingType;
}

public override string Name => "Equals";

public override MethodKind MethodKind => MethodKind.Ordinary;

public override int Arity => 0;

public override bool IsExtensionMethod => false;

public override bool HidesBaseMethodsByName => true;

public override bool IsVararg => false;

public override bool ReturnsVoid => false;

public override bool IsAsync => false;

public override RefKind RefKind => RefKind.None;

public override ImmutableArray<ParameterSymbol> Parameters
=> ImmutableArray.Create<ParameterSymbol>(SynthesizedParameterSymbol.Create(
this,
TypeWithAnnotations.Create(
isNullableEnabled: true,
ContainingType,
isAnnotated: true),
ordinal: 0,
ContainingType.IsStructType() ? RefKind.In : RefKind.None));

public override TypeWithAnnotations ReturnTypeWithAnnotations => TypeWithAnnotations.Create(
isNullableEnabled: true,
ContainingType.DeclaringCompilation.GetSpecialType(SpecialType.System_Boolean));

public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None;

public override ImmutableHashSet<string> ReturnNotNullIfParameterNotNull => ImmutableHashSet<string>.Empty;

public override ImmutableArray<TypeWithAnnotations> TypeArgumentsWithAnnotations
=> ImmutableArray<TypeWithAnnotations>.Empty;

public override ImmutableArray<TypeParameterSymbol> TypeParameters => ImmutableArray<TypeParameterSymbol>.Empty;

public override ImmutableArray<MethodSymbol> ExplicitInterfaceImplementations => ImmutableArray<MethodSymbol>.Empty;

public override ImmutableArray<CustomModifier> RefCustomModifiers => ImmutableArray<CustomModifier>.Empty;

public override Symbol? AssociatedSymbol => null;

public override Symbol ContainingSymbol => ContainingType;

public override ImmutableArray<Location> Locations => ContainingType.Locations;

public override Accessibility DeclaredAccessibility => Accessibility.Public;

public override bool IsStatic => false;

public override bool IsVirtual => false;

public override bool IsOverride => false;

public override bool IsAbstract => false;

public override bool IsSealed => false;

public override bool IsExtern => false;

internal override bool HasSpecialName => false;

internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.SynthesizedRecordEquals;

internal override MethodImplAttributes ImplementationAttributes => MethodImplAttributes.Managed;

internal override bool HasDeclarativeSecurity => false;

internal override MarshalPseudoCustomAttributeData? ReturnValueMarshallingInformation => null;

internal override bool RequiresSecurityObject => false;

internal override CallingConvention CallingConvention => CallingConvention.HasThis;

internal override bool GenerateDebugInfo => false;

public override DllImportData? GetDllImportData() => null;

internal override ImmutableArray<string> GetAppliedConditionalSymbols()
=> ImmutableArray<string>.Empty;

internal override IEnumerable<SecurityAttribute> GetSecurityInformation()
=> Array.Empty<SecurityAttribute>();

internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false;

internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => false;

internal override bool SynthesizesLoweredBoundBody => true;

internal override void GenerateMethodBody(TypeCompilationState compilationState, DiagnosticBag diagnostics)
agocke marked this conversation as resolved.
Show resolved Hide resolved
{
var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics);

// Compare all of the record properties in this class with the argument properties
// Body:
// {
// return other != null && `comparisons`;
agocke marked this conversation as resolved.
Show resolved Hide resolved
// }

var other = F.Parameter(Parameters[0]);
BoundExpression? retExpr = null;
if (!ContainingType.IsStructType())
{
retExpr = F.ObjectNotEqual(other, F.Null(F.SpecialType(SpecialType.System_Object)));
}

var recordProperties = ArrayBuilder<FieldSymbol>.GetInstance();
foreach (var member in ContainingType.GetMembers())
{
// PROTOTYPE: Should generate equality on user-written members as well
if (member is SynthesizedRecordPropertySymbol p)
agocke marked this conversation as resolved.
Show resolved Hide resolved
{
recordProperties.Add(p.BackingField);
}
}
if (recordProperties.Count > 0)
{
var comparisons = EqualityMethodBodySynthesizer.GenerateEqualsComparisons(
ContainingType,
other,
recordProperties,
F);
retExpr = F.LogicalAnd(retExpr, comparisons);
}
recordProperties.Free();

F.CloseMethod(F.Block(F.Return(retExpr)));
}
}
}
Loading