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

Add support for init-only members #43275

Merged
merged 22 commits into from
Apr 30, 2020
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Apr 10, 2020

Part of records feature:

The first commit only has a refactoring (splitting and flattening very long/nested method).

A couple of design points that I expect might be need discussion:

  1. the syntax model adds a new kind of accessor kind. I would have preferred a set accessor just with a different keyword, but I don't think we can do that.

Note: the first commit is a refactoring to one file.

@jcouv jcouv added this to the Compiler.Net5 milestone Apr 10, 2020
@jcouv jcouv self-assigned this Apr 10, 2020
@jcouv jcouv marked this pull request as ready for review April 10, 2020 23:15
@jcouv jcouv requested a review from a team as a code owner April 10, 2020 23:15
@jcouv jcouv force-pushed the init-only branch 3 times, most recently from 0475dde to c69d069 Compare April 13, 2020 16:46
@jcouv jcouv marked this pull request as draft April 13, 2020 19:03
@jcouv
Copy link
Member Author

jcouv commented Apr 13, 2020

Will update this PR based on today's LDM decisions. #Closed

@RikkiGibson RikkiGibson removed the request for review from a team April 13, 2020 19:06
@jcouv jcouv force-pushed the init-only branch 2 times, most recently from e9aea11 to 5cc692a Compare April 16, 2020 05:04
@jcouv jcouv closed this Apr 16, 2020
@jcouv jcouv reopened this Apr 16, 2020
@jcouv jcouv force-pushed the init-only branch 2 times, most recently from 230ad67 to f83c7a5 Compare April 17, 2020 16:46
@jcouv jcouv marked this pull request as ready for review April 17, 2020 17:24
@jcouv
Copy link
Member Author

jcouv commented Apr 17, 2020

@dotnet/roslyn-compiler This PR is open for review.

I've updated this PR with latest decisions from LDM and API design meeting:

  • no init-only fields
  • init-only properties can update readonly fields under narrow conditions
  • metadata uses a modreq on return type of accessor, but no attribute #Closed

@@ -717,24 +717,32 @@ private TypeSymbol GetTypeOfTypeDef(TypeDefinitionHandle typeDef, out bool isNoP
{
var isAllowed = false;

switch (allowedRequiredModifierType)
if ((allowedRequiredModifierType & AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute) != 0 &&
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

if ((allowedRequiredModifierType & AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute) != 0 && [](start = 20, length = 114)

It looks like these changes are going to conflict with changes made in #43355. Consider coordinating with @333fred to make changes in both PRs in a manner that would minimize the churn during merge #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.

Fred, this change allows for looking for more than one modreq at a certain position. Would you mind taking this change into your branch to minimize merge conflicts?


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

@@ -1178,17 +1186,18 @@ private void DecodeParameterOrThrow(ref BlobReader signatureReader, /*out*/ ref
{
info.CustomModifiers = DecodeModifiersOrThrow(
ref signatureReader,
AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute,
AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute |
AllowedRequiredModifierType.System_Runtime_CompilerServices_IsInitOnly,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

AllowedRequiredModifierType.System_Runtime_CompilerServices_IsInitOnly [](start = 20, length = 70)

Allowing this on any parameter doesn't feel right. #Closed

@@ -1592,6 +1595,9 @@ If such a class is used as a base class and if the deriving class defines a dest
<data name="ERR_ExplicitPropertyAddingAccessor" xml:space="preserve">
<value>'{0}' adds an accessor not found in interface member '{1}'</value>
</data>
<data name="ERR_ExplicitPropertyMismatchInitOnly" xml:space="preserve">
<value>Accessors '{0}' and '{1}' don't match by init-only</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

don't match by init-only [](start = 37, length = 24)

Consider rewording for clarity. #Closed

@@ -2891,7 +2897,7 @@ A catch() block after a catch (System.Exception e) block can catch non-CLS excep
<value>Members of readonly field '{0}' cannot be used as a ref or out value (except in a constructor)</value>
</data>
<data name="ERR_AssgReadonly" xml:space="preserve">
<value>A readonly field cannot be assigned to (except in a constructor of the class in which the field is defined or a variable initializer))</value>
<value>A readonly field cannot be assigned to (except in a constructor or init-only member of the class in which the field is defined or a variable initializer))</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

init-only member [](start = 78, length = 16)

Is there a concept of an "init-only member"? #Closed

@@ -6067,4 +6079,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="WRN_GeneratorFailedDuringInitialization_Title" xml:space="preserve">
<value>Generator failed to initialize.</value>
</data>
<data name="ERR_AssignmentInitOnly" xml:space="preserve">
<value>Init-only member '{0}' can only be assigned from a constructor, object initialization or 'with' expression of that type.</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

from [](start = 55, length = 4)

"in" vs. "from"? #Closed

@@ -200,6 +200,7 @@ public enum SyntaxKind : ushort
WhenKeyword = 8437,
DataKeyword = 8438,
WithKeyword = 8439,
InitKeyword = 8440,
/// when adding a contextual keyword following functions must be adapted:
/// <see cref="SyntaxFacts.GetContextualKeywordKinds"/>
/// <see cref="SyntaxFacts.IsContextualKeyword(SyntaxKind)"/>
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 19, 2020

Choose a reason for hiding this comment

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

/// [](start = 8, length = 61)

Consider also adding GetContextualKeywordKind to the list #Closed

@AlekseyTs
Copy link
Contributor

        comp.VerifyDiagnostics();

I'm not sure what you're suggesting to change. I already have a PROTOTYPE for this problem on the line below (decoding is not restrictive enough)

I am suggesting to add a prototype comment, explicitly stating that we expect an error on the C.M(); line.


In reply to: 620859393 [](ancestors = 620859393,620753699)


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:2862 in 7cce780. [](commit_id = 7cce780, deletion_comment = False)

@jcouv
Copy link
Member Author

jcouv commented Apr 28, 2020

        // PROTOTYPE(init-only): can we make this more restrictive (ie. disallow aside from the return value of an instance setter)?

FWIW, the reason why it's a question in my mind is because the decoding of the accessors is done before we decide that they are accessors.


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


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 1d77553. [](commit_id = 1d77553, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

        // PROTOTYPE(init-only): can we make this more restrictive (ie. disallow aside from the return value of an instance setter)?

FWIW, the reason why it's a question in my mind is because the decoding of the accessors is done before we decide that they are accessors.

I can understand that you have a question about how to do that, i.e. how to implement. However, there is no question what the behavior should be, i.e. what the expectations are. And the expectations are that we should be more restrictive. No question.


In reply to: 620863035 [](ancestors = 620863035,620760072)


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 7cce780. [](commit_id = 7cce780, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

        // PROTOTYPE(init-only): can we make this more restrictive (ie. disallow aside from the return value of an instance setter)?

And we also certainly can do that. Again, no question.


In reply to: 620865345 [](ancestors = 620865345,620863035,620760072)


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 7cce780. [](commit_id = 7cce780, deletion_comment = False)

@jcouv jcouv requested a review from AlekseyTs April 28, 2020 22:49

if (typeCode == SignatureTypeCode.ByReference)
{
info.IsByRef = true;

// PROTOTYPE(init-only): we're incorrectly letting modreq(IsExternalInit) into the RefCustomModifiers
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

// PROTOTYPE(init-only): we're incorrectly letting modreq(IsExternalInit) into the RefCustomModifiers [](start = 16, length = 101)

It looks like this comment is obsolete, the if at the end of this block takes care of the issue. BTW, consider moving that if 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.

I'll remove the comment. Thanks
It's a nit, but the reason the if/throw is at the end of this block is that the decoded modreq will get stored rather than lost.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

the reason the if/throw is at the end of this block is that the decoded modreq will get stored rather than lost.

This isn't really useful. I prefer us to follow consistent handling of unsupported modereqs within this type - throw as soon as we recognize the fact


In reply to: 417569537 [](ancestors = 417569537,417491981)

Copy link
Contributor

Choose a reason for hiding this comment

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

It might make sense to do that after the assignmentinfo.RefCustomModifiers = info.CustomModifiers, but it is really not worth trying to decode the next set of modifiers because we aren't even going to try decoding the type of the parameter.


In reply to: 417585593 [](ancestors = 417585593,417569537,417491981)

return false;
}

if ((!method.IsStatic && method.IsConstructor()) || method.IsInitOnly)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

(!method.IsStatic && method.IsConstructor()) [](start = 20, length = 44)

Why so complicated? Check out how IsConstructor is implemented. method.MethodKind == MethodKind.Constructor? #Closed

@@ -160,13 +161,21 @@ public override void VisitProperty(IPropertySymbol symbol)
AddPunctuation(SyntaxKind.OpenBraceToken);

AddAccessor(symbol, symbol.GetMethod, SyntaxKind.GetKeyword);
AddAccessor(symbol, symbol.SetMethod, SyntaxKind.SetKeyword);
// PROTOTYPE(init-only): adjust SymbolDisplayVisitor once we have a public IsInitOnly API
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

// PROTOTYPE(init-only): adjust SymbolDisplayVisitor once we have a public IsInitOnly API [](start = 16, length = 89)

Consider moving the comment into the IsInitOnly helper below, this is where the change should happen. #Closed

@@ -190,7 +199,7 @@ private void AddPropertyNameAndParameters(IPropertySymbol symbol)
this.builder.Add(CreatePart(SymbolDisplayPartKind.PropertyName, symbol, symbol.Name));
}

if (this.format.MemberOptions.IncludesOption(SymbolDisplayMemberOptions.IncludeParameters) && symbol.Parameters.Any())
if (this.format.MemberOptions.IncludesOption(SymbolDisplayMemberOptions.IncludeParameters) && symbol.Parameters.Any<IParameterSymbol>())
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

[](start = 127, length = 18)

This looks like an unintentional change. #Closed


AddSpace();
AddPunctuation(SyntaxKind.CloseBraceToken);
}
}

private static bool IsInitOnly(IPropertySymbol symbol)
{
return (symbol.SetMethod as Symbols.PublicModel.MethodSymbol)?.UnderlyingMethodSymbol.IsInitOnly == true;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

symbol.SetMethod [](start = 20, length = 16)

Consider extracting a helper that takes IMethodSymbol, so that we can use it below. #Closed

@@ -410,7 +419,8 @@ public override void VisitMethod(IMethodSymbol symbol)
}
AddPropertyNameAndParameters(associatedProperty);
AddPunctuation(SyntaxKind.DotToken);
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword : SyntaxKind.SetKeyword);
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword :
IsInitOnly(associatedProperty) ? SyntaxKind.InitKeyword : SyntaxKind.SetKeyword);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

IsInitOnly(associatedProperty) [](start = 28, length = 30)

It feels like we should have a helper that takes IMethodSymbol. We have a method at hand, why go back to the property? #Closed

TypeWithAnnotations returnType,
ImmutableArray<CustomModifier> refCustomModifiers,
ImmutableArray<MethodSymbol> explicitInterfaceImplementations)
{
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers));
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 29, 2020

