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

Parser confused by prefix "!" in pattern guard #52199

Closed
munificent opened this issue Apr 27, 2023 · 10 comments
Closed

Parser confused by prefix "!" in pattern guard #52199

munificent opened this issue Apr 27, 2023 · 10 comments
Assignees
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. area-fe-analyzer-shared Assigned by engineers; when triaging, prefer either area-front-end or area-analyzer. area-front-end Use area-front-end for front end / CFE / kernel format related issues. fe-analyzer-shared-parser Issues with the shared parser's handling of correct code
Milestone

Comments

@munificent
Copy link
Member

Consider:

case Enum.value when !flag:

The intent is that this is matching the constant Enum.value when flag is false. However, the compiler reports:

temp.dart:8:10: Error: 'Enum.value' can't be used as a type because 'Enum' doesn't refer to an import prefix.
    case Enum.value when !flag: print('ok');
         ^^^^^^^^^^
temp.dart:8:27: Error: Expected ':' before this.
    case Enum.value when !flag: print('ok');
                          ^^^^

What it thinks it is parsing is:

case (Enum.value when)! flag:

That is, a null-assert pattern whose inner pattern is Enum.value when. The latter is then interpreted as a variable pattern whose name is when and whose type is the prefixed name Enum.value.

After that, it sees an unexpected identifier flag and doesn't know what to do. This is a parser bug because there is an unambiguous way to parse this, which is to treat when as a contextual keyword.

But, more broadly, we should probably specify explicitly that when can't be used as an identifier in a pattern at all to avoid subtle or complex parsing.

@munificent munificent added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. area-front-end Use area-front-end for front end / CFE / kernel format related issues. area-fe-analyzer-shared Assigned by engineers; when triaging, prefer either area-front-end or area-analyzer. fe-analyzer-shared-parser Issues with the shared parser's handling of correct code labels Apr 27, 2023
@munificent munificent added this to the Dart 3 stable milestone Apr 27, 2023
@munificent
Copy link
Member Author

Note that the type name doesn't have to be qualified. A simpler repro is:

case foo when !flag:

@stereotype441 stereotype441 self-assigned this Apr 27, 2023
@stereotype441
Copy link
Member

stereotype441 commented Apr 27, 2023

Ok, I see the problem. Taking case foo when !flag: as an example, the core issue is that since the parser is event-based, it needs to decide at the point where it sees foo whether it's looking at a variable pattern with a type or an identifier pattern. It can't just try one interpretation and then backtrack if it's wrong, because there's no way to "un-fire" the parser events once it's fired them. But! Thanks to some heroics in the implementation, it has the ability to look ahead and figure out if something looks like a type; and if it does, look at the token(s) after the type.

So what it currently does is:

  • If the token sequence starting at foo looks like a type, and the token that follows is an identifier, it assumes it's looking at a variable pattern with a type; otherwise it assumes it's looking at an identifier pattern.
  • EXCEPT that if the token that follows the supposed type is when or as (both of which are valid identifiers), then it probes further:
  • If the token that follows when or as is a token that could legitimately follow a pattern, then it assumes that it's looking at a variable pattern with a type. (The tokens that could legitimately follow a pattern are ,, :, ||, &&, ), }, ], as, when, ?, !).
  • Otherwise it assumes that it's looking at an identifier pattern.

The place where this breaks down is in the third bullet: if the token that follows the supposed type is when, and the token that follows is as, when, or !, then we still haven't resolved the ambiguity, because those tokens could all legitimately start a guard expression. Similarly, if the token that follows the supposed type is as, and the token that follows is when, then we still haven't resolved the ambiguity, because those tokens could all legitimately start a type. So in point of fact we have problems parsing all of the following:

  • case foo when as: (where as is a local boolean variable)
  • case foo when when: (where when is a local boolean variable)
  • case foo when !flag: (the subject of this issue)
  • case foo as when: (where when is the name of a type)

I thought I was being so clever when I came up with the disambiguation technique described in the bullets above. Now I wish I had done something like @munificent's suggestion:

But, more broadly, we should probably specify explicitly that when can't be used as an identifier in a pattern at all to avoid subtle or complex parsing.

However, given the late hour, I'm inclined to suggest a more targeted fix, where we simply change the disambiguation rule to:

  • If the token sequence starting at foo looks like a type, and the token that follows is an identifier, assume the parser is looking at a variable pattern with a type; otherwise assume it's looking at an identifier pattern.
  • EXCEPT that if the token that follows the supposed type is when or as, then assume the parser is looking at an identifier pattern.

(The italicized part is the change).

I'll work on a CL that does this. I'm happy to discuss further if folks feel like this is the wrong approach.

@munificent
Copy link
Member Author

The targeted fix sounds fine to me. By "assume the parser is looking at an identifier pattern" does that mean that if the assumption turns out to be false then it will still treat when as an identifier in a variable pattern?

So given:

case foo when:

Will it parse that as a variable pattern of type foo with name when? I'm OK with the answer being "yes" or "no".

For 3.1, I'm definitely interested in shipping a (probably language versioned) change that says that when simply can't be used as an identifier in a pattern or guard.

@munificent
Copy link
Member Author

I just checked a big corpus of pub packages, open source Flutter apps, open source Flutter widgets, the Dart repo, and the Flutter repo, and I found zero switch cases containing the identifier when. So I think this targeted workaround or even a broader one that always treats when as a keyword is likely to be not problematic for users.

@stereotype441
Copy link
Member

The targeted fix sounds fine to me. By "assume the parser is looking at an identifier pattern" does that mean that if the assumption turns out to be false then it will still treat when as an identifier in a variable pattern?

Nope! That's the disadvantage of our event-based parser architecture: once you decide on an interpretation and start consuming tokens, you can't go back and change your mind. So:

case foo when:

will become a parse error.

For 3.1, I'm definitely interested in shipping a (probably language versioned) change that says that when simply can't be used as an identifier in a pattern or guard.

SGTM! I'll get to work on the fix now.

@munificent
Copy link
Member Author

So:

case foo when:

will become a parse error.

That's 100% fine with me. No existing pre-Dart 3.0 code should be broken by this (since no non-pattern switch case constant expressions syntactically look like variable patterns) and I'm fine with saying that you can't use when as a variable name in a switch case pattern.

@stereotype441
Copy link
Member

So:

case foo when:

will become a parse error.

That's 100% fine with me. No existing pre-Dart 3.0 code should be broken by this (since no non-pattern switch case constant expressions syntactically look like variable patterns) and I'm fine with saying that you can't use when as a variable name in a switch case pattern.

Just to be clear: the change I'm contemplating also prohibits when and as as typed variable patterns in an irrefutable context, e.g.:

var (int when, int as) = foo(); // ERROR: your variable names are bad and you should feel bad!

I could make the fix only apply in refutable contexts, but it would require some additional plumbing work (and thus be more risky).

@munificent
Copy link
Member Author

Just to be clear: the change I'm contemplating also prohibits when and as as typed variable patterns in an irrefutable context, e.g.:

var (int when, int as) = foo(); // ERROR: your variable names are bad and you should feel bad!

I could make the fix only apply in refutable contexts, but it would require some additional plumbing work (and thus be more risky).

That's also fine. There is no pre-Dart 3.0 code that contains irrefutable patterns, so it's non-breaking. And I don't see any positive value in allowing when or as as variable names in patterns in new code. Hell, I'd make them reserved words if I could (but not right now, obviously).

@munificent
Copy link
Member Author

munificent commented Apr 27, 2023

I went ahead and checked the head commits of the Dart SDK and Flutter repos and I don't see any uses of when as an identifier in a pattern, so I don't think this fix should break anything. (At least, that I could find, in those two repos.)

I did run into many uses of when as a function name from Mockito, but those seem to be unaffected. I also ran into a couple of uses of when as a variable, ironically, in the parser:

// pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart:8813
            when = token = next;

// pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart:8814
            listener.beginSwitchCaseWhenClause(when);

// pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart:8821
          listener.endCaseExpression(caseKeyword, when, token);

// pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart:10424
          when = token = next;

As long as the parser doesn't parse all assignments as pattern assignments, this should be OK.

I also don't see any uses of as in patterns that would be problematic.

@stereotype441
Copy link
Member

Fix is out for review: https://dart-review.googlesource.com/c/sdk/+/299400

copybara-service bot pushed a commit that referenced this issue May 1, 2023
This change fixes parsing of case clauses such as:

    case foo when !flag:

