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

Parse field as a contextual keyword #73947

Merged
merged 39 commits into from
Aug 1, 2024

Conversation

cston
Copy link
Member

@cston cston commented Jun 11, 2024

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Jun 11, 2024
/// <summary>
/// If an accessor kind changes, "field" and "value" within the accessor may need to be reinterpreted.
/// </summary>
internal bool IsInValueKeywordContext;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

you might want to mention how this differs from the above. #Resolved

// Check if we have an initializer
else if (this.CurrentToken.Kind == SyntaxKind.EqualsToken)
// PROTOTYPE: SyntaxKind.GetAccessorDeclaration does seem correct for an initializer.
// PROTOTYPE: Confirm that field should be a keyword, and bind to the backing field, in a property initializer: object P { get; } = field;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

this is interesting case for sure. i actually have no idea what should happen here. Property intitializers can't actually reference this or fields. But if we do not have this bind to the field keyword, this could then bind to somethgin higher... and i have no idea if we want that :) #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.

Removed this case, assuming we will decide field is not a keyword (and should not bind to the backing field) in the property initializer.

private readonly struct FieldAndValueKeywordContext : IDisposable
{
private readonly LanguageParser _parser;
private readonly (bool, bool) _isInFieldAndValueKeywordContext;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

maybe _originalXXX to indicate it was the starting value. #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

nit: separate fields just seems simpler :)

@@ -4191,6 +4221,9 @@ private AccessorDeclarationSyntax ParseAccessorDeclaration(bool isEvent)
accessorName = ConvertToKeyword(accessorName);
}

// PROTOTYPE: How should ‘field’ and ‘value’ be interpreted in the attributes, modifiers? Those parts have already been parsed.
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

we already decided this in ldm, but the answer, afair, is: they are not in scope there. #Resolved

@@ -4266,6 +4299,20 @@ private static SyntaxKind GetAccessorKind(SyntaxToken accessorName)
};
}

private static (bool, bool) GetFieldAndValueKeywordContext(AccessorDeclaringKind declaringKind, SyntaxKind accessorKind)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

name these bools. #Resolved

