Skip to content

Conversation

@CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Dec 10, 2025

We now allow a misplacced 'scoped' keyword to be treated as a normal modifier, and move teh reporting of issues to the binding phase. Today we get entirely thrown off and enter very bad error recovery that often gets thrown entirely off, creating tons of cascading errors.

Note: the net effect of this is much fewer errors, and much better error recovery. The added length in this PR is the addition of new tests, or updating existing tests to report binding errors to demonstrate that errors are still produced semantically where they would have previously been syntactic.

@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner December 10, 2025 13:26
@CyrusNajmabadi CyrusNajmabadi marked this pull request as draft December 10, 2025 13:27
return true;

using var beforeScopedResetPoint = this.GetDisposableResetPoint(resetOnDispose: false);
using var beforeScopedResetPoint = this.GetDisposableResetPoint(resetOnDispose: true);
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 need to for dual reset points. and we can always trivially clean up. we just let the caller then eat the token if this function says it is a keyword.

Diagnostic(ErrorCode.ERR_IdentifierExpected, "readonly").WithLocation(3, 37),
// (3,37): error CS1003: Syntax error, ',' expected
// public static void M(ref scoped readonly int p) => throw null;
Diagnostic(ErrorCode.ERR_SyntaxError, "readonly").WithArguments(",").WithLocation(3, 37),
Copy link
Member Author

Choose a reason for hiding this comment

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

lots of the PR will just be syntax errors going away, and better errors about misplaced 'scopes' modifiers.

Diagnostic(ErrorCode.ERR_RefReadOnlyWrongOrdering, "readonly").WithLocation(3, 26),
// (3,35): error CS0246: The type or namespace name 'scoped' could not be found (are you missing a using directive or an assembly reference?)
// public static void M(readonly scoped ref int p) => throw null;
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "scoped").WithArguments("scoped").WithLocation(3, 35),
Copy link
Member Author

Choose a reason for hiding this comment

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

or removing errors where we considered it a type name, but now realie it should be treated as a modifier.

Diagnostic(ErrorCode.ERR_ScopedAfterInOutRefReadonly, "scoped").WithLocation(1, 29),
// (1,47): error CS8936: Feature 'ref fields' is not available in C# 10.0. Please use language version 11.0 or greater.
// void F(ref scoped int b, in scoped int c, out scoped int d) { }
Diagnostic(ErrorCode.ERR_ScopedAfterInOutRefReadonly, "scoped").WithLocation(1, 47)]);
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 difference between these two is just the errors about a feature not being available. is tehre a helper to filter those out appropriately to make the test cleaner?

// (1,54): error CS1003: Syntax error, ',' expected
// void F(ref scoped int b, in scoped int c, out scoped int d) { }
Diagnostic(ErrorCode.ERR_SyntaxError, "int").WithArguments(",").WithLocation(1, 54)
);
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 some tests all syntax diagnostics went away. as such, i added semantic diagnostics to show we still report problems with the scoped modifier.

M(SyntaxKind.CommaToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.ScopedKeyword);
Copy link
Member Author

Choose a reason for hiding this comment

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

these tests demonstrate better understanding, with 'scoped' properly being treated as a modifier.

Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(1, 26)]);

N(SyntaxKind.ParenthesizedExpression);
N(SyntaxKind.ParenthesizedLambdaExpression);
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 didn't even realize this was a lambda before...

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review December 10, 2025 14:43
@CyrusNajmabadi
Copy link
Member Author

@dotnet/roslyn-compiler this is ready for review.


private bool IsPossibleScopedKeyword(bool isFunctionPointerParameter)
{
using var _ = this.GetDisposableResetPoint(resetOnDispose: true);
Copy link
Member Author

Choose a reason for hiding this comment

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

for those keeping count, this made for a triple nested reset point. We now just have one reset point.

// trivial case. scoped ref/out/in/this is definitely the scoped keyword. Note: the only actual legal
// cases are `scoped ref`, `scoped out`, and `scoped in`. But we detect and allow `scoped this`, `scoped
// params` and `scoped readonly` as well. These will be reported as errors later in binding.
if (IsParameterModifierExcludingScoped(this.CurrentToken))
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 intentionally allow a broader set than before. so you can say things like scoped this. This is caught later in the binder.

return false;

// In C# 14 we decided that within a lambda 'scoped' would *always* be a keyword.
// In C# 14 we decided that within a lambda 'scoped' would *always* be a modifier, not a type.
Copy link
Member Author

Choose a reason for hiding this comment

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

refining this comment. It wasn't 100% accurate. It's not always a keyword. But it is a modifier if it previous would have been allowed as a type.

bool isFunctionPointerParameter,
bool isLambdaParameter)
{
if (this.CurrentToken.ContextualKind != SyntaxKind.ScopedKeyword)
Copy link
Member Author

Choose a reason for hiding this comment

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

this got much easier as we just need to return a bool saying if this should be considered a modifier. We don't actually have to do all the setting-up/rewinding if we change our minds. This means we just need a single reset point in here, and the caller can just eat depending on what we return.

@CyrusNajmabadi
Copy link
Member Author

@dotnet/roslyn-compiler ptal. I would likke to take this before #81604 as the latter ends up changing error recovery at the top level, and we have a bunch of tests that test pathological 'scoped' cases at the top level. These result in bad cascading errors today, and bad (but different) cascading errors after that change (making the diff messy). By cleaning things up here and making scoped-parsing very resilient and clean, it improves the end user UX and helps out other changes to improve bad parsing in other scenarios.

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.

1 participant