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

Improve parser recovery around nullable types in patterns #72805

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

DoctorKrolic
Copy link
Contributor

Closes: #72720

@DoctorKrolic DoctorKrolic requested a review from a team as a code owner March 29, 2024 17:55
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Mar 29, 2024
@dotnet-policy-service dotnet-policy-service bot added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Mar 29, 2024
@DoctorKrolic DoctorKrolic force-pushed the nullable-type-in-pattern-recovery branch from 41d70bf to c058b16 Compare March 29, 2024 17:59
@@ -1774,5 +1756,379 @@ public void CreateNullableArray_07()
}
EOF();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72720")]
public void DisjunctivePattern_NullableType1()
Copy link
Member

Choose a reason for hiding this comment

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

i appreciate all the new tests.

@CyrusNajmabadi
Copy link
Member

Done with pass. Overall approve of the high level idea. Def wary of the actual code change (i don't understand it yet), and def think we need to beef up tests to ensure that things that are now legal to parse give proper diagnostics.

// Since nullable types (no matter reference of value types) are not valid in patterns
// by default we are biased towards the first option and consider case 2 only for error recovery purposes
// (if we parse here as nullable type pattern an error will be reported during binding).
// This condition checks for simple cases, where we better use option 2 and parse a nullable-typed pattern
Copy link
Member

Choose a reason for hiding this comment

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

this comment was great.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented May 5, 2024 via email

@DoctorKrolic
Copy link
Contributor Author

Are you ok with me doing that, or would you prefer to do that?

Go ahead. Just add a simple comment to changed tests, so it is obvious, that it is an intentional "degradation"

@CyrusNajmabadi
Copy link
Member

Will do.

@@ -7015,20 +7015,20 @@ private enum ParseTypeMode
FirstElementOfPossibleTupleLiteral,
}

private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal)
private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal, bool inSwitchArmPattern = false)
Copy link
Member

Choose a reason for hiding this comment

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

whenIsKeyword got renamed to inSwitchArmPattern to try to be clearer. We pass 'true' in here when parsign the pattern for a switch-statement or switch-expression pattern. And it controls how we view 'where' and a few other error recovery rules (including new ones in this pr).

