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 ToString and PrintMembers #52012

Merged
merged 4 commits into from
Mar 29, 2021

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Mar 20, 2021

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/record-structs.md (relevant section copied below for convenience)
Test plan: #51199

Printing members: PrintMembers and ToString methods

The record struct includes a synthesized method equivalent to a method declared as follows:

private bool PrintMembers(System.Text.StringBuilder builder);

The method does the following:

  1. for each of the record struct's printable members (non-static public field and readable property members), appends that member's name followed by " = " followed by the member's value separated with ", ",
  2. return true if the record struct has printable members.

For a member that has a value type, we will convert its value to a string representation using the most efficient method available to the target platform. At present that means calling ToString before passing to StringBuilder.Append.

The PrintMembers method can be declared explicitly.
It is an error if the explicit declaration does not match the expected signature or accessibility.

The record struct includes a synthesized method equivalent to a method declared as follows:

public override string ToString();

The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility.

The synthesized method:

  1. creates a StringBuilder instance,
  2. appends the record struct name to the builder, followed by " { ",
  3. invokes the record struct's PrintMembers method giving it the builder, followed by " " if it returned true,
  4. appends "}",
  5. returns the builder's contents with builder.ToString().

For example, consider the following record struct:

record struct R1(T1 P1, T2 P2);

For this record struct, the synthesized printing members would be something like:

@jcouv jcouv self-assigned this Mar 20, 2021
@jcouv jcouv added this to the C# 10 milestone Mar 20, 2021
///
///
/// For record struct:
/// public static bool operator==(R r1, R r2)
Copy link
Member

Choose a reason for hiding this comment

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

Should be left and right based on #51973?

Copy link
Member Author

Choose a reason for hiding this comment

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

It will. That'll get picked up next time the feature branch is refreshed with changes from main branch.

@jcouv jcouv force-pushed the rs-symbol5 branch 2 times, most recently from 4fb75c8 to 94d781b Compare March 23, 2021 18:13
@@ -1081,7 +1081,7 @@ record C
"System.String! C.Y { get; init; }",
"System.String! C.Y.get",
"void C.Y.init",
"System.String C.ToString()",
Copy link
Member Author

@jcouv jcouv Mar 23, 2021

Choose a reason for hiding this comment

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

📝 we were missing the annotation on record's ToString #Resolved

@jcouv jcouv marked this pull request as ready for review March 23, 2021 23:54
@jcouv jcouv requested a review from a team as a code owner March 23, 2021 23:54
@jcouv
Copy link
Member Author

jcouv commented Mar 25, 2021

@AlekseyTs @dotnet/roslyn-compiler for review. Thanks

DeclarationModifiers.Private :
DeclarationModifiers.Protected;

if (virtualPrintInBase() is object)
if (ContainingType.IsRecord && virtualPrintInBase() is object)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

virtualPrintInBase() [](start = 43, length = 20)

This logic doesn't feel right. If a record class doesn't inherit from object, the method should always have Override flag. If base doesn't contain the right method to override, an appropriate error is going to be reported about that. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 25, 2021

            result |= ContainingType.IsSealed ? DeclarationModifiers.None : DeclarationModifiers.Virtual;

Is this true for a record struct? #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPrintMembers.cs:42 in 21dd0d0. [](commit_id = 21dd0d0, deletion_comment = False)

@@ -3671,7 +3667,7 @@ MethodSymbol addPrintMembersMethod()
else
{
printMembersMethod = (MethodSymbol)existingPrintMembersMethod;
if (this.IsSealed && this.BaseTypeNoUseSiteDiagnostics.IsObjectType())
if ((this.IsSealed && this.BaseTypeNoUseSiteDiagnostics.IsObjectType()) || !isRecordClass)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

this.IsSealed [](start = 25, length = 13)

Is this true for a record struct? If so, consider checking !isRecordClass first. #Closed

@@ -124,6 +130,14 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState,
return;
}
}
else if (ContainingType.IsRecordStruct)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

ContainingType.IsRecordStruct [](start = 25, length = 29)

It looks like we can combine this condition with the previous if #Closed

@@ -113,7 +119,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState,
return;
}

ArrayBuilder<BoundStatement>? block = printableMembers.IsEmpty ? null : ArrayBuilder<BoundStatement>.GetInstance();
ArrayBuilder<BoundStatement>? block = (printableMembers.IsEmpty && !ContainingType.IsRecordStruct) ? null : ArrayBuilder<BoundStatement>.GetInstance();
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

&& !ContainingType.IsRecordStruct [](start = 80, length = 33)

Addition of this condition looks suspicious. Are we going to return from the added if below without releasing the builder? #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.

Thanks. That was indeed a problem.


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

