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

Record structs: add properties and primary ctor #51720

Merged
merged 8 commits into from
Mar 12, 2021

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Mar 7, 2021

... and Deconstruct method.

Test plan: #51199
Relevant sections of the spec:

Members of a record struct

In addition to the members declared in the record struct body, a record struct type has additional synthesized members.
Members are synthesized unless a member with a "matching" signature is declared in the record struct body or
an accessible concrete non-virtual member with a "matching" signature is inherited.
Two members are considered matching if they have the same
signature or would be considered "hiding" in an inheritance scenario.
See https://github.com/dotnet/csharplang/blob/master/spec/basic-concepts.md#signatures-and-overloading

It is an error for a member of a record struct to be named "Clone".

It is an error for an instance field of a record struct to have an unsafe type.

A record struct is not permitted to declare a destructor.

...

Positional record struct members

In addition to the above members, record structs with a parameter list ("positional records") synthesize
additional members with the same conditions as the members above.

Primary Constructor

A record struct has a public constructor whose signature corresponds to the value parameters of the
type declaration. This is called the primary constructor for the type. It is an error to have a primary
constructor and a constructor with the same signature already present in the struct.
A record struct is not permitted to declare a parameterless primary constructor.

Instance field declarations for a record struct are permitted to include variable initializers.
If there is no primary constructor, the instance initializers execute as part of the parameterless constructor.
Otherwise, at runtime the primary constructor executes the instance initializers appearing in the record-struct-body.

If a record struct has a primary constructor, any user-defined constructor must have an
explicit this constructor initializer.

Parameters of the primary constructor as well as members of the record struct are in scope within initializers of instance fields or properties.
Instance members would be an error in these locations (similar to how instance members are in scope in regular constructor initializers
today, but an error to use), but the parameters of the primary constructor would be in scope and useable and
would shadow members. Static members would also be useable.

A warning is produced if a parameter of the primary constructor is not read.

The definite assigment rules for struct instance constructors apply to the primary constructor of record structs. For instance, the following
is an error:

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

Properties

For each record struct parameter of a record struct declaration there is a corresponding public property
member whose name and type are taken from the value parameter declaration.

For a record struct:

  • A public get and init auto-property is created if the record struct has readonly modifier, get and set otherwise.
    Both kinds of set accessors (set and init) are considered "matching". So the user may declare an init-only property
    in place of a synthesized mutable one.
    An inherited abstract property with matching type is overridden.
    It is an error if the inherited property does not have public get and set/init accessors.
    The auto-property is initialized to the value of the corresponding primary constructor parameter.
    Attributes can be applied to the synthesized auto-property and its backing field by using property: or field:
    targets for attributes syntactically applied to the corresponding record struct parameter.

Deconstruct

A positional record struct with at least one parameter synthesizes a public void-returning instance method called Deconstruct with an out
parameter declaration for each parameter of the primary constructor declaration. Each parameter
of the Deconstruct method has the same type as the corresponding parameter of the primary
constructor declaration. The body of the method assigns each parameter of the Deconstruct method
to the value from an instance member access to a member of the same name.
The method can be declared explicitly. It is an error if the explicit declaration does not match
the expected signature or accessibility, or is static.

