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

Support 'scoped' modifier for parameters and locals #61389

Merged
merged 33 commits into from
Jun 3, 2022

Conversation

cston
Copy link
Member

@cston cston commented May 18, 2022

Proposal: low-level-struct-improvements.md
Test plan: #59194

Parse and bind scoped modifier for parameters and locals; report binding error for scoped when compiling with -langversion:10 or earlier; emit modifier to metadata with LifetimeAnnotationAttribute. The scoped modifier can be applied to ref or to ref struct values.

The PR does not include:

  • Changes to escape rules for scoped variables (see spec)
  • conversions involving scoped
  • scoped considered in overrides and interface implementation

@cston cston marked this pull request as ready for review May 18, 2022 21:16
@cston cston requested review from a team as code owners May 18, 2022 21:16
@RikkiGibson RikkiGibson self-assigned this May 18, 2022
@cston cston force-pushed the scoped-keyword branch from 4ef51cc to c6f7e22 Compare May 19, 2022 03:49
@cston cston force-pushed the scoped-keyword branch from c6f7e22 to 405ce6a Compare May 19, 2022 04:56
@@ -412,6 +412,9 @@
<summary>Gets the optional "readonly" keyword.</summary>
</PropertyComment>
</Field>
<Field Name="ScopedKeyword" Type="SyntaxToken" Optional="true">
<Kind Name="ScopedKeyword"/>
</Field>
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi May 19, 2022

Choose a reason for hiding this comment

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

part of the type syntax, and not part of the parameter syntax? #Resolved

Copy link
Contributor

@RikkiGibson RikkiGibson May 19, 2022

Choose a reason for hiding this comment

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

The feature supports both scoped ref Span<int> param and ref scoped Span<int> param and they mean different things. Haven't looked closely at this PR yet but it doesn't surprise me that Scoped is part of the ParameterSyntax directly.

It doesn't surprise me that scoped goes into the Modifiers on a ParameterSyntax. I mentioned to Chuck offline that it would be best if our parse for readonly ref readonly Span<int> local; has a similar parse as scoped ref scoped Span<int> local;. I think this makes them match. Although this makes me wonder if this ReadOnlyKeyword should "grow up" into a generalized trailing Modifiers list.