@@ -431,7 +438,7 @@ private CSharpSyntaxNode ParseExpressionOrPatternForSwitchStatement()
{
var savedState = _termState;
_termState |= TerminatorState.IsExpressionOrPatternInCaseLabelOfSwitchStatement;
var pattern = ParsePattern(Precedence.Conditional, whenIsKeyword: true);
var pattern = ParsePattern(Precedence.Conditional, inSwitchArmPattern: true);
Copy link
Member

Choose a reason for hiding this comment

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

one of hte two locations where 'true' is passed in. Here we're parsing case clauses for switch-statements.

@@ -589,7 +596,7 @@ SeparatedSyntaxList<SwitchExpressionArmSyntax> parseSwitchExpressionArms()

var savedState = _termState;
_termState |= TerminatorState.IsPatternInSwitchExpressionArm;
var pattern = ParsePattern(Precedence.Coalescing, whenIsKeyword: true);
var pattern = ParsePattern(Precedence.Coalescing, inSwitchArmPattern: true);
Copy link
Member

Choose a reason for hiding this comment

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

one of hte two locations where 'true' is passed in. Here we're parsing the arms for switch-expressions.

@CyrusNajmabadi
Copy link
Member

lgtm. @dotnet/roslyn-compiler for reviews please.

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.

Done review pass. I didn't get any further than the comments that spelled out invalid assumptions for parsing behavior.

src/Compilers/CSharp/Portable/Parser/LanguageParser.cs Outdated Show resolved Hide resolved
{
// If we're in a cast statement/expression arm, then we have:
//
// `switch { case X ? y` or
Copy link
Member

Choose a reason for hiding this comment

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

This assumption is not correct. The following is valid code:

        switch(i3) {
            case b ? i1 : i2: break;
        }

This comment needs to be corrected, and we should re-examine this approach to ensure that we're not breaking anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "why on Earth this is legal" question should have probably been asked N years ago when switch statements were designed) I was sure that conditional expressions in case labels aren't semantically legal and thus there is no difference whether this is parsed as a conditional or a nullable pattern + label statement. There is already a test in the codebase for that case and I initially wrote a comment explaining how these 2 parse strategies both lead to a compiler error thus are identical for our purposes, had to revert that change. And I'm somewhat surprised that only a very few new test cases now are back to bad state after removing this assumption, so I don't think we should do additional work to restore them

Copy link
Member

Choose a reason for hiding this comment

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

The "why on Earth this is legal" question should have probably been asked N years ago when switch statements were designed)

So, this is legal because cases allow for patterns. And one sort of pattern is the 'constant pattern' (that's how x is 0 works for example). So these places allow for any sort of constant expression (including conditional expressions). Note: for conditional expressions to be constant, their condition itself must be constant. But that can often normally happen, esp. in generated code, or code referencing constants that are conditionally compiled in.

//
// `switch { case X ? y` or
//
// e switch { X ? y `
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, this assumption is closer to correct, but isn't always:

        _ = i3 switch {
                (b ? i1 : i2) => 3
        };

You do need the parentheses to force parse in this manner, but I'm concerned that we'll still end up with bad behavior here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that this requires parentheses, it wasn't affected even with the wrong assumption. Added a test for this case

@DoctorKrolic DoctorKrolic requested a review from 333fred May 7, 2024 15:38
UsingExpression("""
obj switch
{
(flag ? a : b) => 1
Copy link
Member

Choose a reason for hiding this comment

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

can you have a test for switch statements as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are already in the codebase. See commit 3a6f3bd, I've changed one of those tests back to what it was. The parenthesized case is exactly below it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here they are in main:

[Fact]
public void ConditionalAsConstantPatternInSwitchStatement()
{
UsingStatement(@"switch(a) { case a?x:y: break; }",
TestOptions.RegularWithPatternCombinators
);
N(SyntaxKind.SwitchStatement);
{
N(SyntaxKind.SwitchKeyword);
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "a");
}
N(SyntaxKind.CloseParenToken);
N(SyntaxKind.OpenBraceToken);
N(SyntaxKind.SwitchSection);
{
N(SyntaxKind.CaseSwitchLabel);
{
N(SyntaxKind.CaseKeyword);
N(SyntaxKind.ConditionalExpression);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "a");
}
N(SyntaxKind.QuestionToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "x");
}
N(SyntaxKind.ColonToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "y");
}
}
N(SyntaxKind.ColonToken);
}
N(SyntaxKind.BreakStatement);
{
N(SyntaxKind.BreakKeyword);
N(SyntaxKind.SemicolonToken);
}
}
N(SyntaxKind.CloseBraceToken);
}
EOF();
}
[Fact]
public void ConditionalAsConstantPatternInSwitchStatement_Parenthesized()
{
UsingStatement(@"switch(a) { case (a?x:y): break; }",
TestOptions.RegularWithPatternCombinators
);
N(SyntaxKind.SwitchStatement);
{
N(SyntaxKind.SwitchKeyword);
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "a");
}
N(SyntaxKind.CloseParenToken);
N(SyntaxKind.OpenBraceToken);
N(SyntaxKind.SwitchSection);
{
N(SyntaxKind.CaseSwitchLabel);
{
N(SyntaxKind.CaseKeyword);
N(SyntaxKind.ParenthesizedExpression);
{
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.ConditionalExpression);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "a");
}
N(SyntaxKind.QuestionToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "x");
}
N(SyntaxKind.ColonToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "y");
}
}
N(SyntaxKind.CloseParenToken);
}
N(SyntaxKind.ColonToken);
}
N(SyntaxKind.BreakStatement);
{
N(SyntaxKind.BreakKeyword);
N(SyntaxKind.SemicolonToken);
}
}
N(SyntaxKind.CloseBraceToken);
}
EOF();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. 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.

Compiler has poor error recovery when encountering nullable types in pattern locations
3 participants