Skip to content

Add support for match statements to parser - #4519

Merged
DimitrisJim merged 4 commits into
RustPython:mainfrom
astral-sh:charlie/match
Feb 21, 2023
Merged

DimitrisJim merged 4 commits into
RustPython:mainfrom
astral-sh:charlie/match

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Feb 18, 2023 •

Copy link
Copy Markdown
Collaborator

Summary

This is a work-in-progress, but it does work thus far for the implemented cases. I'm putting it up now to (1) signal that I'm working on it, and (2) get feedback from anyone else that has explored this and can see problems ahead.

closes #4323
closes #3427

@charliermarsh
charliermarsh force-pushed the charlie/match branch 3 times, most recently from 5265ca2 to 85e8243 Compare February 18, 2023 04:08
@andersk

andersk commented Feb 18, 2023 •

Copy link
Copy Markdown
Contributor

The main challenge here is that match and case are soft keywords. Naïvely adding them as keyword tokens will break parsing of code like

match = re.match(pattern, str)
if match is not None:
    print(match[1])

that uses match as an identifier.

We typically don’t know whether match should be treated as a keyword or an identifier until many tokens later. Some stupid examples I’ve collected where this results in interesting parse tree differences:

match *a + b, c   # ((match * a) + b), c
match *a + b, c:  # match ((*(a + b)), c):

match *(a + b), c   # (match * (a + b)), c
match *(a + b), c:  # match ((*(a + b)), c):

match (*a + b, c)   # match ((*(a + b)), c)
match (*a + b, c):  # match ((*(a + b)), c):
match -a * b + c   # (match - (a * b)) + c
match -a * b + c:  # match (((-a) * b) + c):

match -(a * b) + c   # (match - (a * b)) + c
match -(a * b) + c:  # match ((-(a * b) + c):

match (-a) * b + c   # (match (-(a * b))) + c
match (-a) * b + c:  # match (((-a) * b) + c):
match ().a   # (match()).a
match ().a:  # match (().a):

match (()).a   # (match(())).a
match (()).a:  # match (().a):

match ((),).a   # (match(())).a
match ((),).a:  # match (((),).a):
match (a).b   # (match(a)).b
match (a).b:  # match (a.b):

match (a.b)   # match(a.b)
match (a.b):  # match (a.b):

match (a,).b   # (match(a)).b     (not (match((a,))).b)
match (a,).b:  # match ((a,).b):  (not match (a.b):)

match ((a,)).b   # (match((a,))).b
match ((a,)).b:  # match ((a,).b):
match [a].b   # (match[a]).b
match [a].b:  # match ([a].b):

match [a,].b   # (match[(a,)]).b  (not (match[a]).b)
match [a,].b:  # match ([a].b):   (not match ([(a,)].b):)

match [(a,)].b   # (match[(a,)]).b
match [(a,)].b:  # match ([(a,)].b):

My rough plan (slightly amended after looking again) was going to be:

  • start parsing match * …, match(EXPR)…, match[EXPR]… under the assumption that it’s an expression with a match identifier, but
  • start parsing match + …, match - …, match()…, match(EXPR, …, match[EXPR, … under the assumption that it’s a match statement,

so that in either case there should be enough information to convert to the other form later if we find the initial assumption was wrong. This specific initial classification is important: there’s not enough information to translate in the other direction in either case.

Of course, this is easier said than coded in LALRPOP, and I’m not entirely sure whether LALRPOP bugs are interfering with my attempt (I already found one).

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

Thanks, this is extremely helpful. (I kind of assumed I was missing something important given how smoothly this went.)

If you’re further along and want to keep pushing on it, I can close and defer to you. Or if you want to collaborate on a branch that works too. Or I can keep trying here based on some of the pointers above. Whatever you prefer.

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

Some discussion from Parso that may or may not end up being relevant here: davidhalter/parso#138 (comment)

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

To what degree would getting those LALRPOP changes merge help here? Do we need to consider publishing a fork for now?

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

I also found this interesting on how Python 3.5 and earlier handled async and await as soft keywords via lexer tricks, but I doubt they’d help us here: https://benjam.info/blog/posts/2019-09-18-python-deep-dive-tokenizer/#async-and-await

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

Although… could we have the lexer look ahead? Is that crazy? Isn’t it a match statement IFF the line starts with match and ends in a colon? (Haven’t thought this through, I assume that won’t work.)

@andersk

andersk commented Feb 18, 2023

Copy link
Copy Markdown
Contributor

No I don’t think a lexer trick like that can work:

match()[a:
    b]
match()[a:
    b]:

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

Well you’d need to skip over bracketed content, no? I.e., look ahead until you hit a logical newline (not a physical newline)? At which point you’d either have a semicolon or not.

@charliermarsh

Copy link
Copy Markdown
Collaborator Author

Put differently, what I’m asking is whether we could backtrack or lookahead in the lexer (where we have more control) rather than complicating the parser. E.g. I think LALRPOP does some backtracking in its own lexer (https://github.com/lalrpop/lalrpop/blob/b777a6a884052d6283971f71b3ba453aa6d417ab/lalrpop/src/tok/mod.rs#L138), but I’d have to try this myself to have a real opinion on it.

@charliermarsh
charliermarsh force-pushed the charlie/match branch 6 times, most recently from 8d0f96e to 5b44678 Compare February 19, 2023 20:32
@charliermarsh
charliermarsh marked this pull request as ready for review February 19, 2023 20:33
Comment thread compiler/parser/python.lalrpop Resolved
@charliermarsh
charliermarsh force-pushed the charlie/match branch 2 times, most recently from 9016f92 to 33bc7f2 Compare February 20, 2023 00:08
@charliermarsh

Copy link
Copy Markdown
Collaborator Author

This is able to parse all of test_patma.py.

@charliermarsh
charliermarsh force-pushed the charlie/match branch 4 times, most recently from 55610d6 to 0893e73 Compare February 20, 2023 03:37
@charliermarsh

Copy link
Copy Markdown
Collaborator Author

From what I can tell, performance is the ~same or within 1% (for parsing CPython via Ruff).

kwd_patterns: vec![],
}
},
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I suspect some of this could be made more concise.

@youknowone

Copy link
Copy Markdown
Member

@andersk could you review this PR?

@youknowone youknowone left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not sure this design fits for full-featured match statement support, but it looks really good as starting of it.

Comment thread compiler/parser/python.lalrpop Resolved
Comment thread compiler/parser/src/soft_keywords.rs Resolved
@DimitrisJim

Copy link
Copy Markdown
Member

@youknowone I say we merge this later today, what do you think?

@youknowone

youknowone commented Feb 21, 2023 •

Copy link
Copy Markdown
Member

Totally fine if andersk is busy. I forgot the specific branch, but I found @charliermarsh already tested it with ruff somewhere.
Here: astral-sh/ruff#3047

Thanks @flying-sheep

@youknowone

Copy link
Copy Markdown
Member

unpinning #2759

@charliermarsh

charliermarsh commented Feb 21, 2023 •

Copy link
Copy Markdown
Collaborator Author

Yeah I've run that branch over the following codebases and, in each case, verified that the lint violation counts are identical before and after that change:

  • Hatch (198 violations)
  • Pandas (2766 violations)
  • Zulip (151 violations)
  • scikit-build-core (9 violations)
  • Airflow (11 violations)

On CPython, we see 18367 violations before and 18533 after; however, one fewer parse error in test_patma.py, so more violations are expected, since we're now actually analyzing that file.

@DimitrisJim
DimitrisJim merged commit ddf4976 into RustPython:main Feb 21, 2023
@DimitrisJim

Copy link
Copy Markdown
Member

Merging so we don't unnecessarily hold up astral-sh/ruff#282 too, really big thanks for this @charliermarsh!

charliermarsh pushed a commit to RustPython/Parser that referenced this pull request Jul 17, 2023
Extends #95
Closes #82 

Adds parsing of new `type` soft keyword for defining type aliases.

Supports type alias statements as defined in PEP 695 e.g. 

```python
# A non-generic type alias
type IntOrStr = int | str

# A generic type alias
type ListOrSet[T] = list[T] | set[T]

# A type alias that includes a forward reference
type AnimalOrVegetable = Animal | "Vegetable"

# A generic self-referential type alias
type RecursiveList[T] = T | list[RecursiveList[T]]
```

All type parameter kinds are supported as in #95.

Builds on soft keyword abstractions introduced in RustPython/RustPython#4519
zanieb added a commit to astral-sh/RustPython-Parser that referenced this pull request Jul 17, 2023
…hon#97)

Extends RustPython#95
Closes RustPython#82

Adds parsing of new `type` soft keyword for defining type aliases.

Supports type alias statements as defined in PEP 695 e.g.

```python
type IntOrStr = int | str

type ListOrSet[T] = list[T] | set[T]

type AnimalOrVegetable = Animal | "Vegetable"

type RecursiveList[T] = T | list[RecursiveList[T]]
```

All type parameter kinds are supported as in RustPython#95.

Builds on soft keyword abstractions introduced in RustPython/RustPython#4519
zanieb added a commit to astral-sh/RustPython-Parser that referenced this pull request Jul 17, 2023
…hon#97)

Extends RustPython#95
Closes RustPython#82

Adds parsing of new `type` soft keyword for defining type aliases.

Supports type alias statements as defined in PEP 695 e.g.

```python
type IntOrStr = int | str

type ListOrSet[T] = list[T] | set[T]

type AnimalOrVegetable = Animal | "Vegetable"

type RecursiveList[T] = T | list[RecursiveList[T]]
```

All type parameter kinds are supported as in RustPython#95.

Builds on soft keyword abstractions introduced in RustPython/RustPython#4519
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants