Browse Source

Add support for C# 10 record structs.

pull/2696/head
Siegfried Pammer 3 years ago
parent
commit
d248867302
  1. 105
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
  2. 9
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  3. 1
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
  4. 7
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
  5. 6
      ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
  6. 7
      ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs
  7. 1
      ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs
  8. 9
      ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
  9. 18
      ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
  10. 21
      ICSharpCode.Decompiler/DecompilerSettings.cs
  11. 7
      ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs
  12. 4
      ICSharpCode.Decompiler/Output/IAmbience.cs
  13. 2
      ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs
  14. 4
      ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs
  15. 4
      ILSpy/Languages/CSharpHighlightingTokenWriter.cs
  16. 4
      ILSpy/Languages/CSharpLanguage.cs

105
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs

@ -1,6 +1,9 @@
using System; using System;
using System.Runtime.InteropServices;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class RecordClasses
{ {
public record Base(string A); public record Base(string A);
@ -122,6 +125,108 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public abstract string AbstractProp { get; } public abstract string AbstractProp { get; }
} }
}
internal class RecordStructs
{
public record struct Base(string A);
public record CopyCtor(string A)
{
protected CopyCtor(CopyCtor _)
{
}
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public record struct Empty;
public record struct Fields
{
public int A;
public double B;
public object C;
public dynamic D;
public string S;
}
public record struct Interface(int B) : IRecord;
public interface IRecord
{
}
public record struct Pair<A, B>
{
public A First { get; init; }
public B Second { get; init; }
}
public record struct PairWithPrimaryCtor<A, B>(A First, B Second);
public record struct PrimaryCtor(int A, string B);
public record struct PrimaryCtorWithAttribute([RecordTest("param")] [property: RecordTest("property")][field: RecordTest("field")] int a);
public record struct PrimaryCtorWithField(int A, string B)
{
public double C = 1.0;
public string D = A + B;
}
public record struct PrimaryCtorWithInParameter(in int A, in string B);
public record struct PrimaryCtorWithProperty(int A, string B)
{
public double C { get; init; } = 1.0;
public string D { get; } = A + B;
}
public record struct 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()
{
A = 41;
B = 42;
O = null;
S = "Hello";
D = null;
}
}
[AttributeUsage(AttributeTargets.All)]
public class RecordTestAttribute : Attribute
{
public RecordTestAttribute(string name)
{
}
}
public class WithExpressionTests
{
public Fields Test(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input
};
}
public Fields Test2(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input with {
A = 43
}
};
}
}
}
} }
namespace System.Runtime.CompilerServices namespace System.Runtime.CompilerServices
{ {

9
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -505,6 +505,7 @@ namespace ICSharpCode.Decompiler.CSharp
typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables; typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables;
typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors; typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors;
typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses; typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses;
typeSystemAstBuilder.SupportRecordStructs = settings.RecordStructs;
return typeSystemAstBuilder; return typeSystemAstBuilder;
} }
@ -1264,7 +1265,11 @@ namespace ICSharpCode.Decompiler.CSharp
// e.g. DelegateDeclaration // e.g. DelegateDeclaration
return entityDecl; return entityDecl;
} }
bool isRecord = settings.RecordClasses && typeDef.IsRecord; bool isRecord = typeDef.Kind switch {
TypeKind.Class => settings.RecordClasses && typeDef.IsRecord,
TypeKind.Struct => settings.RecordStructs && typeDef.IsRecord,
_ => false,
};
RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null; RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null;
if (recordDecompiler != null) if (recordDecompiler != null)
decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler); decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler);
@ -1311,7 +1316,7 @@ namespace ICSharpCode.Decompiler.CSharp
IEnumerable<IMember> allOrderedMembers = RequiresNativeOrdering(typeDef) ? GetMembersWithNativeOrdering(typeDef) : IEnumerable<IMember> allOrderedMembers = RequiresNativeOrdering(typeDef) ? GetMembersWithNativeOrdering(typeDef) :
fieldsAndProperties.Concat(typeDef.Events).Concat(typeDef.Methods); fieldsAndProperties.Concat(typeDef.Events).Concat(typeDef.Methods);
var allOrderedEntities = typeDef.NestedTypes.Concat<IEntity>(allOrderedMembers); var allOrderedEntities = typeDef.NestedTypes.Concat<IEntity>(allOrderedMembers).ToArray();
// Decompile members that are not compiler-generated. // Decompile members that are not compiler-generated.
foreach (var entity in allOrderedEntities) foreach (var entity in allOrderedEntities)

1
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs

@ -232,6 +232,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
astBuilder.UseNullableSpecifierForValueTypes = (ConversionFlags & ConversionFlags.UseNullableSpecifierForValueTypes) != 0; astBuilder.UseNullableSpecifierForValueTypes = (ConversionFlags & ConversionFlags.UseNullableSpecifierForValueTypes) != 0;
astBuilder.SupportInitAccessors = (ConversionFlags & ConversionFlags.SupportInitAccessors) != 0; astBuilder.SupportInitAccessors = (ConversionFlags & ConversionFlags.SupportInitAccessors) != 0;
astBuilder.SupportRecordClasses = (ConversionFlags & ConversionFlags.SupportRecordClasses) != 0; astBuilder.SupportRecordClasses = (ConversionFlags & ConversionFlags.SupportRecordClasses) != 0;
astBuilder.SupportRecordStructs = (ConversionFlags & ConversionFlags.SupportRecordStructs) != 0;
return astBuilder; return astBuilder;
} }

7
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs

@ -1579,6 +1579,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
WriteKeyword(Roles.RecordKeyword); WriteKeyword(Roles.RecordKeyword);
braceStyle = policy.ClassBraceStyle; braceStyle = policy.ClassBraceStyle;
break; break;
case ClassType.RecordStruct:
WriteKeyword(Roles.RecordStructKeyword);
WriteKeyword(Roles.StructKeyword);
braceStyle = policy.StructBraceStyle;
break;
default: default:
WriteKeyword(Roles.ClassKeyword); WriteKeyword(Roles.ClassKeyword);
braceStyle = policy.ClassBraceStyle; braceStyle = policy.ClassBraceStyle;
@ -1602,7 +1607,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
{ {
constraint.AcceptVisitor(this); constraint.AcceptVisitor(this);
} }
if (typeDeclaration.ClassType == ClassType.RecordClass && typeDeclaration.Members.Count == 0) if (typeDeclaration.ClassType is (ClassType.RecordClass or ClassType.RecordStruct) && typeDeclaration.Members.Count == 0)
{ {
Semicolon(); Semicolon();
} }

6
ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs

@ -721,6 +721,9 @@ namespace ICSharpCode.Decompiler.CSharp
return false; return false;
if (!body.Instructions[0].MatchReturn(out var returnValue)) if (!body.Instructions[0].MatchReturn(out var returnValue))
return false; return false;
// special case for empty record struct; always returns true;
if (returnValue.MatchLdcI4(1))
return true;
var variables = body.Ancestors.OfType<ILFunction>().Single().Variables; var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0); var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
Debug.Assert(IsRecordType(other.Type)); Debug.Assert(IsRecordType(other.Type));
@ -908,6 +911,9 @@ namespace ICSharpCode.Decompiler.CSharp
return false; return false;
if (!body.Instructions[0].MatchReturn(out var returnValue)) if (!body.Instructions[0].MatchReturn(out var returnValue))
return false; return false;
// special case for empty record struct; always returns false;
if (returnValue.MatchLdcI4(0))
return true;
var hashedMembers = new List<IMember>(); var hashedMembers = new List<IMember>();
bool foundBaseClassHash = false; bool foundBaseClassHash = false;
if (!Visit(returnValue)) if (!Visit(returnValue))

7
ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs

@ -35,9 +35,13 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
Interface, Interface,
Enum, Enum,
/// <summary> /// <summary>
/// C# 9 'record' /// C# 9 'record class'
/// </summary> /// </summary>
RecordClass, RecordClass,
/// <summary>
/// C# 10 'record struct'
/// </summary>
RecordStruct,
} }
/// <summary> /// <summary>
@ -62,6 +66,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
case ClassType.Class: case ClassType.Class:
return GetChildByRole(Roles.ClassKeyword); return GetChildByRole(Roles.ClassKeyword);
case ClassType.Struct: case ClassType.Struct:
case ClassType.RecordStruct:
return GetChildByRole(Roles.StructKeyword); return GetChildByRole(Roles.StructKeyword);
case ClassType.Interface: case ClassType.Interface:
return GetChildByRole(Roles.InterfaceKeyword); return GetChildByRole(Roles.InterfaceKeyword);

1
ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs

@ -89,6 +89,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public static readonly TokenRole StructKeyword = new TokenRole("struct"); public static readonly TokenRole StructKeyword = new TokenRole("struct");
public static readonly TokenRole ClassKeyword = new TokenRole("class"); public static readonly TokenRole ClassKeyword = new TokenRole("class");
public static readonly TokenRole RecordKeyword = new TokenRole("record"); public static readonly TokenRole RecordKeyword = new TokenRole("record");
public static readonly TokenRole RecordStructKeyword = new TokenRole("record");
} }
} }

9
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

@ -219,6 +219,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// Controls whether C# 9 "record" class types are supported. /// Controls whether C# 9 "record" class types are supported.
/// </summary> /// </summary>
public bool SupportRecordClasses { get; set; } public bool SupportRecordClasses { get; set; }
/// <summary>
/// Controls whether C# 10 "record" struct types are supported.
/// </summary>
public bool SupportRecordStructs { get; set; }
#endregion #endregion
#region Convert Type #region Convert Type
@ -1775,6 +1780,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
modifiers |= Modifiers.Ref; modifiers |= Modifiers.Ref;
} }
} }
if (SupportRecordStructs && typeDefinition.IsRecord)
{
classType = ClassType.RecordStruct;
}
break; break;
case TypeKind.Enum: case TypeKind.Enum:
classType = ClassType.Enum; classType = ClassType.Enum;

18
ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs

@ -61,10 +61,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration) public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration)
{ {
if (!(constructorDeclaration.Body.Statements.FirstOrDefault() is ExpressionStatement stmt))
return;
var currentCtor = (IMethod)constructorDeclaration.GetSymbol(); var currentCtor = (IMethod)constructorDeclaration.GetSymbol();
ConstructorInitializer ci; ConstructorInitializer ci = null;
if (constructorDeclaration.Body.Statements.FirstOrDefault() is ExpressionStatement stmt)
{
switch (stmt.Expression) switch (stmt.Expression)
{ {
// Pattern for reference types: // Pattern for reference types:
@ -117,11 +117,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
default: default:
return; return;
} }
}
if (context.DecompileRun.RecordDecompilers.TryGetValue(currentCtor.DeclaringTypeDefinition, out var record) if (context.DecompileRun.RecordDecompilers.TryGetValue(currentCtor.DeclaringTypeDefinition, out var record)
&& currentCtor.Equals(record.PrimaryConstructor) && currentCtor.Equals(record.PrimaryConstructor))
&& ci.ConstructorInitializerType == ConstructorInitializerType.Base)
{ {
if (record.IsInheritedRecord && if (record.IsInheritedRecord &&
ci?.ConstructorInitializerType == ConstructorInitializerType.Base &&
constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl) constructorDeclaration.Parent is TypeDeclaration { BaseTypes: { Count: >= 1 } } typeDecl)
{ {
var baseType = typeDecl.BaseTypes.First(); var baseType = typeDecl.BaseTypes.First();
@ -172,7 +173,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (instanceCtorsNotChainingWithThis.Length > 0) if (instanceCtorsNotChainingWithThis.Length > 0)
{ {
var ctorMethodDef = instanceCtorsNotChainingWithThis[0].GetSymbol() as IMethod; var ctorMethodDef = instanceCtorsNotChainingWithThis[0].GetSymbol() as IMethod;
if (ctorMethodDef != null && ctorMethodDef.DeclaringType.IsReferenceType == false) ITypeDefinition declaringTypeDefinition = ctorMethodDef?.DeclaringTypeDefinition;
if (ctorMethodDef != null && declaringTypeDefinition?.IsReferenceType == false && !declaringTypeDefinition.IsRecord)
return; return;
bool ctorIsUnsafe = instanceCtorsNotChainingWithThis.All(c => c.HasModifier(Modifiers.Unsafe)); bool ctorIsUnsafe = instanceCtorsNotChainingWithThis.All(c => c.HasModifier(Modifiers.Unsafe));
@ -187,6 +189,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// Recognize field or property initializers: // Recognize field or property initializers:
// Translate first statement in all ctors (if all ctors have the same statement) into an initializer. // Translate first statement in all ctors (if all ctors have the same statement) into an initializer.
bool allSame; bool allSame;
bool isPrimaryCtor = declaringTypeDefinition.IsReferenceType == true && declaringTypeDefinition.IsRecord;
do do
{ {
Match m = fieldInitializerPattern.Match(instanceCtorsNotChainingWithThis[0].Body.FirstOrDefault()); Match m = fieldInitializerPattern.Match(instanceCtorsNotChainingWithThis[0].Body.FirstOrDefault());
@ -211,11 +214,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// remove record ctor parameter assignments // remove record ctor parameter assignments
if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record)) if (!IsPropertyDeclaredByPrimaryCtor(fieldOrPropertyOrEvent as IProperty, record))
break; break;
isPrimaryCtor = true;
} }
else else
{ {
// cannot transform if member is not found // cannot transform if member is not found
if (fieldOrPropertyOrEventDecl == null) if (fieldOrPropertyOrEventDecl == null || !isPrimaryCtor)
break; break;
} }

21
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -145,6 +145,7 @@ namespace ICSharpCode.Decompiler
if (languageVersion < CSharp.LanguageVersion.CSharp10_0) if (languageVersion < CSharp.LanguageVersion.CSharp10_0)
{ {
fileScopedNamespaces = false; fileScopedNamespaces = false;
recordStructs = false;
} }
if (languageVersion < CSharp.LanguageVersion.CSharp11_0) if (languageVersion < CSharp.LanguageVersion.CSharp11_0)
{ {
@ -156,7 +157,7 @@ namespace ICSharpCode.Decompiler
{ {
if (parameterNullCheck) if (parameterNullCheck)
return CSharp.LanguageVersion.CSharp11_0; return CSharp.LanguageVersion.CSharp11_0;
if (fileScopedNamespaces) if (fileScopedNamespaces || recordStructs)
return CSharp.LanguageVersion.CSharp10_0; return CSharp.LanguageVersion.CSharp10_0;
if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension
|| recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns) || recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns)
@ -262,6 +263,24 @@ namespace ICSharpCode.Decompiler
} }
} }
bool recordStructs = true;
/// <summary>
/// Use C# 10 <c>record</c> structs.
/// </summary>
[Category("C# 10.0 / VS 2022")]
[Description("DecompilerSettings.RecordStructs")]
public bool RecordStructs {
get { return recordStructs; }
set {
if (recordStructs != value)
{
recordStructs = value;
OnPropertyChanged();
}
}
}
bool withExpressions = true; bool withExpressions = true;
/// <summary> /// <summary>

7
ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs

@ -105,6 +105,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
initInst = ci.Arguments.Single(); initInst = ci.Arguments.Single();
break; break;
default: default:
var typeDef = v.Type.GetDefinition();
if (context.Settings.WithExpressions && typeDef?.IsReferenceType == false && typeDef.IsRecord)
{
instType = v.Type;
blockKind = BlockKind.WithInitializer;
break;
}
return false; return false;
} }
int initializerItemsCount = 0; int initializerItemsCount = 0;

4
ICSharpCode.Decompiler/Output/IAmbience.cs

@ -105,6 +105,10 @@ namespace ICSharpCode.Decompiler.Output
/// Support <c>record</c> classes. /// Support <c>record</c> classes.
/// </summary> /// </summary>
SupportRecordClasses = 0x20000, SupportRecordClasses = 0x20000,
/// <summary>
/// Support <c>record</c> structs.
/// </summary>
SupportRecordStructs = 0x40000,
StandardConversionFlags = ShowParameterNames | StandardConversionFlags = ShowParameterNames |
ShowAccessibility | ShowAccessibility |

2
ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs

@ -88,7 +88,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
Nullability NullableContext { get; } Nullability NullableContext { get; }
/// <summary> /// <summary>
/// Gets whether the type has the necessary members to be considered a C# 9 record type. /// Gets whether the type has the necessary members to be considered a C# 9 record or C# 10 record struct type.
/// </summary> /// </summary>
bool IsRecord { get; } bool IsRecord { get; }
} }

4
ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs

@ -758,13 +758,13 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
private bool ComputeIsRecord() private bool ComputeIsRecord()
{ {
if (Kind != TypeKind.Class) if (Kind != TypeKind.Class && Kind != TypeKind.Struct)
return false; return false;
var metadata = module.metadata; var metadata = module.metadata;
var typeDef = metadata.GetTypeDefinition(handle); var typeDef = metadata.GetTypeDefinition(handle);
bool opEquality = false; bool opEquality = false;
bool opInequality = false; bool opInequality = false;
bool clone = false; bool clone = Kind == TypeKind.Struct;
foreach (var methodHandle in typeDef.GetMethods()) foreach (var methodHandle in typeDef.GetMethods())
{ {
var method = metadata.GetMethodDefinition(methodHandle); var method = metadata.GetMethodDefinition(methodHandle);

4
ILSpy/Languages/CSharpHighlightingTokenWriter.cs

@ -231,9 +231,11 @@ namespace ICSharpCode.ILSpy
case "class": case "class":
case "interface": case "interface":
case "delegate": case "delegate":
case "record":
color = referenceTypeKeywordsColor; color = referenceTypeKeywordsColor;
break; break;
case "record":
color = role == Roles.RecordKeyword ? referenceTypeKeywordsColor : valueTypeKeywordsColor;
break;
case "select": case "select":
case "group": case "group":
case "by": case "by":

4
ILSpy/Languages/CSharpLanguage.cs

@ -735,6 +735,10 @@ namespace ICSharpCode.ILSpy
{ {
flags |= ConversionFlags.SupportRecordClasses; flags |= ConversionFlags.SupportRecordClasses;
} }
if (settings.RecordStructs)
{
flags |= ConversionFlags.SupportRecordStructs;
}
if (settings.InitAccessors) if (settings.InitAccessors)
{ {
flags |= ConversionFlags.SupportInitAccessors; flags |= ConversionFlags.SupportInitAccessors;

Loading…
Cancel
Save