Choose a reason for hiding this comment

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

Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers)); [](start = 12, length = 102)

Not quite the assert I suggested to add, but, if it holds, let's keep it this way. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 29, 2020

Done with review pass (iteration 15) #Closed

@jcouv jcouv requested a review from AlekseyTs April 29, 2020 20:37
AlekseyTs
AlekseyTs previously approved these changes Apr 29, 2020
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 (iteration 16)


internal static bool HasIsExternalInitModifier(this ImmutableArray<CustomModifier> modifiers)
{
if (modifiers.IsDefault)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 30, 2020

Choose a reason for hiding this comment

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

if (modifiers.IsDefault) [](start = 12, length = 24)

Why do we need this check here, but not in the helper above. What is different? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

This feels wrong, It feels unexpected for a symbol to expose modifiers as ImmutableArray<CustomModifier>.Default. I think we should understand the root cause.


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

@@ -265,7 +265,7 @@ internal TypeWithAnnotations MergeEquivalentTypes(TypeWithAnnotations other, Var
/// <summary>
/// The list of custom modifiers, if any, associated with the <see cref="Type"/>.
/// </summary>
public ImmutableArray<CustomModifier> CustomModifiers => _extensions.CustomModifiers;
public ImmutableArray<CustomModifier> CustomModifiers => _extensions?.CustomModifiers ?? default;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 30, 2020

Choose a reason for hiding this comment

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

_extensions?.CustomModifiers ?? default; [](start = 65, length = 40)

Has something changed? Why do we need to add this logic in this PR? #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.

CI flagged an issue (null ref crash)

Copy link
Member Author

Choose a reason for hiding this comment

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

in this assertion in SignatureOnlyMethodSymbol:
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers));

Copy link
Member Author

Choose a reason for hiding this comment

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

@AlekseyTs Two more tests failed in CI, desktop only. I'm pushing fixes now. Thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

in this assertion in SignatureOnlyMethodSymbol:
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers));

Why do we have a a type without _extensions? Is it default value of the structure? Why SignatureOnlyMethodSymbol uses it? I think we should understand the root cause, other places could be affected as well.


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

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 hit this problem in PerformCrefOverloadResolution which does pass a default type.
The reason I'm fixing the issue in TypeWithAnnotations is that it mostly already handled when _extensions is null. Do you think it would be better to return an empty array instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

We hit this problem in PerformCrefOverloadResolution which does pass a default type.

It looks like the comparer that is used there is supposed to ignore return type and also is supposed to ignore custom modifiers. So. I think we need to understand why exactly do we get here for the default instance, what are we trying to do and why.

The reason I'm fixing the issue in TypeWithAnnotations is that it mostly already handled when _extensions is null. Do you think it would be better to return an empty array instead?

I am not sure if we even should touch this property. Default instances of this structure are not safe by design, I see other properties that will throw or return something that consumers would not expect normally. We simply shouldn't be operating on them.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

If the new assert is what causing the problem, then I think we should simply adjust it to deal with default instance and not access the property.


In reply to: 418044532 [](ancestors = 418044532,418013638)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 30, 2020

@jcouv Please do not merge without closing the loop for two active threads related to recent changes. #Closed

@AlekseyTs AlekseyTs dismissed their stale review April 30, 2020 13:18

A discussion is ongoing around more recent changes

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 (iteration 22)

@jcouv jcouv merged commit f423b85 into dotnet:features/records Apr 30, 2020
@jcouv jcouv deleted the init-only branch April 30, 2020 22:41
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