F.CloseMethod(F.Return(F.Literal(false)));
return;
}
}
else
{
MethodSymbol? printMethod = FindValidPrintMembersMethod(ContainingType.BaseTypeNoUseSiteDiagnostics, DeclaringCompilation);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

FindValidPrintMembersMethod [](start = 48, length = 27)

Usage of this method here doesn't feel right. Should we simply get OverriddenMethod here? #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 25, 2021

    internal static MethodSymbol? FindValidPrintMembersMethod(TypeSymbol containingType, CSharpCompilation compilation)

It is quite possible I am missing something, but it looks to me that we shouldn't be needing this method. I already commented on the two call sites in this file and I believe we should remove the other remaining call as well. Instead we should override MethodChecks where we would verify that OverriddenMethod belongs to the immediate base. #Closed


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

// static private bool PrintMembers(System.Text.StringBuilder builder) => throw null;
Diagnostic(ErrorCode.ERR_StaticAPIInRecord, "PrintMembers").WithArguments("C1.PrintMembers(System.Text.StringBuilder)").WithLocation(4, 25)
);
}
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2021

Choose a reason for hiding this comment

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

} [](start = 8, length = 1)

Are we testing a scenario when synthesized PrintMembers used explicitly by user code? #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.

Updated ToString_UserDefinedPrintMembers to cover


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

Copy link
Contributor

Choose a reason for hiding this comment

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

Are we testing a scenario when synthesized PrintMembers used explicitly by user code?

Updated ToString_UserDefinedPrintMembers to cover

It looks like it doesn't call "synthesized PrintMembers".


In reply to: 601913646 [](ancestors = 601913646,601724459)

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 ToString_CallingSynthesizedPrintMembers


In reply to: 601929272 [](ancestors = 601929272,601913646,601724459)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 25, 2021

Done with review pass (commit 1) #Closed

@jcouv
Copy link
Member Author

jcouv commented Mar 25, 2021

            result |= ContainingType.IsSealed ? DeclarationModifiers.None : DeclarationModifiers.Virtual;

Yes. The sealed modifier is applied for all structs in MakeModifiers:

            switch (typeKind)
            {
...
                case TypeKind.Struct:
                case TypeKind.Enum:
                    mods |= DeclarationModifiers.Sealed;
                    break;
...
            }


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


Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPrintMembers.cs:42 in 21dd0d0. [](commit_id = 21dd0d0, deletion_comment = False)

@jcouv jcouv requested a review from AlekseyTs March 26, 2021 00:03
{
if (printableMembers.IsEmpty)
{
// return false;
F.CloseMethod(F.Return(F.Literal(false)));
return;
}
block = ArrayBuilder<BoundStatement>.GetInstance();
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 26, 2021

Choose a reason for hiding this comment

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

block = ArrayBuilder.GetInstance(); [](start = 20, length = 51)

Consider moving block's declaration and initialization after the entire if. #ByDesign

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'll leave this as-is. Seems fine


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

@@ -6781,9 +6879,6 @@ extends A
";
var comp = CreateCompilationWithIL(new[] { source, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.Regular9);
comp.VerifyEmitDiagnostics(
// (2,19): error CS8864: Records may only inherit from object or another record
// public record C : B
Diagnostic(ErrorCode.ERR_BadRecordBase, "B").WithLocation(2, 19),
// (4,29): error CS8871: 'C.PrintMembers(StringBuilder)' does not override expected method from 'B'.
// protected override bool PrintMembers(System.Text.StringBuilder builder) => throw null;
Diagnostic(ErrorCode.ERR_DoesNotOverrideBaseMethod, "PrintMembers").WithArguments("C.PrintMembers(System.Text.StringBuilder)", "B").WithLocation(4, 29)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 26, 2021

Choose a reason for hiding this comment

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

ERR_DoesNotOverrideBaseMethod [](start = 37, length = 29)

I would expect the same error reported for a record like this:

public record C : B;

Do we have a test for a scenario like this? #Closed

@AlekseyTs
Copy link
Contributor

    internal static MethodSymbol? FindValidPrintMembersMethod(TypeSymbol containingType, CSharpCompilation compilation)

Instead we should override MethodChecks where we would verify that OverriddenMethod belongs to the immediate base.

It doesn't look like the override is added. It feels like we need it and are missing a unit-test for the error scenario when overriding skips immediate base.


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


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

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 26, 2021

Done with review pass (commit 3) #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 26, 2021

Done with review pass (commit 3), it looks like there are legitimate test failures. #Closed

@jcouv
Copy link
Member Author

jcouv commented Mar 26, 2021

    internal static MethodSymbol? FindValidPrintMembersMethod(TypeSymbol containingType, CSharpCompilation compilation)

I thought it was covered by check in VerifyOverridesPrintMembersFromBase already. But we only apply that check when the PrintMembers method is in source.


In reply to: 807855586 [](ancestors = 807855586,807125057)


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

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 4)

@RikkiGibson RikkiGibson self-assigned this Mar 29, 2021
@@ -48,21 +48,14 @@ protected override DeclarationModifiers MakeDeclarationModifiers(DeclarationModi
#endif
return result;

MethodSymbol? virtualPrintInBase()
#if DEBUG
bool modifiersAreValid(DeclarationModifiers modifiers)
Copy link
Contributor

@RikkiGibson RikkiGibson Mar 29, 2021

Choose a reason for hiding this comment

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

What happens when the surrounding #if DEBUG directives are removed from here and the call site? #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.

We'd emit non-product code into the shipping product.


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

@jcouv jcouv merged commit 7574293 into dotnet:features/record-structs Mar 29, 2021
@jcouv jcouv deleted the rs-symbol5 branch March 29, 2021 21:43
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.

4 participants