diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index cdcab56dc..02b24a0d1 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -105,6 +105,7 @@
+
diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index 3c674d65a..d0ab94223 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -404,6 +404,12 @@ namespace ICSharpCode.Decompiler.Tests
RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview);
}
+ [Test]
+ public void Records([ValueSource(nameof(roslynLatestOnlyOptions))] CompilerOptions cscOptions)
+ {
+ RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview);
+ }
+
[Test]
public void NullPropagation([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions)
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
new file mode 100644
index 000000000..4afa52c18
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
@@ -0,0 +1,89 @@
+namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
+{
+ public record Empty
+ {
+ }
+
+ public record Fields
+ {
+ public int A;
+ public double B = 1.0;
+ public object C;
+ public dynamic D;
+ public string S = "abc";
+ }
+
+ public record Pair
+ {
+ public A First {
+ get;
+ init;
+ }
+ public B Second {
+ get;
+ init;
+ }
+ }
+
+ public record Properties
+ {
+ public int A {
+ get;
+ set;
+ }
+ public int B {
+ get;
+ }
+ public int C => 43;
+ public object O {
+ get;
+ set;
+ }
+ public string S {
+ get;
+ set;
+ }
+ public dynamic D {
+ get;
+ set;
+ }
+
+ public Properties()
+ {
+ B = 42;
+ }
+ }
+
+ public abstract record WithNestedRecords
+ {
+ public record A : WithNestedRecords
+ {
+ public override string AbstractProp => "A";
+ }
+
+ public record B : WithNestedRecords
+ {
+ public override string AbstractProp => "B";
+
+ public int? Value {
+ get;
+ set;
+ }
+ }
+
+ public record DerivedGeneric : Pair where T : struct
+ {
+ public bool Flag;
+ }
+
+ public abstract string AbstractProp {
+ get;
+ }
+ }
+}
+namespace System.Runtime.CompilerServices
+{
+ internal class IsExternalInit
+ {
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index b3bc01da1..02b15fdda 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -18,14 +18,11 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
-using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
-using System.Runtime.InteropServices;
using System.Threading;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
@@ -415,6 +412,7 @@ namespace ICSharpCode.Decompiler.CSharp
typeSystemAstBuilder.AddResolveResultAnnotations = true;
typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables;
typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors;
+ typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses;
return typeSystemAstBuilder;
}
@@ -1170,6 +1168,8 @@ namespace ICSharpCode.Decompiler.CSharp
// e.g. DelegateDeclaration
return entityDecl;
}
+ bool isRecord = settings.RecordClasses && typeDef.IsRecord;
+ RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, CancellationToken) : null;
foreach (var type in typeDef.NestedTypes)
{
if (!type.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, type.MetadataToken, settings))
@@ -1179,20 +1179,28 @@ namespace ICSharpCode.Decompiler.CSharp
typeDecl.Members.Add(nestedType);
}
}
- foreach (var field in typeDef.Fields)
+ // With C# 9 records, the relative order of fields and properties matters:
+ IEnumerable fieldsAndProperties = recordDecompiler?.FieldsAndProperties
+ ?? typeDef.Fields.Concat(typeDef.Properties);
+ foreach (var fieldOrProperty in fieldsAndProperties)
{
- if (!field.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, field.MetadataToken, settings))
+ if (fieldOrProperty.MetadataToken.IsNil || MemberIsHidden(module.PEFile, fieldOrProperty.MetadataToken, settings))
+ {
+ continue;
+ }
+ if (fieldOrProperty is IField field)
{
if (typeDef.Kind == TypeKind.Enum && !field.IsConst)
continue;
var memberDecl = DoDecompile(field, decompileRun, decompilationContext.WithCurrentMember(field));
typeDecl.Members.Add(memberDecl);
}
- }
- foreach (var property in typeDef.Properties)
- {
- if (!property.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, property.MetadataToken, settings))
+ else if (fieldOrProperty is IProperty property)
{
+ if (recordDecompiler?.PropertyIsGenerated(property) == true)
+ {
+ continue;
+ }
var propDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property));
typeDecl.Members.Add(propDecl);
}
@@ -1207,6 +1215,10 @@ namespace ICSharpCode.Decompiler.CSharp
}
foreach (var method in typeDef.Methods)
{
+ if (recordDecompiler?.MethodIsGenerated(method) == true)
+ {
+ continue;
+ }
if (!method.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, method.MetadataToken, settings))
{
var memberDecl = DoDecompile(method, decompileRun, decompilationContext.WithCurrentMember(method));
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
index c966c2c87..f24f57489 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
@@ -80,6 +80,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
case ClassType.Enum:
writer.WriteKeyword(Roles.EnumKeyword, "enum");
break;
+ case ClassType.RecordClass:
+ writer.WriteKeyword(Roles.RecordKeyword, "record");
+ break;
default:
throw new Exception("Invalid value for ClassType");
}
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
index c8bb5da81..7c3591f52 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
@@ -1480,6 +1480,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
WriteKeyword(Roles.StructKeyword);
braceStyle = policy.StructBraceStyle;
break;
+ case ClassType.RecordClass:
+ WriteKeyword(Roles.RecordKeyword);
+ braceStyle = policy.ClassBraceStyle;
+ break;
default:
WriteKeyword(Roles.ClassKeyword);
braceStyle = policy.ClassBraceStyle;
diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
new file mode 100644
index 000000000..026268ab7
--- /dev/null
+++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
@@ -0,0 +1,912 @@
+// Copyright (c) 2020 Daniel Grunwald
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Threading;
+
+using ICSharpCode.Decompiler.IL;
+using ICSharpCode.Decompiler.IL.Transforms;
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace ICSharpCode.Decompiler.CSharp
+{
+ class RecordDecompiler
+ {
+ readonly IDecompilerTypeSystem typeSystem;
+ readonly ITypeDefinition recordTypeDef;
+ readonly CancellationToken cancellationToken;
+ readonly List orderedMembers;
+ readonly bool isInheritedRecord;
+ readonly IType baseClass;
+ readonly Dictionary backingFieldToAutoProperty = new Dictionary();
+ readonly Dictionary autoPropertyToBackingField = new Dictionary();
+
+ public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken)
+ {
+ this.typeSystem = dts;
+ this.recordTypeDef = recordTypeDef;
+ this.cancellationToken = cancellationToken;
+ this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class);
+ this.isInheritedRecord = !baseClass.IsKnownType(KnownTypeCode.Object);
+ DetectAutomaticProperties();
+ this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty);
+ }
+
+ void DetectAutomaticProperties()
+ {
+ var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
+ foreach (var property in recordTypeDef.Properties)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var p = (IProperty)property.Specialize(subst);
+ if (IsAutoProperty(p, out var field))
+ {
+ backingFieldToAutoProperty.Add(field, p);
+ autoPropertyToBackingField.Add(p, field);
+ }
+ }
+
+ bool IsAutoProperty(IProperty p, out IField field)
+ {
+ field = null;
+ if (p.Parameters.Count != 0)
+ return false;
+ if (p.Getter != null)
+ {
+ if (!IsAutoGetter(p.Getter, out field))
+ return false;
+ }
+ if (p.Setter != null)
+ {
+ if (!IsAutoSetter(p.Setter, out var field2))
+ return false;
+ if (field != null)
+ {
+ if (!field.Equals(field2))
+ return false;
+ }
+ else
+ {
+ field = field2;
+ }
+ }
+ if (field == null)
+ return false;
+ if (!IsRecordType(field.DeclaringType))
+ return false;
+ return field.Name == $"<{p.Name}>k__BackingField";
+ }
+
+ bool IsAutoGetter(IMethod method, out IField field)
+ {
+ field = null;
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ // return this.field;
+ if (!body.Instructions[0].MatchReturn(out var retVal))
+ return false;
+ if (method.IsStatic)
+ {
+ return retVal.MatchLdsFld(out field);
+ }
+ else
+ {
+ if (!retVal.MatchLdFld(out var target, out field))
+ return false;
+ return target.MatchLdThis();
+ }
+ }
+
+ bool IsAutoSetter(IMethod method, out IField field)
+ {
+ field = null;
+ Debug.Assert(!method.IsStatic);
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ // this.field = value;
+ ILInstruction valueInst;
+ if (method.IsStatic)
+ {
+ if (!body.Instructions[0].MatchStsFld(out field, out valueInst))
+ return false;
+ }
+ else
+ {
+ if (!body.Instructions[0].MatchStFld(out var target, out field, out valueInst))
+ return false;
+ if (!target.MatchLdThis())
+ return false;
+ }
+ if (!valueInst.MatchLdLoc(out var value))
+ return false;
+ if (!(value.Kind == VariableKind.Parameter && value.Index == 0))
+ return false;
+ return body.Instructions[1].MatchReturn(out var retVal) && retVal.MatchNop();
+ }
+ }
+
+ static List DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary backingFieldToAutoProperty)
+ {
+ // For records, the order of members is important:
+ // Equals/GetHashCode/PrintMembers must agree on an order of fields+properties.
+ // The IL metadata has the order of fields and the order of properties, but we
+ // need to detect the correct interleaving.
+ // We could try to detect this from the PrintMembers body, but let's initially
+ // restrict ourselves to the common case where the record only uses properties.
+ var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
+ return recordTypeDef.Properties.Select(p => p.Specialize(subst)).Concat(
+ recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst)).Where(f => !backingFieldToAutoProperty.ContainsKey(f))
+ ).ToList();
+ }
+
+ ///
+ /// Gets the fields and properties of the record type, interleaved as necessary to
+ /// maintain Equals/ToString/etc. semantics.
+ ///
+ public IEnumerable FieldsAndProperties => orderedMembers;
+
+ bool IsRecordType(IType type)
+ {
+ return type.GetDefinition() == recordTypeDef
+ && type.TypeArguments.SequenceEqual(recordTypeDef.TypeParameters);
+ }
+
+ ///
+ /// Gets whether the member of the record type will be automatically generated by the compiler.
+ ///
+ public bool MethodIsGenerated(IMethod method)
+ {
+ if (method.IsConstructor && method.Parameters.Count == 1
+ && IsRecordType(method.Parameters[0].Type))
+ {
+ return IsGeneratedCopyConstructor(method);
+ }
+ switch (method.Name)
+ {
+ // Some members in records are always compiler-generated and lead to a
+ // "duplicate definition" error if we emit the generated code.
+ case "op_Equality":
+ case "op_Inequality":
+ {
+ // Don't emit comparison operators into C# record definition
+ // Note: user can declare additional operator== as long as they have
+ // different parameter types.
+ return method.Parameters.Count == 2
+ && method.Parameters.All(p => IsRecordType(p.Type));
+ }
+ case "Equals" when method.Parameters.Count == 1:
+ {
+ IType paramType = method.Parameters[0].Type;
+ if (paramType.IsKnownType(KnownTypeCode.Object) && method.IsOverride)
+ {
+ // override bool Equals(object? obj): always generated
+ return true;
+ }
+ else if (IsRecordType(paramType))
+ {
+ // virtual bool Equals(R? other): generated unless user-declared
+ return IsGeneratedEquals(method);
+ }
+ else if (isInheritedRecord && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(paramType, baseClass) && method.IsOverride)
+ {
+ // override bool Equals(BaseClass? obj): always generated
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ case "GetHashCode":
+ return IsGeneratedGetHashCode(method);
+ case "$" when method.Parameters.Count == 0:
+ // Always generated; Method name cannot be expressed in C#
+ return true;
+ case "PrintMembers":
+ return IsGeneratedPrintMembers(method);
+ case "ToString" when method.Parameters.Count == 0:
+ return IsGeneratedToString(method);
+ default:
+ return false;
+ }
+ }
+
+ internal bool PropertyIsGenerated(IProperty property)
+ {
+ switch (property.Name)
+ {
+ case "EqualityContract":
+ return IsGeneratedEqualityContract(property);
+ default:
+ return false;
+ }
+ }
+
+ private bool IsGeneratedCopyConstructor(IMethod method)
+ {
+ /*
+ call BaseClass..ctor(ldloc this, ldloc original)
+ stfld k__BackingField(ldloc this, ldfld k__BackingField(ldloc original))
+ leave IL_0000 (nop)
+ */
+ Debug.Assert(method.IsConstructor && method.Parameters.Count == 1);
+ if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any())
+ return false;
+ if (method.Accessibility != Accessibility.Protected)
+ return false;
+ if (orderedMembers == null)
+ return false;
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ var variables = body.Ancestors.OfType().Single().Variables;
+ var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
+ Debug.Assert(IsRecordType(other.Type));
+ int pos = 0;
+ // First instruction is the base constructor call
+ if (!(body.Instructions[pos] is Call { Method: { IsConstructor: true } } baseCtorCall))
+ return false;
+ if (!object.Equals(baseCtorCall.Method.DeclaringType, baseClass))
+ return false;
+ if (baseCtorCall.Arguments.Count != (isInheritedRecord ? 2 : 1))
+ return false;
+ if (!baseCtorCall.Arguments[0].MatchLdThis())
+ return false;
+ if (isInheritedRecord)
+ {
+ if (!baseCtorCall.Arguments[1].MatchLdLoc(other))
+ return false;
+ }
+ pos++;
+ // Then all the fields are copied over
+ foreach (var member in orderedMembers)
+ {
+ if (!(member is IField field))
+ {
+ if (!autoPropertyToBackingField.TryGetValue((IProperty)member, out field))
+ continue;
+ }
+ if (pos >= body.Instructions.Count)
+ return false;
+ if (!body.Instructions[pos].MatchStFld(out var lhsTarget, out var lhsField, out var valueInst))
+ return false;
+ if (!lhsTarget.MatchLdThis())
+ return false;
+ if (!lhsField.Equals(field))
+ return false;
+
+ if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField))
+ return false;
+ if (!rhsTarget.MatchLdLoc(other))
+ return false;
+ if (!rhsField.Equals(field))
+ return false;
+ pos++;
+ }
+ return body.Instructions[pos] is Leave;
+ }
+
+ private bool IsGeneratedEqualityContract(IProperty property)
+ {
+ // Generated member:
+ // protected virtual Type EqualityContract {
+ // [CompilerGenerated] get => typeof(R);
+ // }
+ Debug.Assert(property.Name == "EqualityContract");
+ if (property.Accessibility != Accessibility.Protected)
+ return false;
+ if (!(property.IsVirtual || property.IsOverride))
+ return false;
+ if (property.IsSealed)
+ return false;
+ var getter = property.Getter;
+ if (!(getter != null && !property.CanSet))
+ return false;
+ if (property.GetAttributes().Any())
+ return false;
+ if (getter.GetReturnTypeAttributes().Any())
+ return false;
+ var attrs = getter.GetAttributes().ToList();
+ if (attrs.Count != 1)
+ return false;
+ if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
+ return false;
+ var body = DecompileBody(getter);
+ if (body == null || body.Instructions.Count != 1)
+ return false;
+ if (!(body.Instructions.Single() is Leave leave))
+ return false;
+ // leave IL_0000 (call GetTypeFromHandle(ldtypetoken R))
+ if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty))
+ return false;
+ return IsRecordType(ty);
+ }
+
+ private bool IsGeneratedPrintMembers(IMethod method)
+ {
+ Debug.Assert(method.Name == "PrintMembers");
+ if (method.Parameters.Count != 1)
+ return false;
+ if (!method.IsOverridable)
+ return false;
+ if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any())
+ return false;
+ if (method.Accessibility != Accessibility.Protected)
+ return false;
+ if (orderedMembers == null)
+ return false;
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ var variables = body.Ancestors.OfType().Single().Variables;
+ var builder = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
+ if (builder.Type.ReflectionName != "System.Text.StringBuilder")
+ return false;
+ int pos = 0;
+ if (isInheritedRecord)
+ {
+ // Special case: inherited record adding no new members
+ if (body.Instructions[pos].MatchReturn(out var returnValue)
+ && IsBaseCall(returnValue) && !orderedMembers.Any(IsPrintedMember))
+ {
+ return true;
+ }
+ // if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f {
+ // callvirt Append(ldloc builder, ldstr ", ")
+ // }
+ if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst))
+ return false;
+ if (!IsBaseCall(condition))
+ return false;
+ // trueInst = callvirt Append(ldloc builder, ldstr ", ")
+ trueInst = Block.Unwrap(trueInst);
+ if (!MatchStringBuilderAppend(trueInst, builder, out var val))
+ return false;
+ if (!(val.MatchLdStr(out string text) && text == ", "))
+ return false;
+ pos++;
+
+ bool IsBaseCall(ILInstruction inst)
+ {
+ if (!(inst is CallInstruction { Method: { Name: "PrintMembers" } } call))
+ return false;
+ if (call.Arguments.Count != 2)
+ return false;
+ if (!call.Arguments[0].MatchLdThis())
+ return false;
+ if (!call.Arguments[1].MatchLdLoc(builder))
+ return false;
+ return true;
+ }
+ }
+ bool needsComma = false;
+ foreach (var member in orderedMembers)
+ {
+ if (!IsPrintedMember(member))
+ continue;
+ cancellationToken.ThrowIfCancellationRequested();
+ /*
+ callvirt Append(ldloc builder, ldstr "A")
+ callvirt Append(ldloc builder, ldstr " = ")
+ callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(addressof System.Int32(call get_A(ldloc this))))
+ callvirt Append(ldloc builder, ldstr ", ")
+ callvirt Append(ldloc builder, ldstr "B")
+ callvirt Append(ldloc builder, ldstr " = ")
+ callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(ldflda B(ldloc this)))
+ leave IL_0000 (ldc.i4 1) */
+ if (!MatchStringBuilderAppendConstant(out string text))
+ return false;
+ string expectedText = (needsComma ? ", " : "") + member.Name + " = ";
+ if (text != expectedText)
+ return false;
+ if (!MatchStringBuilderAppend(body.Instructions[pos], builder, out var val))
+ return false;
+ if (val is CallInstruction { Method: { Name: "ToString", IsStatic: false } } toStringCall)
+ {
+ if (toStringCall.Arguments.Count != 1)
+ return false;
+ val = toStringCall.Arguments[0];
+ if (val is AddressOf addressOf)
+ {
+ val = addressOf.Value;
+ }
+ }
+ else if (val is Box box)
+ {
+ if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(box.Type, member.ReturnType))
+ return false;
+ val = box.Argument;
+ }
+ if (val is CallInstruction getterCall && member is IProperty property)
+ {
+ if (!getterCall.Method.Equals(property.Getter))
+ return false;
+ if (getterCall.Arguments.Count != 1)
+ return false;
+ if (!getterCall.Arguments[0].MatchLdThis())
+ return false;
+ }
+ else if (val.MatchLdFld(out var target, out var field) || val.MatchLdFlda(out target, out field))
+ {
+ if (!target.MatchLdThis())
+ return false;
+ if (!field.Equals(member))
+ return false;
+ }
+ else
+ {
+ return false;
+ }
+ pos++;
+ needsComma = true;
+ }
+ // leave IL_0000 (ldc.i4 1)
+ return body.Instructions[pos].MatchReturn(out var retVal)
+ && retVal.MatchLdcI4(needsComma ? 1 : 0);
+
+ bool IsPrintedMember(IMember member)
+ {
+ if (member.IsStatic)
+ {
+ return false; // static fields/properties are not printed
+ }
+ if (member.Name == "EqualityContract")
+ {
+ return false; // EqualityContract is never printed
+ }
+ if (member.IsExplicitInterfaceImplementation)
+ {
+ return false; // explicit interface impls are not printed
+ }
+ if (member.IsOverride)
+ {
+ return false; // override is not printed (again), the virtual base property was already printed
+ }
+ return true;
+ }
+
+ bool MatchStringBuilderAppendConstant(out string text)
+ {
+ text = null;
+ while (MatchStringBuilderAppend(body.Instructions[pos], builder, out var val) && val.MatchLdStr(out string valText))
+ {
+ text += valText;
+ pos++;
+ }
+ return text != null;
+ }
+ }
+
+ private bool MatchStringBuilderAppend(ILInstruction inst, ILVariable sb, out ILInstruction val)
+ {
+ val = null;
+ if (!(inst is CallVirt { Method: { Name: "Append", DeclaringType: { Namespace: "System.Text", Name: "StringBuilder" } } } call))
+ return false;
+ if (call.Arguments.Count != 2)
+ return false;
+ if (!call.Arguments[0].MatchLdLoc(sb))
+ return false;
+ val = call.Arguments[1];
+ return true;
+ }
+
+ private bool IsGeneratedToString(IMethod method)
+ {
+ Debug.Assert(method.Name == "ToString" && method.Parameters.Count == 0);
+ if (!method.IsOverride)
+ return false;
+ if (method.IsSealed)
+ return false;
+ if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any())
+ return false;
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ // stloc stringBuilder(newobj StringBuilder..ctor())
+ if (!body.Instructions[0].MatchStLoc(out var stringBuilder, out var stringBuilderInit))
+ return false;
+ if (!(stringBuilderInit is NewObj { Arguments: { Count: 0 }, Method: { DeclaringTypeDefinition: { Name: "StringBuilder", Namespace: "System.Text" } } }))
+ return false;
+ // callvirt Append(ldloc stringBuilder, ldstr "R")
+ if (!MatchAppendCallWithValue(body.Instructions[1], recordTypeDef.Name))
+ return false;
+ // callvirt Append(ldloc stringBuilder, ldstr " { ")
+ if (!MatchAppendCallWithValue(body.Instructions[2], " { "))
+ return false;
+ // if (callvirt PrintMembers(ldloc this, ldloc stringBuilder)) { trueInst }
+ if (!body.Instructions[3].MatchIfInstruction(out var condition, out var trueInst))
+ return true;
+ if (!(condition is CallVirt { Method: { Name: "PrintMembers" } } printMembersCall))
+ return false;
+ if (printMembersCall.Arguments.Count != 2)
+ return false;
+ if (!printMembersCall.Arguments[0].MatchLdThis())
+ return false;
+ if (!printMembersCall.Arguments[1].MatchLdLoc(stringBuilder))
+ return false;
+ // trueInst: callvirt Append(ldloc stringBuilder, ldstr " ")
+ if (!MatchAppendCallWithValue(Block.Unwrap(trueInst), " "))
+ return false;
+ // callvirt Append(ldloc stringBuilder, ldstr "}")
+ if (!MatchAppendCallWithValue(body.Instructions[4], "}"))
+ return false;
+ // leave IL_0000 (callvirt ToString(ldloc stringBuilder))
+ if (!(body.Instructions[5] is Leave leave))
+ return false;
+ if (!(leave.Value is CallVirt { Method: { Name: "ToString" } } toStringCall))
+ return false;
+ if (toStringCall.Arguments.Count != 1)
+ return false;
+ return toStringCall.Arguments[0].MatchLdLoc(stringBuilder);
+
+ bool MatchAppendCall(ILInstruction inst, out string val)
+ {
+ val = null;
+ if (!(inst is CallVirt { Method: { Name: "Append" } } call))
+ return false;
+ if (call.Arguments.Count != 2)
+ return false;
+ if (!call.Arguments[0].MatchLdLoc(stringBuilder))
+ return false;
+ return call.Arguments[1].MatchLdStr(out val);
+ }
+
+ bool MatchAppendCallWithValue(ILInstruction inst, string val)
+ {
+ return MatchAppendCall(inst, out string tmp) && tmp == val;
+ }
+ }
+
+ private bool IsGeneratedEquals(IMethod method)
+ {
+ // virtual bool Equals(R? other) {
+ // return other != null && EqualityContract == other.EqualityContract && EqualityComparer.Default.Equals(A, other.A) && ...;
+ // }
+ Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1);
+ if (method.Parameters.Count != 1)
+ return false;
+ if (!method.IsOverridable)
+ return false;
+ if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any())
+ return false;
+ if (orderedMembers == null)
+ return false;
+ var body = DecompileBody(method);
+ if (body == null)
+ return false;
+ if (!body.Instructions[0].MatchReturn(out var returnValue))
+ return false;
+ var variables = body.Ancestors.OfType().Single().Variables;
+ var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
+ Debug.Assert(IsRecordType(other.Type));
+ var conditions = UnpackLogicAndChain(returnValue);
+ Debug.Assert(conditions.Count >= 1);
+ int pos = 0;
+ if (isInheritedRecord)
+ {
+ // call BaseClass::Equals(ldloc this, ldloc other)
+ if (pos >= conditions.Count)
+ return false;
+ if (!(conditions[pos] is Call { Method: { Name: "Equals" } } call))
+ return false;
+ if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType, baseClass))
+ return false;
+ if (call.Arguments.Count != 2)
+ return false;
+ if (!call.Arguments[0].MatchLdThis())
+ return false;
+ if (!call.Arguments[1].MatchLdLoc(other))
+ return false;
+ pos++;
+ }
+ else
+ {
+ // comp.o(ldloc other != ldnull)
+ if (pos >= conditions.Count)
+ return false;
+ if (!conditions[pos].MatchCompNotEqualsNull(out var arg))
+ return false;
+ if (!arg.MatchLdLoc(other))
+ return false;
+ pos++;
+ // call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other))
+ // Special-cased because Roslyn isn't using EqualityComparer here.
+ if (pos >= conditions.Count)
+ return false;
+ if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall))
+ return false;
+ if (!opEqualityCall.Method.DeclaringType.IsKnownType(KnownTypeCode.Type))
+ return false;
+ if (opEqualityCall.Arguments.Count != 2)
+ return false;
+ if (!MatchGetEqualityContract(opEqualityCall.Arguments[0], out var target1))
+ return false;
+ if (!MatchGetEqualityContract(opEqualityCall.Arguments[1], out var target2))
+ return false;
+ if (!target1.MatchLdThis())
+ return false;
+ if (!target2.MatchLdLoc(other))
+ return false;
+ pos++;
+ }
+ foreach (var member in orderedMembers)
+ {
+ if (!MemberConsideredForEquality(member))
+ continue;
+ if (member.Name == "EqualityContract")
+ {
+ continue; // already special-cased
+ }
+ // EqualityComparer.Default.Equals(A, other.A)
+ // callvirt Equals(call get_Default(), ldfld k__BackingField(ldloc this), ldfld k__BackingField(ldloc other))
+ if (pos >= conditions.Count)
+ return false;
+ if (!(conditions[pos] is CallVirt { Method: { Name: "Equals" } } equalsCall))
+ return false;
+ if (equalsCall.Arguments.Count != 3)
+ return false;
+ if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], member.ReturnType))
+ return false;
+ if (!MatchMemberAccess(equalsCall.Arguments[1], out var target1, out var member1))
+ return false;
+ if (!MatchMemberAccess(equalsCall.Arguments[2], out var target2, out var member2))
+ return false;
+ if (!target1.MatchLdThis())
+ return false;
+ if (!member1.Equals(member))
+ return false;
+ if (!target2.MatchLdLoc(other))
+ return false;
+ if (!member2.Equals(member))
+ return false;
+ pos++;
+ }
+ return pos == conditions.Count;
+ }
+
+ static List UnpackLogicAndChain(ILInstruction rootOfChain)
+ {
+ var result = new List();
+ Visit(rootOfChain);
+ return result;
+
+ void Visit(ILInstruction inst)
+ {
+ if (inst.MatchLogicAnd(out var lhs, out var rhs))
+ {
+ Visit(lhs);
+ Visit(rhs);
+ }
+ else
+ {
+ result.Add(inst);
+ }
+ }
+ }
+
+ private static bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target)
+ {
+ target = null;
+ if (!(inst is CallVirt { Method: { Name: "get_EqualityContract" } } call))
+ return false;
+ if (call.Arguments.Count != 1)
+ return false;
+ target = call.Arguments[0];
+ return true;
+ }
+
+ private static bool IsEqualityComparerGetDefaultCall(ILInstruction inst, IType type)
+ {
+ if (!(inst is Call { Method: { Name: "get_Default", IsStatic: true } } call))
+ return false;
+ if (!(call.Method.DeclaringType is { Name: "EqualityComparer", Namespace: "System.Collections.Generic" }))
+ return false;
+ if (call.Method.DeclaringType.TypeArguments.Count != 1)
+ return false;
+ if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType.TypeArguments[0], type))
+ return false;
+ return call.Arguments.Count == 0;
+ }
+
+ bool MemberConsideredForEquality(IMember member)
+ {
+ if (member.IsStatic)
+ return false;
+ if (member is IProperty property)
+ {
+ if (property.Name == "EqualityContract")
+ return !isInheritedRecord;
+ return autoPropertyToBackingField.ContainsKey(property);
+ }
+ else
+ {
+ return member is IField;
+ }
+ }
+
+ bool IsGeneratedGetHashCode(IMethod method)
+ {
+ /*
+ return (
+ (
+ EqualityComparer.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer.Default.GetHashCode(A)
+ ) * -1521134295 + EqualityComparer.Default.GetHashCode(B)
+ ) * -1521134295 + EqualityComparer