Constructions like this require some lookahead in order to parse
correctly, because the token `when` is valid both as an identifier and
as a part of the grammar for a case clause. Therefore, at the time
`foo` is encountered, the parser must decide whether it is looking at
a variable pattern (`foo when`, where `when` is the name of the
variable) or an identifier pattern (`foo`, where `when` begins the
case's guard clause). Previous to this fix, the algorithm for
disambiguating these two choices was as follows:

- If the token sequence starting at `foo` looked like a type, and the
  token that follows was an identifier, the parser assumed it was
  looking at a variable pattern with a type; otherwise it assumed it
  was looking at an identifier pattern.

- EXCEPT that if the token that followed the supposed type was `when`
  or `as` (both of which are valid identifiers), then it probed
  further:

- If the token that followed `when` or `as` was a token that could
  legitimately follow a pattern, then it assumed that it was looking
  at a variable pattern with a type. (The tokens that could
  legitimately follow a pattern are `,`, `:`, `||`, `&&`, `)`, `}`,
  `]`, `as`, `when`, `?`, `!`).

- Otherwise it assumed that it was looking at an identifier pattern.

This didn't fully disambiguate, because the third bullet didn't
account for the fact that the tokens `as`, `when`, and `!` could
_either_ legitimately follow a pattern _or_ legitimately begin an
expression (or, in the case of `when`, a type), therefore constructs
like the following were incorrectly parsed:

- `case foo when as:` (where `as` is a local boolean variable)
- `case foo when when:` (where `when` is a local boolean variable)
- `case foo when !flag:` (where `flag` is a local boolean variable)
- `case foo as when:` (where `when` is the name of a type)

The solution is to simplify the disambiguation logic so that if if the
token that follows the supposed type is `when` or `as`, then the
parser assumes that it's looking at an identifier pattern, _not_ a
typed variable pattern.

The consequence of this is that the above four constructions are
parsed correctly; however it is no longer possible for a typed
variable pattern to name a variable `when` or `as`.

For consistency we would like to prohibit _any_ variable pattern from
naming a variable `when` or `as`, however to keep this change as small
as possible (and reduce the risk involved in a possible cherry-pick)
that will be postponed until a later CL.

Cherry-pick request: #52215

Bug: #52199
Change-Id: I925c70b83bb1ef6efdf0cb2f24ea6186e55079b8
Cherry-pick: https://dart-review.googlesource.com/c/sdk/+/299400
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/299700
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Jens Johansen <jensj@google.com>
copybara-service bot pushed a commit that referenced this issue May 4, 2023
Bug: #52199
Change-Id: I04fcac4a2b9a7e49a0231441feb764a022dd5ab8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/300320
Reviewed-by: Bob Nystrom <rnystrom@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit that referenced this issue May 8, 2023
In https://dart-review.googlesource.com/c/sdk/+/299400, the parser was
adjusted so that it no longer accepts `when` and `as` as the names for
variable patterns in cases where there is a possible ambiguity
(e.g. `int when` is not accepted as a pattern because `int` is a
legitimate pattern, therefore `when` could introduce a guard
clause). This change further prohibits `when` and `as` from being the
names of variable patterns or identifier patterns even in the case
where there is no ambiguity. This is in line with the discussion at
#52199 (comment),
and the spec change at
dart-lang/language#3033.

Three new error codes are introduced, to cover the three circumstances
in which `when` or `as` might be used illegally: in a declared
variable pattern, in an assigned variable pattern, or in an identifier
pattern. I've also added analyzer tests to ensure that the parser
recovers from these errors nicely. Unfortunately, nice error recovery
is only feasible in the non-ambiguous cases.

I've also updated the language test expectations in
`tests/language/patterns/version_2_32_changes_error_test.dart` to
reflect the new error messages, and added a few more examples of uses
of `when` and `as` that are still permitted.

Fixes #52260.

Bug: #52260
Change-Id: I229f627aa639659c30b83c74895759207da279f7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/301482
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Jens Johansen <jensj@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit that referenced this issue May 12, 2023
… when/as.

In https://dart-review.googlesource.com/c/sdk/+/299400, the parser was
adjusted so that it no longer accepts `when` and `as` as the names for
variable patterns in cases where there is a possible ambiguity
(e.g. `int when` is not accepted as a pattern because `int` is a
legitimate pattern, therefore `when` could introduce a guard
clause). This change further prohibits `when` and `as` from being the
names of variable patterns or identifier patterns even in the case
where there is no ambiguity. This is in line with the discussion at
#52199 (comment),
and the spec change at
dart-lang/language#3033.

Three new error codes are introduced, to cover the three circumstances
in which `when` or `as` might be used illegally: in a declared
variable pattern, in an assigned variable pattern, or in an identifier
pattern. I've also added analyzer tests to ensure that the parser
recovers from these errors nicely. Unfortunately, nice error recovery
is only feasible in the non-ambiguous cases.

I've also updated the language test expectations in
`tests/language/patterns/version_2_32_changes_error_test.dart` to
reflect the new error messages, and added a few more examples of uses
of `when` and `as` that are still permitted.

Fixes: #52311

Bug: #52260
Change-Id: I30cb3a1ca627c6db3d281740fb59de74c4118c2b
Cherry-pick: https://dart-review.googlesource.com/c/sdk/+/301482
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/302100
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Jens Johansen <jensj@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. area-fe-analyzer-shared Assigned by engineers; when triaging, prefer either area-front-end or area-analyzer. area-front-end Use area-front-end for front end / CFE / kernel format related issues. fe-analyzer-shared-parser Issues with the shared parser's handling of correct code
Projects
None yet
Development

No branches or pull requests

2 participants