@jcouv jcouv added this to the 16.10 milestone Mar 7, 2021
@jcouv jcouv self-assigned this Mar 7, 2021
@jcouv jcouv marked this pull request as ready for review March 8, 2021 20:56
@jcouv jcouv requested a review from a team as a code owner March 8, 2021 20:56
@RikkiGibson RikkiGibson self-assigned this Mar 8, 2021
ParameterListSyntax? paramList = declaredMembersAndInitializers.RecordDeclarationWithParameters?.ParameterList;
ParameterListSyntax? paramList = declaredMembersAndInitializers.RecordDeclarationWithParameters switch
{
RecordDeclarationSyntax recordDecl => recordDecl.ParameterList,
Copy link
Contributor

@RikkiGibson RikkiGibson Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like this could be a useful extension method on TypeDeclarationSyntax #ByDesign

@@ -795,6 +795,7 @@ public static SyntaxKind GetTypeDeclarationKind(SyntaxKind kind)
case SyntaxKind.InterfaceKeyword:
return SyntaxKind.InterfaceDeclaration;
case SyntaxKind.RecordKeyword:
// PROTOTYPE(record-structs): anything we can do?
Copy link
Contributor

@RikkiGibson RikkiGibson Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need an additional parameter for the record keyword. #WontFix

@@ -270,7 +271,8 @@ internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticMode
internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticModel parentModel, int position, PrimaryConstructorBaseTypeSyntax constructorInitializer, out SemanticModel speculativeModel)
{
if (MemberSymbol is SynthesizedRecordConstructor primaryCtor &&
Root.FindToken(position).Parent?.AncestorsAndSelf().OfType<PrimaryConstructorBaseTypeSyntax>().FirstOrDefault() == primaryCtor.GetSyntax().PrimaryConstructorBaseType)
primaryCtor.GetSyntax() is RecordDeclarationSyntax recordDecl &&
Copy link
Contributor

@RikkiGibson RikkiGibson Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that we can't get a speculative model for a record struct here? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here relates to the primary constructor base type, which doesn't exist for record structs. That's why we only need to handle record classes here.


In reply to: 589956647 [](ancestors = 589956647)

@@ -796,6 +796,7 @@ partial class C
[Fact]
public void PartialRecord_ParametersInScopeOfBothParts()
{
// PROTOTYPE(record-structs): ported
Copy link
Contributor

@RikkiGibson RikkiGibson Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the significance of these comments? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much. Just helps keep track of which tests have been duplicated/ported for record-structs. I think this will help with feature review at the end (are there some more tests that we want to move over?). I'll bulk remove those comments once the feature review is done.


In reply to: 589957946 [](ancestors = 589957946)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 9, 2021

@jcouv Could you please clarify the following:

Instance field declarations for a record struct are permitted to include variable initializers.
If there is no primary constructor, the instance initializers execute as part of the parameterless constructor.
Otherwise, at runtime the primary constructor executes the instance initializers appearing in the record-struct-body.

If I remember correctly, structures in C# cannot declare parameter-less constructors and instance initializers are not permitted in structures for this reason, i.e. no constructor to perform the initialization. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 9, 2021

If a record struct has a primary constructor, any user-defined constructor, except "copy constructor" must have an
explicit this constructor initializer.

It looks like the spec doesn't define a "copy constructor". #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 9, 2021

record struct Pos(int X) // definite assignment error in primary constructor

Consider clarifying what is the error to eliminate any guess work. #Closed

@@ -3414,6 +3427,11 @@ private void CheckForStructBadInitializers(DeclaredMembersAndInitializersBuilder
{
Debug.Assert(TypeKind == TypeKind.Struct);

if (builder.RecordDeclarationWithParameters is RecordStructDeclarationSyntax { ParameterList: { ParameterCount: >= 0 } })
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParameterCount: >= 0 [](start = 108, length = 20)

What is the point of this check? #Closed

var ctor = declaredMembersAndInitializers.RecordPrimaryConstructor;
Debug.Assert(ctor is object);
members.Add(ctor);

if (ctor.ParameterCount != 0)
{
// properties and Deconstruct
var existingOrAddedMembers = addProperties(ctor.Parameters);
addDeconstruct(ctor, existingOrAddedMembers);
}

primaryAndCopyCtorAmbiguity = ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this, TypeCompareKind.AllIgnoreOptions);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

primaryAndCopyCtorAmbiguity = ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this, TypeCompareKind.AllIgnoreOptions); [](start = 16, length = 129)

A concept of a copy constructor is not defined in the spec for a record struct. This calculation doesn't make sense then. #Closed

// Ignore the record copy constructor
if (!IsRecord ||
!(SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method) && method is not SynthesizedRecordConstructor))
if (!(IsRecord && SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method) && method is not SynthesizedRecordConstructor))
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!(IsRecord && SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method) && method is not SynthesizedRecordConstructor)) [](start = 32, length = 123)

It is not clear what is the motivation for this change. If it is just a refactoring, please revert it because it doesn't look semantically equivalent. #Closed