@@ -1163,6 +1165,8 @@ internal static DeclarationModifiers GetModifier(SyntaxKind kind, SyntaxKind con
return DeclarationModifiers.Partial;
case SyntaxKind.AsyncKeyword:
return DeclarationModifiers.Async;
case SyntaxKind.ScopedKeyword:
return AllowScopedModifier() ? DeclarationModifiers.Scoped : DeclarationModifiers.None;
Copy link
Contributor

@Neme12 Neme12 May 20, 2022

Choose a reason for hiding this comment

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

Wouldn't it be better if the keyword was parsed regardless of language version, but in a lower version it would show an error that the feature isn't available? That would make for a better experience when users try this feature but didn't upgrade their lang version, and would also allow the language version code fix to work. #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. I think the 'file' or 'required' parsing changes should end up looking similar to what we do in here. https://github.com/dotnet/roslyn/pull/58431/files#diff-a655d393d9799eabace5e5109383232749be8052f1fd825fc52b9f170908e838

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. I'd initially expected breaking changes parsing scoped as a modifier, so I'd made the modifier conditional on language version, but the syntax may be unambiguous after all. I've updated the parser to allow scoped as a modifier unconditionally, and moved the language version check to binding.

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.

Haven't looked at DeclarationScopeParsingTests yet. Looks like we're on a good track but did have some feedback and questions.

src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs Outdated Show resolved Hide resolved
@@ -1163,6 +1165,8 @@ internal static DeclarationModifiers GetModifier(SyntaxKind kind, SyntaxKind con
return DeclarationModifiers.Partial;
case SyntaxKind.AsyncKeyword:
return DeclarationModifiers.Async;
case SyntaxKind.ScopedKeyword:
return AllowScopedModifier() ? DeclarationModifiers.Scoped : DeclarationModifiers.None;
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. I think the 'file' or 'required' parsing changes should end up looking similar to what we do in here. https://github.com/dotnet/roslyn/pull/58431/files#diff-a655d393d9799eabace5e5109383232749be8052f1fd825fc52b9f170908e838

src/Compilers/CSharp/Portable/Parser/LanguageParser.cs Outdated Show resolved Hide resolved
static unsafe void Main()
{
delegate*<scoped R, void> f1 = &F1;
delegate*<ref scoped int, scoped ref int, void> f2 = &F2;
Copy link
Contributor

@RikkiGibson RikkiGibson May 21, 2022

Choose a reason for hiding this comment

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

let's make sure we test scenarios where 'scoped' doesn't match as well. #Pending

}

// PROTOTYPE: Report error for implicit conversion between delegate types that differ by 'scoped',
// and between function pointer types and methods that differ by 'scoped'.
Copy link
Contributor

@RikkiGibson RikkiGibson May 21, 2022

Choose a reason for hiding this comment

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

seems like all conversions should be disallowed when there are 'scoped' differences. It actually makes me wonder if a 'modreq' is needed to prevent downlevel compilers from attempting to perform such conversions. #Pending

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, and I'll address this in a subsequent PR.

src/Compilers/Core/Portable/MetadataReader/PEModule.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Symbols/DeclarationScope.cs Outdated Show resolved Hide resolved
@@ -1283,7 +1296,7 @@ bool isStructOrRecordKeyword(SyntaxToken token)

private bool ShouldAsyncBeTreatedAsModifier(bool parsingStatementNotDeclaration)
{
Debug.Assert(this.CurrentToken.ContextualKind == SyntaxKind.AsyncKeyword);
Debug.Assert(this.CurrentToken.Kind == SyntaxKind.IdentifierToken && GetModifier(this.CurrentToken) != DeclarationModifiers.None);
Copy link
Member Author

@cston cston May 23, 2022

Choose a reason for hiding this comment

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

Debug.Assert

The updated assert is from #60823 where this method is also renamed. #Resolved

@cston cston force-pushed the scoped-keyword branch from e90cda4 to 0d48f5a Compare May 24, 2022 23:30
@cston cston requested a review from a team as a code owner May 26, 2022 04:07
@cston cston force-pushed the scoped-keyword branch from d425261 to 290be82 Compare May 26, 2022 04:14
@@ -68,6 +68,10 @@ public override TResult Accept<TResult>(SymbolVisitor<TResult> visitor)

public ImmutableArray<CustomModifier> CustomModifiers => ImmutableArray.Create<CustomModifier>();

public bool IsRefScoped => false;

public bool IsValueScoped => false;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi May 26, 2022

Choose a reason for hiding this comment

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

interesting name. curious what the intuition is on 'value scoped' #Resolved

Copy link
Member Author

@cston cston May 26, 2022

Choose a reason for hiding this comment

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

The name is intended to indicate that the members of the ref struct, that is, the value of the ref struct, does not escape the current method.

(false, false) => DeclarationScope.Unscoped,
(false, true) => type.IsRefLikeType ? DeclarationScope.ValueScoped : null,
(true, false) => refKind != RefKind.None ? DeclarationScope.RefScoped : null,
(true, true) => null,
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

(true, true) => null,

The spec says: "The declaration scoped ref scoped Span<int> is allowed but is redundant with ref scoped Span<int>." Why not handle this combination this way? #Closed

@@ -19,7 +18,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols
/// <summary>
/// A source parameter, potentially with a default value, attributes, etc.
/// </summary>
internal class SourceComplexParameterSymbol : SourceParameterSymbol, IAttributeTargetSymbol
internal abstract class SourceComplexParameterSymbol : SourceParameterSymbol, IAttributeTargetSymbol
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

abstract

I do not see any advantage in making this type abstract especially that now we are forced to create a bigger type where SourceComplexParameterSymbol would work. If the goal is to make RefCustomModifiers property abstract, then we can rename this type to SourceComplexParameterSymbolBase and add sealed derived type called SourceComplexParameterSymbol with old implementation of the property. "Restoring" logic at creation sites. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jun 2, 2022

        RefKind refKind,

It feels like we should add DeclarationScope to this class since we handle RefKind here as well. This way we will be able to keep the original logic above and create SourceSimpleParameterSymbol instances in more scenarios.


In reply to: 1144323560


In reply to: 1144323560


In reply to: 1144323560


In reply to: 1144323560


Refers to: src/Compilers/CSharp/Portable/Symbols/Source/SourceParameterSymbol.cs:90 in 88f0053. [](commit_id = 88f0053, deletion_comment = False)


if (!isParams &&
!isExtensionMethodThis &&
scope == DeclarationScope.Unscoped &&
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

scope == DeclarationScope.Unscoped &&

I do not think addition of this condition is justified. We should still be able to create SourceSimpleParameterSymbol in this case. SourceComplexParameterSymbol doesn't add any interesting behavior around DeclarationScope that SourceSimpleParameterSymbol cannot handle, other than the storage, of course. As I suggested below, we should simply move the storage to this class and restore the name for SourceComplexParameterSymbolWithCustomModifiersPrecedingByRef class. #Closed

@@ -173,5 +173,7 @@ internal override bool IsMetadataOut
internal override ImmutableArray<int> InterpolatedStringHandlerArgumentIndexes => ImmutableArray<int>.Empty;

internal override bool HasInterpolatedStringHandlerArgumentError => false;

internal override DeclarationScope Scope => DeclarationScope.Unscoped;
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

DeclarationScope.Unscoped

Is this the right value for this parameter? The spec says the following: "scoped ref T is the default type of this." #Closed

@@ -210,12 +237,13 @@ internal override string GetDebuggerDisplay()
SyntaxToken identifierToken,
LocalDeclarationKind declarationKind,
EqualsValueClauseSyntax initializer = null,
Binder initializerBinderOpt = null)
Binder initializerBinderOpt = null,
bool scopedModifier = false)
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

bool scopedModifier = false

Were all existing call sites examined in case an explicit value should be passed? #Closed

@@ -313,7 +314,7 @@ internal void BuildLocalFunctions(StatementSyntax statement, ref ArrayBuilder<Lo
}
}

protected SourceLocalSymbol MakeLocal(VariableDeclarationSyntax declaration, VariableDeclaratorSyntax declarator, LocalDeclarationKind kind, Binder initializerBinderOpt = null)
protected SourceLocalSymbol MakeLocal(VariableDeclarationSyntax declaration, VariableDeclaratorSyntax declarator, LocalDeclarationKind kind, Binder initializerBinderOpt = null, bool scopedModifier = false)
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

, bool scopedModifier = false

Were all call sites examined incase an explicit value should be passed? #Closed

(true, true) => null,
};
}