{
return accessorKind switch
{
SyntaxKind.GetAccessorDeclaration => (declaringKind == AccessorDeclaringKind.Property, false),
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

named args for the literals. #Resolved

@@ -5108,7 +5155,7 @@ private static bool CanReuseVariableDeclarator(CSharp.Syntax.VariableDeclaratorS
// declaration. However, in the specific case of variable declarators, Dev10
// specifically treats it as a variable name, even if it could be interpreted as a
// keyword.
var name = this.ParseIdentifierToken();
var name = this.ParseIdentifierToken(reportErrorRatherThanConvertingFieldOrValue: true);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

for somethign as core as ParseIdentifierToken, it feels very strange to need such a specialized arg. esp. since we haven't needed that sort of arg for other contextual tokens? #Resolved

@@ -5439,7 +5486,7 @@ private EnumMemberDeclarationSyntax ParseEnumMemberDeclaration()
equalsValue = _syntaxFactory.EqualsValueClause(
this.EatToken(SyntaxKind.EqualsToken),
this.CurrentToken.Kind is SyntaxKind.CommaToken or SyntaxKind.CloseBraceToken
? this.ParseIdentifierName(ErrorCode.ERR_ConstantExpected)
? this.ParseIdentifierName(null, ErrorCode.ERR_ConstantExpected)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

named arg for literal. #Resolved

SyntaxToken identifierToken = this.EatToken();

if (this.IsInAsync && identifierToken.ContextualKind == SyntaxKind.AwaitKeyword)
{
identifierToken = this.AddError(identifierToken, ErrorCode.ERR_BadAwaitAsIdentifier);
}
else if (isFieldOrValueInKeywordContext) // PROTOTYPE: Are we testing this with "value"?
{
if (reportErrorRatherThanConvertingFieldOrValue.GetValueOrDefault())
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

not gettnig why we need this bool?. seems out of sorts with how we genrally do contexts and parsing of identifiers/contextual-keywords. #Resolved

@@ -8562,7 +8641,7 @@ private bool IsPossibleStatement(bool acceptAccessibilityMods)
return true;

case SyntaxKind.IdentifierToken:
return IsTrueIdentifier();
return IsTrueIdentifier() || IsCurrentTokenFieldOrValueInKeywordContext();
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

this logic is what IsTrueIdentifier is supposed to already check. #Resolved

@@ -11624,11 +11718,11 @@ private ArgumentSyntax ParseArgumentExpression(bool isIndexer)

if (isIndexer && this.CurrentToken.Kind is SyntaxKind.CommaToken or SyntaxKind.CloseBracketToken)
{
expression = this.ParseIdentifierName(ErrorCode.ERR_ValueExpected);
expression = this.ParseIdentifierName(null, ErrorCode.ERR_ValueExpected);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

named parametres. #Resolved

@@ -13722,17 +13834,26 @@ private new struct ResetPoint
internal readonly TerminatorState TerminatorState;
internal readonly bool IsInAsync;
internal readonly bool IsInQuery;
// PROTOTYPE: Why do we need this? We don't need something similar for 'partial'
Copy link
Member

Choose a reason for hiding this comment

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

we need this because partial can look locally to figure out what it means. it is not affected by outer contexts. that's not the case for async/queries/field/value. field/value shuld be treated very closely to async/await. An outer context pushes us into a context where those tokens now become keywords.

fortunately field/value is also simpler because we never change the value as we go deeper into the accessor.

</TypeComment>
<FactoryComment>
<summary>Creates a ValueExpressionSyntax node.</summary>
</FactoryComment>
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

Alternatively, we could use LiteralExpressionSyntax, just like we use that for boolean expressions with true and false keyword. but i'm not sure how compiler would feel about fallout of these being thought of as literals, so i think it's totally ok to not do that. #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.

Will leave as is.

@@ -286,6 +286,9 @@ internal enum NodeFlags : ushort
ContainsStructuredTrivia = 1 << 9,

InheritMask = IsNotMissing | ContainsAnnotations | ContainsAttributes | ContainsDiagnostics | ContainsDirectives | ContainsSkippedText | ContainsStructuredTrivia,

FactoryContextIsInFieldKeywordContext = 1 << 10,
FactoryContextIsInValueKeywordContext = 1 << 11,
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jun 12, 2024

Choose a reason for hiding this comment

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

i would move these up next to the other FactoryContext values. #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

note: this also makes us out of bits again.

}
}

internal bool ParsedInValueKeywordContext
Copy link
Member

Choose a reason for hiding this comment

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

where is the code for this checked. there should be some point that is checking this, and thus ensuring that if we do an incremental parse, where a statement used to be in a property, and it moves to be in a method, that the meaning of field/value go back to being an identifier (and vice versa).

note: this is not a strange scenario to be in. people convert properties to methods and back. So incremental parsing def needs to be good here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is this property necessary? To switch the context between a property accessor and a method would require changing the containing member definition kind which I suspect invalidates the body of property, accessor, or method.

@CyrusNajmabadi
Copy link
Member

done with initial pass.

@cston cston force-pushed the field-value-keywords branch from 393e476 to 538db7e Compare June 12, 2024 23:57
@cston cston requested review from a team as code owners July 29, 2024 19:48
Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

I've looked through most of the tests, but not FieldKeywordTests.cs yet. Will get to that later.

@cston cston changed the base branch from main to features/field-keyword July 30, 2024 17:09
@cston cston requested review from a team and RikkiGibson July 30, 2024 20:03
@RikkiGibson RikkiGibson self-assigned this Jul 31, 2024
Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Looks good but haven't gotten through tests yet.

{
string name = identifier.Text;
var requiredVersion = MessageID.IDS_FeatureFieldKeyword.RequiredVersion();
diagnostics.Add(ErrorCode.INF_IdentifierConflictWithContextualKeyword, syntax, name, requiredVersion.ToDisplayString());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an open issue that we are tracking resolving in a future PR?

{
accessorsHaveImplementation = true;
}

usesFieldKeyword = usesFieldKeyword || containsFieldKeyword(accessor);
Copy link
Contributor

@RikkiGibson RikkiGibson Jul 31, 2024

Choose a reason for hiding this comment

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

The other usage of this local function passes the body as an argument, should we do the same 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 reason for passing accessor rather than body is to include attributes from the accessor.

isReadOnly: (hasGetAccessor && !hasSetAccessor) || isInitOnly,
// Synthesized backing field for 'field' should not be marked 'initonly'
// since the field might be modified in the get accessor.
isReadOnly: !usesFieldKeyword && ((hasGetAccessor && !hasSetAccessor) || isInitOnly),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow this logic.. what if we are in a readonly property or a property on a readonly struct? But it seems like this would give a non-readonly field in that case.

I would expect to be able to use the field keyword in such cases, but it would need to refer to a readonly field. You would be able to write to the field in an init accessor, or a property initializer, or by assigning to the property in the constructor.

Copy link
Member Author

@cston cston Aug 1, 2024

Choose a reason for hiding this comment

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

Should the synthesized field of a readonly property be readonly, or should the field be mutable, regardless? Should this be an open question for language design?

class C
{
    object P
    {
        get
        {
            field ??= ""; // ok?
            return field;
        }
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

The field should be writable except when the accessor is readonly in the "readonly modifier" sense. i.e. the get-only case you have shown here should be writable.

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 a PROTOTYPE comment for now.

@@ -230,21 +230,11 @@ protected void SetFactoryContext(SyntaxFactoryContext context)
{
SetFlags(NodeFlags.FactoryContextIsInQuery);
}
}

internal static NodeFlags SetFactoryContext(NodeFlags flags, SyntaxFactoryContext context)
Copy link
Contributor

@RikkiGibson RikkiGibson Jul 31, 2024

Choose a reason for hiding this comment

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

Was this just dead code? #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.

Yes.


static bool containsFieldKeyword(SyntaxNode syntax)
{
return syntax.DescendantTokens().Any(static t => t.Kind() == SyntaxKind.FieldKeyword);
Copy link
Contributor

@RikkiGibson RikkiGibson Jul 31, 2024

Choose a reason for hiding this comment

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

Does this traversal avoid creating the red tree? #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.

No, but haven't we already created the red tree for the property simply by referencing the root SyntaxNode?

Copy link
Member

Choose a reason for hiding this comment

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

no. walking down the red tree causes the red allocations. ideally this walk is done on the green tree to avoid actual allocs.

}
}
""";
var verifier = CompileAndVerify(source, expectedOutput: """
Copy link
Contributor

@RikkiGibson RikkiGibson Jul 31, 2024

Choose a reason for hiding this comment

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

It will be good to also verify contents of compilation.GetMember("C").GetMembers(). I would expect that because other backing fields are present there that this would be too.

No need to do this across all the tests, just one or two places would probably be fine. #Resolved

comp.VerifyEmitDiagnostics(
// (5,22): info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead.
// get { return field; }
Diagnostic(ErrorCode.INF_IdentifierConflictWithContextualKeyword, "field").WithArguments("field", "preview").WithLocation(5, 22),
Copy link
Contributor

Choose a reason for hiding this comment

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

I am confused by these INF diagnostics. If we are already in LangVersion preview I wouldn't expect them to be reported. These will show up in the IDE error list if someone is just using the feature normally and intentionally. Is this something we're planning on fixing later?

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 believe we want to report this info-level diagnostic always, even with language version preview, to allow users to discover where there may be a breaking change. But perhaps we should only report the diagnostic when field would otherwise bind to a member other than the synthesized field. cc @jaredpar

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 reporting it in all LangVersions made sense when we were just shipping the diagnostic in order to grasp the severity of the potential break. I don't think it makes sense to report in the case that the feature is being used normally and there is no symbol field in scope.

I have a lot more thoughts here, but basically, I think there is a need to design this experience more deeply/holistically, thinking about what should happen in various combinations of LangVersion, and the kind of declaration which is named field, and the way the field identifier/keyword is being used.

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 a PROTOTYPE comment to Binder.ReportFieldContextualKeywordConflict.

}

[Fact]
public void FieldInInitializer_03()
Copy link
Contributor

@RikkiGibson RikkiGibson Jul 31, 2024

Choose a reason for hiding this comment

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

tests for case when accessor uses field and property has an initializer? #Resolved

}

[Fact]
public void Attribute_02()
Copy link
Contributor

Choose a reason for hiding this comment

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

also tests for when property or accessor attribute uses [field: ...]? I would expect these to behave the same as they do on auto-properties today.

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've added an item to the test plan to verify attributes can be attached to the synthesized field using [field: ...], and I'll handle that in a subsequent PR.

N(SyntaxKind.EqualsValueClause);
{
N(SyntaxKind.EqualsToken);
IdentifierNameOrFieldExpression(languageVersion, escapeIdentifier: false, expectedParsedAsToken);
Copy link
Contributor

@RikkiGibson RikkiGibson Aug 1, 2024

Choose a reason for hiding this comment

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

It feels like it would be more straightforward to just use the generated baseline when we expect it to be the same in all the cases of the theory. #Resolved

@@ -561,6 +582,7 @@ class C
[CombinatorialValues(LanguageVersion.CSharp12, LanguageVersion.Preview)] LanguageVersion languageVersion)
{
string source = """
#pragma warning disable 168
Copy link
Contributor

@RikkiGibson RikkiGibson Aug 1, 2024

Choose a reason for hiding this comment

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

Please include something like the message of the diagnostic which is being disabled or its name in ErrorCode to hint at what is going on here. #Resolved

@@ -3876,7 +3879,32 @@ private SyntaxToken TryEatCheckedOrHandleUnchecked(ref SyntaxToken operatorKeywo
semicolon);
}

private AccessorListSyntax ParseAccessorList(bool isEvent)
private readonly struct FieldKeywordContext : IDisposable
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Aug 1, 2024

Choose a reason for hiding this comment

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

could make a ref-struct, so this doesn't get put on the heap accidentally. #Resolved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Feature - Field Keyword untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants