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

Records: allow positional members to be implemented with fields #52480

Merged
merged 6 commits into from
Apr 15, 2021

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Apr 8, 2021

Test plan: #51199

Opened issue #52630 for disallowing hidden members to be picked for positional usage.
Spec change: dotnet/csharplang#4673

@jcouv jcouv marked this pull request as ready for review April 9, 2021 00:36
@jcouv jcouv requested a review from a team as a code owner April 9, 2021 00:36
@jcouv jcouv mentioned this pull request Apr 10, 2021
92 tasks
@RikkiGibson RikkiGibson self-assigned this Apr 12, 2021
@RikkiGibson
Copy link
Contributor

RikkiGibson commented Apr 13, 2021

Was this test judged to no longer be useful? I expected the behavior to be unchanged here since we consider consts to be static.

    const int P = 4;

src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs (Line 1911)

edit: This seems to be how CodeStream allows commenting on unchanged lines. Seeing it for the first time. Interesting. #Resolved

@jcouv jcouv requested review from a team and AlekseyTs April 13, 2021 02:42
@jcouv
Copy link
Member Author

jcouv commented Apr 13, 2021

I'd removed it because I thought it was duplicate and wrong name. But FieldAsPositionalMember_Const is on record classes. I'll restore


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


if (!memberSignatures.TryGetValue(targetField, out existingMember))
{
existingMember = OverriddenOrHiddenMembersHelpers.FindFirstHiddenMemberIfAny(targetField, memberIsFromSomeCompilation: true);
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 13, 2021

Choose a reason for hiding this comment

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

Does this method return a different value when called with this targetField than when it is called with the targetProperty above?
#Resolved

}
else
{
isInherited = true;
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 13, 2021

Choose a reason for hiding this comment

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

I didn't understand why we avoid setting this for fields. In this case, the field is also inherited, right? We don't use it, but at the same time it seems simpler if we set it unconditionally. #Resolved

Copy link
Member Author

@jcouv jcouv Apr 13, 2021

Choose a reason for hiding this comment

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

We shouldn't set isInherited to true when a field was found from base types here, because we may find a field on the current type (which would be preferred and is not inherited). #Resolved

@@ -3466,7 +3466,6 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde
{
switch (member)
{
case FieldSymbol:
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

case FieldSymbol: [](start = 19, length = 18)

It looks like for fields the only thing we are going to compare is the Name. I think we should consider simply using a separate dictionary for that and avoiding an introduction of SignatureOnlyFieldSymbol with the only purpose to lookup a field by name. #Closed

if (existingMember is null)
{
addProperty(new SynthesizedRecordPropertySymbol(this, syntax, param, isOverride: false, diagnostics));
}
else if (existingMember is FieldSymbol { IsStatic: false } field)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

existingMember is FieldSymbol { IsStatic: false } field [](start = 29, length = 55)

Are we making sure the type matches? Where? #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.

That was missing. Thanks


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

@@ -3743,12 +3744,42 @@ ImmutableArray<PropertySymbol> addProperties(ImmutableArray<ParameterSymbol> rec
if (!memberSignatures.TryGetValue(targetProperty, out var existingMember))
{
existingMember = OverriddenOrHiddenMembersHelpers.FindFirstHiddenMemberIfAny(targetProperty, memberIsFromSomeCompilation: true);
isInherited = true;
if (existingMember is FieldSymbol)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

if (existingMember is FieldSymbol) [](start = 24, length = 34)

The logic feels too convoluted and I think it can be significantly simplified as follows:

  1. First, we look for a matching property in the current record;
  2. Then we look for a matching field in the current record;
  3. Then we look for a first hidden member in base #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.

We shouldn't look at the base last though. A property from the base should win over a field from the current type, to avoid breaking existing scenario (below).
That said, I think we can achieve that by calling FindFirstHiddenMemberIfAny only once and keeping the result.

public record Base 
{
    public int X { get; set; } 
}
public record C(int X) : Base
{
    public new int X;
}

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

Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't look at the base last though. A property from the base should win over a field from the current type, to avoid breaking existing scenario (below).

I don't think this behavior is sound, even though it avoids a breaking change.


In reply to: 612935164 [](ancestors = 612935164,612815137)

/// A representation of a field symbol that is intended only to be used for comparison purposes
/// (esp in MemberSignatureComparer).
/// </summary>
internal sealed class SignatureOnlyFieldSymbol : FieldSymbol
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

SignatureOnlyFieldSymbol [](start = 26, length = 24)

It feels like we can achieve the goal without this type. #Closed

comp.VerifyDiagnostics(
// (6,9): error CS0102: The type 'C' already contains a definition for 'X'
comp.VerifyEmitDiagnostics(
// (4,10): error CS8652: The feature 'positional fields in records' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

// (4,10): error CS8652: The feature 'positional fields in records' is currently in Preview and unsupported. To use Preview features, use the 'preview' language version. [](start = 16, length = 171)

Should we use explicit language version for this compilation so that the test doesn't break once default version is changed? It looks like this comment is applicable to other tests too. #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 checked a few such tests and they all use the CreateCompilation defined in this file with explicit language version 9.


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

.maxstack 2
IL_0000: ldarg.1
IL_0001: ldarg.0
IL_0002: call ""int Base.I.get""
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

IL_0002: call ""int Base.I.get"" [](start = 2, length = 39)

This looks unexpected. I think C.I should be used here. #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

And that is probably going to be a breaking change.


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

Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

It looks like you intentionally tried to preserve previous behavior, but I am not sure if the proposed behavior is the right one to have and whether we explicitly approved it at LDM.


In reply to: 612824890 [](ancestors = 612824890,612824515)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added an open LDM issue to test plan.
#51199

I'll start email thread and also put record structs on the agenda for next Monday, since multiple small questions remain.


In reply to: 612828289 [](ancestors = 612828289,612824890,612824515)

Copy link
Contributor

Choose a reason for hiding this comment

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

Added an open LDM issue to test plan.

Untill this is settled, I do not think we should merge this sub-feature.


In reply to: 612941755 [](ancestors = 612941755,612828289,612824890,612824515)


public record Base
{
public int I = 42;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

int [](start = 11, length = 3)

Are we testing scenario with a type mismatch? #Closed


public record Base
{
public int I { get; set; } = 42;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

public int I { get; set; } = 42; [](start = 4, length = 32)

Are we testing scenario when base has a field and derived has a property? #Closed

}
record C3(int P)
{
internal int P = P;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

int [](start = 13, length = 3)

Are we testing type mismatch scenario? #Closed


record struct A(int X)
{
public int X = X;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 13, 2021

Choose a reason for hiding this comment

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

int [](start = 11, length = 3)

Are we testing type mismatch case? #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 13, 2021

Done with review pass (commit 4) #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 6)

existingMember = inheritedMember;
}
// There should be an error if we picked a member that is hidden
// This will be fixed in C# 9 as part of 16.10. Tracked by https://github.com/dotnet/roslyn/issues/52630
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we plan to do this after feature merge?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. I'll make the breaking change in 16.10 and both changes will meet in features/compiler.

Comment on lines +3474 to +3477
if (!fieldsByName.ContainsKey(fieldName))
{
fieldsByName.Add(fieldName, member);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it feels like this could be replaced with fieldsByName[fieldName] = member; (in error scenarios with multiple identically named fields, which one we pick feels unimportant).

Copy link
Member Author

Choose a reason for hiding this comment

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

If it's okay, I'll leave as-is. Thanks

@jcouv jcouv merged commit d6a3c91 into dotnet:features/record-structs Apr 15, 2021
@jcouv jcouv deleted the record-fields branch April 15, 2021 01:03
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