public override ImmutableArray<CSharpAttributeData> GetAttributes()
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

GetAttributes

Should LifetimeAnnotationAttribute be filtered out by this method? #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 22)

@@ -215,7 +215,7 @@ public static bool IsSourceParameterWithEnumeratorCancellationAttribute(this Par
{
switch (parameter)
{
case SourceComplexParameterSymbol source:
case SourceComplexParameterSymbolBase source:
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 2, 2022

Choose a reason for hiding this comment

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

SourceComplexParameterSymbolBase

Is this change related to the feature? #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 23)

@@ -980,12 +1016,12 @@ public override ImmutableArray<CSharpAttributeData> GetAttributes()
}

bool filterIsReadOnlyAttribute = this.RefKind == RefKind.In;
bool filterLifetimeAnnotationAttribute = Scope != DeclarationScope.Unscoped;
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 3, 2022

Choose a reason for hiding this comment

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

Scope != DeclarationScope.Unscoped

I think the attribute should be filtered out even in this condition. It looks like it can be used to encode this value as well. #Closed

{
Assert.Equal(expectedRefKind, parameter.RefKind);
// https://github.com/dotnet/roslyn/issues/61647: Use public API.
//Assert.Equal(expectedScope == DeclarationScope.RefScoped, parameter.IsRefScoped);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 3, 2022

Choose a reason for hiding this comment

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

//Assert.Equal(expectedScope == DeclarationScope.RefScoped, parameter.IsRefScoped);

Is it worth testing through internal API for now? #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.

The only caller is the preceding method which tests the internal API ParameterSymbol.Scope.

{
Assert.Equal(expectedRefKind, local.RefKind);
// https://github.com/dotnet/roslyn/issues/61647: Use public API.
//Assert.Equal(expectedScope == DeclarationScope.RefScoped, local.IsRefScoped);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 3, 2022

Choose a reason for hiding this comment

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

//Assert.Equal(expectedScope == DeclarationScope.RefScoped, local.IsRefScoped);

Is it worth testing through internal API for now? #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.

The only caller is the preceding method which tests the internal API LocalSymbol.Scope.

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 33), assuming CI is passing.

@cston cston merged commit 75ac25c into dotnet:features/ref-fields Jun 3, 2022
@cston cston deleted the scoped-keyword branch June 3, 2022 18:50
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.

5 participants