Copy link
Member Author

@jcouv jcouv Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not necessary. It is semantically equivalent though (DeMorgan law). I even checked with the built-in refactoring:
image

I think it's easier to understand this way, but if you feel strongly I could revert. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's easier to understand this way, but if you feel strongly I could revert.

If it is not broken, don't fix it


In reply to: 590784798 [](ancestors = 590784798)

@@ -270,7 +271,8 @@ internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticMode
internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticModel parentModel, int position, PrimaryConstructorBaseTypeSyntax constructorInitializer, out SemanticModel speculativeModel)
{
if (MemberSymbol is SynthesizedRecordConstructor primaryCtor &&
Root.FindToken(position).Parent?.AncestorsAndSelf().OfType<PrimaryConstructorBaseTypeSyntax>().FirstOrDefault() == primaryCtor.GetSyntax().PrimaryConstructorBaseType)
primaryCtor.GetSyntax() is RecordDeclarationSyntax recordDecl &&
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

primaryCtor.GetSyntax() is RecordDeclarationSyntax recordDecl && [](start = 16, length = 64)

Alternatively we could check if the containing type is a class. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current approach is okay and relatively compact.


In reply to: 590762612 [](ancestors = 590762612)

{
Debug.Assert(recordStructDecl.ParameterList is object);

Binder bodyBinder = this.GetBinder(recordStructDecl);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binder bodyBinder = this.GetBinder(recordStructDecl) [](start = 12, length = 52)

It doesn't look like the binder is needed #Closed

@jcouv
Copy link
Member Author

jcouv commented Mar 9, 2021

Could you clarify [...] If I remember correctly, structures in C# cannot declare parameter-less constructors and instance initializers are not permitted in structures for this reason, i.e. no constructor to perform the initialization.

As far as the meaning of the spec, there is context in the intro: "Record structs will also follow the same rules as structs for parameterless instance constructors and field initializers, but this document assumes that we will lift those restrictions for structs generally."

So the intention was that instance initializers would be allowed in record-structs.
The scenario without parameter list: record struct S { int field = 1; } is not yet allowed (marked with PROTOTYPE in TypeDeclaration_NoInstanceInitializers).
The scenario with parameter list is allowed already (see RecordProperties_01 for non-empty case; I'll add a test for empty parameter list). I initially had disallowed the scenario with empty parameter list, but then relaxed it (it works). Let me know what you think.


In reply to: 794467406 [](ancestors = 794467406)

@jcouv
Copy link
Member Author

jcouv commented Mar 9, 2021

PR for fixing spec: dotnet/csharplang#4515


In reply to: 794471038 [](ancestors = 794471038)

@AlekseyTs
Copy link
Contributor

I initially had disallowed the scenario with empty parameter list, but then relaxed it (it works). Let me know what you think.

I think "it works" isn't clearly defined at this point. This looks like a violation of the spec: "A record struct is not permitted to declare a parameterless primary constructor." I think an empty parameter list should be disallowed. That should be designed, implemented and tested separately when the other feature is in, or the other feature should take care of this, if record structs are in first.
Also, it is not clear why member initializers wouldn't synthesize a parameter-less constructor in a record struct with a primary constructor, once those are supported. It feels like all this needs additional design work. I think the spec should be cleaned up, the non-primary constructor scenarios removed, or moved to an open issues section.


In reply to: 794541167 [](ancestors = 794541167,794467406)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 9, 2021

PR for fixing spec: dotnet/csharplang#4515

Please make sure to adjust PR description accordingly and reflect the change in the commit comment when this PR will be merged.


In reply to: 794545994 [](ancestors = 794545994,794471038)

@jcouv
Copy link
Member Author

jcouv commented Mar 9, 2021

No, I haven't been including quotes from the spec into the commit comment. So that won't be a problem.
I only included sections of the spec in PR description to clarify the scope and for convenience.


In reply to: 794600331 [](ancestors = 794600331,794590011,794574016,794545994,794471038)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

Done with review pass (commit 3) #Closed

@jcouv
Copy link
Member Author

jcouv commented Mar 10, 2021

Added a test to illustrate. I'll update the spec


In reply to: 794476198 [](ancestors = 794476198)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

    internal override ExecutableCodeBinder TryGetBodyBinder(BinderFactory? binderFactoryOpt = null, bool ignoreAccessibility = false)

Consider asserting that we are not getting here for a record struct. #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordConstructor.cs:69 in 244a64a. [](commit_id = 244a64a, deletion_comment = False)

@@ -140,8 +140,7 @@ internal Binder GetBinder(SyntaxNode node, int position, CSharpSyntaxNode member

internal InMethodBinder GetRecordConstructorInMethodBinder(SynthesizedRecordConstructor constructor)
{
// PROTOTYPE(record-structs): update for record structs
RecordDeclarationSyntax typeDecl = constructor.GetSyntax();
TypeDeclarationSyntax typeDecl = constructor.GetSyntax();
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typeDecl [](start = 34, length = 8)

Consider asserting that this is not a record struct #Closed

{
Debug.Assert(typeDecl.Kind() is SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or SyntaxKind.RecordStructDeclaration [](start = 73, length = 37)

It feels like we should never call this method for a record struct. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

        if (constructor is SynthesizedRecordCopyCtor copyCtor)

It feels like we should return null for a record struct primary constructor here. #Closed


Refers to: src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs:1942 in 244a64a. [](commit_id = 244a64a, deletion_comment = False)

@@ -1963,9 +1967,9 @@ private static void ReportCtorInitializerCycles(MethodSymbol method, BoundExpres
CSharpSyntaxNode containerNode = constructor.GetNonNullSyntaxNode();
BinderFactory binderFactory = compilation.GetBinderFactory(containerNode.SyntaxTree);

if (containerNode is RecordDeclarationSyntax recordDecl)
if (containerNode is RecordDeclarationSyntax or RecordStructDeclarationSyntax)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (containerNode is RecordDeclarationSyntax or RecordStructDeclarationSyntax) [](start = 16, length = 78)

It feels like the change is not necessary, assuming we return null above. #Closed

@@ -1991,6 +1995,10 @@ private static void ReportCtorInitializerCycles(MethodSymbol method, BoundExpres
outerBinder = binderFactory.GetInRecordBodyBinder(recordDecl);
break;

case RecordStructDeclarationSyntax recordStructDecl:
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case RecordStructDeclarationSyntax recordStructDecl: [](start = 16, length = 56)

It feels like the change is not necessary, assuming we return null above. #Closed

@AlekseyTs
Copy link
Contributor

        if (constructor is SynthesizedRecordCopyCtor copyCtor)

In fact, we probably can do this for any constructor in a struct.


In reply to: 795698328 [](ancestors = 795698328)


Refers to: src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs:1942 in 244a64a. [](commit_id = 244a64a, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

        if (constructor is SynthesizedRecordCopyCtor copyCtor)

Or in an enum


In reply to: 795704469 [](ancestors = 795704469,795698328)


Refers to: src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs:1942 in 244a64a. [](commit_id = 244a64a, deletion_comment = False)

case RecordDeclarationSyntax recordDecl:
return recordDecl;
case RecordStructDeclarationSyntax recordStructDecl:
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case RecordStructDeclarationSyntax recordStructDecl: [](start = 16, length = 52)

It feels like this case is not needed, we should never need the binder. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

Done with review pass (commit 5) #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 10, 2021

It looks like there are legitimate test failures. #Closed

@@ -1935,7 +1939,11 @@ private static void ReportCtorInitializerCycles(MethodSymbol method, BoundExpres
}
}

if (constructor is SynthesizedRecordCopyCtor copyCtor)
if (constructor is SynthesizedRecordConstructor && constructor.ContainingType.IsStructType())
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constructor is SynthesizedRecordConstructor && constructor.ContainingType.IsStructType() [](start = 16, length = 88)

This is not blocking, but I would simply change this condition to check if the containing type is a structure or an enum. #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 7)

@jcouv jcouv merged commit b250da4 into dotnet:features/record-structs Mar 12, 2021
@jcouv jcouv deleted the rs-symbol2 branch March 12, 2021 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants