-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
[mypyc] Add match
statement support
#13953
Merged
Merged
Changes from 100 commits
Commits
Show all changes
102 commits
Select commit
Hold shift + click to select a range
3e6744e
Add `match` statement entrypoint:
dosisod 39f366d
Cleanup
dosisod 8bee07a
Add value pattern check
dosisod 0883015
Cleanup
dosisod e646c44
Reset
dosisod b27b380
Add value pattern
dosisod f1e6a30
Explicitly type out len 2 or pattern
dosisod 31e3ace
Add multiple Or Pattern support
dosisod 538d488
Add one
dosisod b653335
Generalize
dosisod 2870c8e
Minimize
dosisod 988769d
Rearange codeblock
dosisod 6cae5f8
Cleanup
dosisod 856dc85
Make it more readable
dosisod 65c9aae
Add class pattern support
dosisod 2955d9f
Add wildcard
dosisod 96fb472
Add multibody support
dosisod f961fd0
Fix failing tests
dosisod 899c954
Add final block gotos
dosisod dcf84e0
Add complex sanity test
dosisod eb2dc2d
Add run test
dosisod 522461d
Add pattern guard for value pattern
dosisod 8ce67d9
Add pattern guard support to all existing patterns
dosisod c364489
Add singleton pattern
dosisod d43e8ca
Greatly reduce number of opcodes
dosisod acdeb33
Move code_block out
dosisod beb1476
Move out next_block
dosisod 4d8cbc3
Cleanup
dosisod a332551
Add elifs
dosisod 8fd6546
Move pattern building to its own function
dosisod 42b8d58
Move body builder out of each if stmt
dosisod 130647e
Add recursive matching
dosisod cc9f375
Add basic AsPattern support
dosisod 331531b
Add groundwork for captured patterns
dosisod 92b05b8
Convert to visitor
dosisod 2e1a2ad
Rewrite using visitor
dosisod c55236e
Add basic AsPattern support for ValuePattern:
dosisod 0af9b24
Add Or pattern support for AsPattern
dosisod aedd1a6
Add AsPattern support for class pattern
dosisod 5b95596
Cleanup
dosisod 25d0edc
Add basic positional arg parsing
dosisod b85f178
Add support for variable number of positional args
dosisod 9293549
Use self.code_block
dosisod 2132f78
Add support for keyword class patterns
dosisod c5dd161
Add better scoping entering
dosisod c64c343
Split context managers
dosisod 49d60c8
Support nested patterns in class pattern
dosisod 1deecb4
Fix as pattern binding to subpatterns
dosisod df4a146
Add positional captures
dosisod 0376526
Add basic mapping support
dosisod 5e479b0
Add key value patterns
dosisod 820b9bd
Add basic mapping rest
dosisod 29eadd5
Make sure to pop keys from rest dict
dosisod d538d69
Cleanup
dosisod 553e41c
Split match stuff into its own file
dosisod 5c8b4c5
Merge remote-tracking branch 'upstream/master' into mypyc-match
dosisod ec128ad
Add a bunch of tests
dosisod 1b0523b
Fix next_block not being setup for or pattern
dosisod 626d8f1
Fix as pattern variable being assigned if condition is false
dosisod 3bb2e55
Sorta fix mapping issue:
dosisod 59f140e
Switch to using PyDict_Check
dosisod 67efc08
Fix dict item being accessed via get attr instead of get item
dosisod e57f0a1
Check that key is contained in dict before grabbing it
dosisod 214dcea
Finish map runtime tests
dosisod ad82d9f
Add empty sequence pattern matching
dosisod c59c465
Add basic sequence support
dosisod fc74fa2
Get unbound sequence capture working at end of list (almost):
dosisod 739962f
Fix last commit
dosisod 0115381
Updates
dosisod 38a5cc5
Require exact size for fixed length sequences
dosisod 04f0cbc
Very hacky support for star pattern in middle of list
dosisod 360f686
Hackily add leading star pattern
dosisod 9714a2c
Cleanups
dosisod 5b70d51
Renaming
dosisod 25564c8
Cleanup
dosisod 9601a23
Cleanups
dosisod 16e8d12
Renames
dosisod b241029
Cleanup
dosisod 4b2cfc3
Cleanups
dosisod ede2e14
More cleanups
dosisod 0b30e8b
Add class pattern support for builtins
dosisod 8f0a3bf
Cleanup
dosisod 96cd6b1
Add more tests
dosisod 47f9682
Black
dosisod 532ff8a
Uncomment old tests, add python_version flag
dosisod f73b60a
Reorganize ops
dosisod b525188
Isort
dosisod e01090c
Merge upstream
dosisod a117794
Switch to using older typing syntax
dosisod 22c1327
Fix build errors:
dosisod 3836f00
Only run match code for Python 3.10+
dosisod 4f18437
Fix length of empty sequence patterns not being checked
dosisod 4f218b9
Fix `[*rest]` patterns not binding to `rest`
dosisod 6c5de33
Allow for pattern matching Mapping and Sequence protocols
dosisod c8f8c84
Remove previously added command-line arguments:
dosisod dff0c71
Fix flags not being defined in Python 3.9 and below:
dosisod 03fab7e
Merge branch 'master' into mypyc-match
dosisod 07486a0
Attempt to fix last commit:
dosisod 85ec2d7
Merge branch 'master' into mypyc-match
dosisod 9728bc6
Trigger CI
dosisod 5bb3e28
Merge branch 'master' into mypyc-match
dosisod af5bca8
Add review suggestions:
dosisod File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,346 @@ | ||
from contextlib import contextmanager | ||
from typing import Generator, List, Optional, Tuple | ||
|
||
from mypy.nodes import MatchStmt, NameExpr, TypeInfo | ||
from mypy.patterns import ( | ||
AsPattern, | ||
ClassPattern, | ||
MappingPattern, | ||
OrPattern, | ||
Pattern, | ||
SequencePattern, | ||
SingletonPattern, | ||
StarredPattern, | ||
ValuePattern, | ||
) | ||
from mypy.traverser import TraverserVisitor | ||
from mypy.types import Instance, TupleType, get_proper_type | ||
from mypyc.ir.ops import BasicBlock, Value | ||
from mypyc.ir.rtypes import object_rprimitive | ||
from mypyc.irbuild.builder import IRBuilder | ||
from mypyc.primitives.dict_ops import ( | ||
dict_copy, | ||
dict_del_item, | ||
mapping_has_key, | ||
supports_mapping_protocol, | ||
) | ||
from mypyc.primitives.generic_ops import generic_ssize_t_len_op | ||
from mypyc.primitives.list_ops import ( | ||
sequence_get_item, | ||
sequence_get_slice, | ||
supports_sequence_protocol, | ||
) | ||
from mypyc.primitives.misc_ops import slow_isinstance_op | ||
|
||
# From: https://peps.python.org/pep-0634/#class-patterns | ||
MATCHABLE_BUILTINS = { | ||
"builtins.bool", | ||
"builtins.bytearray", | ||
"builtins.bytes", | ||
"builtins.dict", | ||
"builtins.float", | ||
"builtins.frozenset", | ||
"builtins.int", | ||
"builtins.list", | ||
"builtins.set", | ||
"builtins.str", | ||
"builtins.tuple", | ||
} | ||
|
||
|
||
class MatchVisitor(TraverserVisitor): | ||
builder: IRBuilder | ||
code_block: BasicBlock | ||
next_block: BasicBlock | ||
final_block: BasicBlock | ||
subject: Value | ||
match: MatchStmt | ||
|
||
as_pattern: Optional[AsPattern] = None | ||
|
||
def __init__(self, builder: IRBuilder, match_node: MatchStmt) -> None: | ||
self.builder = builder | ||
|
||
self.code_block = BasicBlock() | ||
self.next_block = BasicBlock() | ||
self.final_block = BasicBlock() | ||
|
||
self.match = match_node | ||
self.subject = builder.accept(match_node.subject) | ||
|
||
def build_match_body(self, index: int) -> None: | ||
self.builder.activate_block(self.code_block) | ||
|
||
guard = self.match.guards[index] | ||
|
||
if guard: | ||
self.code_block = BasicBlock() | ||
|
||
cond = self.builder.accept(guard) | ||
self.builder.add_bool_branch(cond, self.code_block, self.next_block) | ||
|
||
self.builder.activate_block(self.code_block) | ||
|
||
self.builder.accept(self.match.bodies[index]) | ||
self.builder.goto(self.final_block) | ||
|
||
def visit_match_stmt(self, m: MatchStmt) -> None: | ||
for i, pattern in enumerate(m.patterns): | ||
self.code_block = BasicBlock() | ||
self.next_block = BasicBlock() | ||
|
||
pattern.accept(self) | ||
|
||
self.build_match_body(i) | ||
self.builder.activate_block(self.next_block) | ||
|
||
self.builder.goto_and_activate(self.final_block) | ||
|
||
def visit_value_pattern(self, pattern: ValuePattern) -> None: | ||
value = self.builder.accept(pattern.expr) | ||
|
||
cond = self.builder.binary_op(self.subject, value, "==", pattern.expr.line) | ||
|
||
self.bind_as_pattern(value) | ||
|
||
self.builder.add_bool_branch(cond, self.code_block, self.next_block) | ||
|
||
def visit_or_pattern(self, pattern: OrPattern) -> None: | ||
backup_block = self.next_block | ||
self.next_block = BasicBlock() | ||
|
||
for p in pattern.patterns: | ||
# Hack to ensure the as pattern is bound to each pattern in the | ||
# "or" pattern, but not every subpattern | ||
backup = self.as_pattern | ||
p.accept(self) | ||
self.as_pattern = backup | ||
|
||
self.builder.activate_block(self.next_block) | ||
self.next_block = BasicBlock() | ||
|
||
self.next_block = backup_block | ||
self.builder.goto(self.next_block) | ||
|
||
def visit_class_pattern(self, pattern: ClassPattern) -> None: | ||
cond = self.builder.call_c( | ||
slow_isinstance_op, | ||
[self.subject, self.builder.accept(pattern.class_ref)], | ||
pattern.line, | ||
) | ||
|
||
self.builder.add_bool_branch(cond, self.code_block, self.next_block) | ||
|
||
self.bind_as_pattern(self.subject, new_block=True) | ||
|
||
if pattern.positionals: | ||
if pattern.class_ref.fullname in MATCHABLE_BUILTINS: | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
pattern.positionals[0].accept(self) | ||
|
||
return | ||
|
||
node = pattern.class_ref.node | ||
assert isinstance(node, TypeInfo) | ||
|
||
ty = node.names.get("__match_args__") | ||
assert ty | ||
|
||
match_args_type = get_proper_type(ty.type) | ||
assert isinstance(match_args_type, TupleType) | ||
|
||
match_args: List[str] = [] | ||
|
||
for item in match_args_type.items: | ||
proper_item = get_proper_type(item) | ||
assert isinstance(proper_item, Instance) and proper_item.last_known_value | ||
|
||
match_arg = proper_item.last_known_value.value | ||
assert isinstance(match_arg, str) | ||
|
||
match_args.append(match_arg) | ||
|
||
for i, expr in enumerate(pattern.positionals): | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
positional = self.builder.py_get_attr(self.subject, match_args[i], expr.line) | ||
dosisod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
with self.enter_subpattern(positional): | ||
expr.accept(self) | ||
|
||
for key, value in zip(pattern.keyword_keys, pattern.keyword_values): | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
attr = self.builder.py_get_attr(self.subject, key, value.line) | ||
dosisod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
with self.enter_subpattern(attr): | ||
value.accept(self) | ||
|
||
def visit_as_pattern(self, pattern: AsPattern) -> None: | ||
if pattern.pattern: | ||
old_pattern = self.as_pattern | ||
self.as_pattern = pattern | ||
pattern.pattern.accept(self) | ||
self.as_pattern = old_pattern | ||
|
||
elif pattern.name: | ||
target = self.builder.get_assignment_target(pattern.name) | ||
|
||
self.builder.assign(target, self.subject, pattern.line) | ||
|
||
self.builder.goto(self.code_block) | ||
|
||
def visit_singleton_pattern(self, pattern: SingletonPattern) -> None: | ||
if pattern.value is None: | ||
obj = self.builder.none_object() | ||
elif pattern.value is True: | ||
obj = self.builder.true() | ||
else: | ||
obj = self.builder.false() | ||
|
||
cond = self.builder.binary_op(self.subject, obj, "is", pattern.line) | ||
|
||
self.builder.add_bool_branch(cond, self.code_block, self.next_block) | ||
|
||
def visit_mapping_pattern(self, pattern: MappingPattern) -> None: | ||
is_dict = self.builder.call_c(supports_mapping_protocol, [self.subject], pattern.line) | ||
|
||
self.builder.add_bool_branch(is_dict, self.code_block, self.next_block) | ||
|
||
keys: List[Value] = [] | ||
|
||
for key, value in zip(pattern.keys, pattern.values): | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
key_value = self.builder.accept(key) | ||
keys.append(key_value) | ||
|
||
exists = self.builder.call_c(mapping_has_key, [self.subject, key_value], pattern.line) | ||
|
||
self.builder.add_bool_branch(exists, self.code_block, self.next_block) | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
item = self.builder.gen_method_call( | ||
self.subject, "__getitem__", [key_value], object_rprimitive, pattern.line | ||
) | ||
|
||
with self.enter_subpattern(item): | ||
value.accept(self) | ||
|
||
if pattern.rest: | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
rest = self.builder.call_c(dict_copy, [self.subject], pattern.rest.line) | ||
|
||
target = self.builder.get_assignment_target(pattern.rest) | ||
|
||
self.builder.assign(target, rest, pattern.rest.line) | ||
|
||
for i, key_name in enumerate(keys): | ||
self.builder.call_c(dict_del_item, [rest, key_name], pattern.keys[i].line) | ||
|
||
self.builder.goto(self.code_block) | ||
|
||
def visit_sequence_pattern(self, seq_pattern: SequencePattern) -> None: | ||
star_index, capture, patterns = prep_sequence_pattern(seq_pattern) | ||
|
||
is_list = self.builder.call_c(supports_sequence_protocol, [self.subject], seq_pattern.line) | ||
|
||
self.builder.add_bool_branch(is_list, self.code_block, self.next_block) | ||
|
||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
actual_len = self.builder.call_c(generic_ssize_t_len_op, [self.subject], seq_pattern.line) | ||
min_len = len(patterns) | ||
|
||
is_long_enough = self.builder.binary_op( | ||
actual_len, | ||
self.builder.load_int(min_len), | ||
"==" if star_index is None else ">=", | ||
seq_pattern.line, | ||
) | ||
|
||
self.builder.add_bool_branch(is_long_enough, self.code_block, self.next_block) | ||
|
||
for i, pattern in enumerate(patterns): | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
if star_index is not None and i >= star_index: | ||
current = self.builder.binary_op( | ||
actual_len, self.builder.load_int(min_len - i), "-", pattern.line | ||
) | ||
|
||
else: | ||
current = self.builder.load_int(i) | ||
|
||
item = self.builder.call_c(sequence_get_item, [self.subject, current], pattern.line) | ||
|
||
with self.enter_subpattern(item): | ||
pattern.accept(self) | ||
|
||
if capture and star_index is not None: | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
capture_end = self.builder.binary_op( | ||
actual_len, self.builder.load_int(min_len - star_index), "-", capture.line | ||
) | ||
|
||
rest = self.builder.call_c( | ||
sequence_get_slice, | ||
[self.subject, self.builder.load_int(star_index), capture_end], | ||
capture.line, | ||
) | ||
|
||
target = self.builder.get_assignment_target(capture) | ||
self.builder.assign(target, rest, capture.line) | ||
|
||
self.builder.goto(self.code_block) | ||
|
||
def bind_as_pattern(self, value: Value, new_block: bool = False) -> None: | ||
if self.as_pattern and self.as_pattern.pattern and self.as_pattern.name: | ||
if new_block: | ||
self.builder.activate_block(self.code_block) | ||
self.code_block = BasicBlock() | ||
|
||
target = self.builder.get_assignment_target(self.as_pattern.name) | ||
self.builder.assign(target, value, self.as_pattern.pattern.line) | ||
|
||
self.as_pattern = None | ||
|
||
if new_block: | ||
self.builder.goto(self.code_block) | ||
|
||
@contextmanager | ||
def enter_subpattern(self, subject: Value) -> Generator[None, None, None]: | ||
old_subject = self.subject | ||
self.subject = subject | ||
yield | ||
self.subject = old_subject | ||
|
||
|
||
def prep_sequence_pattern( | ||
seq_pattern: SequencePattern, | ||
) -> Tuple[Optional[int], Optional[NameExpr], List[Pattern]]: | ||
star_index: Optional[int] = None | ||
capture: Optional[NameExpr] = None | ||
patterns: List[Pattern] = [] | ||
|
||
for i, pattern in enumerate(seq_pattern.patterns): | ||
if isinstance(pattern, StarredPattern): | ||
star_index = i | ||
capture = pattern.capture | ||
|
||
else: | ||
patterns.append(pattern) | ||
|
||
return star_index, capture, patterns |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we could possibly use a more efficient isinstance op if we are dealing with native classes or primitive types. Can you at least add a TODO comment about this (this can be improved in a follow-up PR)?