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

[mypyc] Add match statement support #13953

Merged
merged 102 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
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 Oct 9, 2022
39f366d
Cleanup
dosisod Oct 9, 2022
8bee07a
Add value pattern check
dosisod Oct 10, 2022
0883015
Cleanup
dosisod Oct 10, 2022
e646c44
Reset
dosisod Oct 11, 2022
b27b380
Add value pattern
dosisod Oct 11, 2022
f1e6a30
Explicitly type out len 2 or pattern
dosisod Oct 11, 2022
31e3ace
Add multiple Or Pattern support
dosisod Oct 11, 2022
538d488
Add one
dosisod Oct 11, 2022
b653335
Generalize
dosisod Oct 11, 2022
2870c8e
Minimize
dosisod Oct 11, 2022
988769d
Rearange codeblock
dosisod Oct 11, 2022
6cae5f8
Cleanup
dosisod Oct 11, 2022
856dc85
Make it more readable
dosisod Oct 11, 2022
65c9aae
Add class pattern support
dosisod Oct 11, 2022
2955d9f
Add wildcard
dosisod Oct 12, 2022
96fb472
Add multibody support
dosisod Oct 12, 2022
f961fd0
Fix failing tests
dosisod Oct 12, 2022
899c954
Add final block gotos
dosisod Oct 12, 2022
dcf84e0
Add complex sanity test
dosisod Oct 12, 2022
eb2dc2d
Add run test
dosisod Oct 12, 2022
522461d
Add pattern guard for value pattern
dosisod Oct 12, 2022
8ce67d9
Add pattern guard support to all existing patterns
dosisod Oct 12, 2022
c364489
Add singleton pattern
dosisod Oct 12, 2022
d43e8ca
Greatly reduce number of opcodes
dosisod Oct 12, 2022
acdeb33
Move code_block out
dosisod Oct 14, 2022
beb1476
Move out next_block
dosisod Oct 14, 2022
4d8cbc3
Cleanup
dosisod Oct 14, 2022
a332551
Add elifs
dosisod Oct 14, 2022
8fd6546
Move pattern building to its own function
dosisod Oct 14, 2022
42b8d58
Move body builder out of each if stmt
dosisod Oct 14, 2022
130647e
Add recursive matching
dosisod Oct 14, 2022
cc9f375
Add basic AsPattern support
dosisod Oct 15, 2022
331531b
Add groundwork for captured patterns
dosisod Oct 16, 2022
92b05b8
Convert to visitor
dosisod Oct 16, 2022
2e1a2ad
Rewrite using visitor
dosisod Oct 16, 2022
c55236e
Add basic AsPattern support for ValuePattern:
dosisod Oct 16, 2022
0af9b24
Add Or pattern support for AsPattern
dosisod Oct 16, 2022
aedd1a6
Add AsPattern support for class pattern
dosisod Oct 16, 2022
5b95596
Cleanup
dosisod Oct 16, 2022
25d0edc
Add basic positional arg parsing
dosisod Oct 19, 2022
b85f178
Add support for variable number of positional args
dosisod Oct 19, 2022
9293549
Use self.code_block
dosisod Oct 19, 2022
2132f78
Add support for keyword class patterns
dosisod Oct 19, 2022
c5dd161
Add better scoping entering
dosisod Oct 19, 2022
c64c343
Split context managers
dosisod Oct 19, 2022
49d60c8
Support nested patterns in class pattern
dosisod Oct 19, 2022
1deecb4
Fix as pattern binding to subpatterns
dosisod Oct 20, 2022
df4a146
Add positional captures
dosisod Oct 20, 2022
0376526
Add basic mapping support
dosisod Oct 20, 2022
5e479b0
Add key value patterns
dosisod Oct 21, 2022
820b9bd
Add basic mapping rest
dosisod Oct 21, 2022
29eadd5
Make sure to pop keys from rest dict
dosisod Oct 21, 2022
d538d69
Cleanup
dosisod Oct 21, 2022
553e41c
Split match stuff into its own file
dosisod Oct 21, 2022
5c8b4c5
Merge remote-tracking branch 'upstream/master' into mypyc-match
dosisod Oct 21, 2022
ec128ad
Add a bunch of tests
dosisod Oct 21, 2022
1b0523b
Fix next_block not being setup for or pattern
dosisod Oct 23, 2022
626d8f1
Fix as pattern variable being assigned if condition is false
dosisod Oct 23, 2022
3bb2e55
Sorta fix mapping issue:
dosisod Oct 23, 2022
59f140e
Switch to using PyDict_Check
dosisod Oct 23, 2022
67efc08
Fix dict item being accessed via get attr instead of get item
dosisod Oct 23, 2022
e57f0a1
Check that key is contained in dict before grabbing it
dosisod Oct 23, 2022
214dcea
Finish map runtime tests
dosisod Oct 23, 2022
ad82d9f
Add empty sequence pattern matching
dosisod Oct 23, 2022
c59c465
Add basic sequence support
dosisod Oct 23, 2022
fc74fa2
Get unbound sequence capture working at end of list (almost):
dosisod Oct 23, 2022
739962f
Fix last commit
dosisod Oct 23, 2022
0115381
Updates
dosisod Oct 24, 2022
38a5cc5
Require exact size for fixed length sequences
dosisod Oct 24, 2022
04f0cbc
Very hacky support for star pattern in middle of list
dosisod Oct 24, 2022
360f686
Hackily add leading star pattern
dosisod Oct 24, 2022
9714a2c
Cleanups
dosisod Oct 24, 2022
5b70d51
Renaming
dosisod Oct 24, 2022
25564c8
Cleanup
dosisod Oct 25, 2022
9601a23
Cleanups
dosisod Oct 25, 2022
16e8d12
Renames
dosisod Oct 25, 2022
b241029
Cleanup
dosisod Oct 25, 2022
4b2cfc3
Cleanups
dosisod Oct 25, 2022
ede2e14
More cleanups
dosisod Oct 25, 2022
0b30e8b
Add class pattern support for builtins
dosisod Oct 25, 2022
8f0a3bf
Cleanup
dosisod Oct 27, 2022
96cd6b1
Add more tests
dosisod Oct 27, 2022
47f9682
Black
dosisod Oct 27, 2022
532ff8a
Uncomment old tests, add python_version flag
dosisod Oct 27, 2022
f73b60a
Reorganize ops
dosisod Oct 27, 2022
b525188
Isort
dosisod Oct 27, 2022
e01090c
Merge upstream
dosisod Oct 27, 2022
a117794
Switch to using older typing syntax
dosisod Oct 28, 2022
22c1327
Fix build errors:
dosisod Oct 28, 2022
3836f00
Only run match code for Python 3.10+
dosisod Oct 28, 2022
4f18437
Fix length of empty sequence patterns not being checked
dosisod Oct 28, 2022
4f218b9
Fix `[*rest]` patterns not binding to `rest`
dosisod Oct 28, 2022
6c5de33
Allow for pattern matching Mapping and Sequence protocols
dosisod Oct 31, 2022
c8f8c84
Remove previously added command-line arguments:
dosisod Oct 31, 2022
dff0c71
Fix flags not being defined in Python 3.9 and below:
dosisod Oct 31, 2022
03fab7e
Merge branch 'master' into mypyc-match
dosisod Nov 4, 2022
07486a0
Attempt to fix last commit:
dosisod Nov 5, 2022
85ec2d7
Merge branch 'master' into mypyc-match
dosisod Nov 15, 2022
9728bc6
Trigger CI
dosisod Nov 15, 2022
5bb3e28
Merge branch 'master' into mypyc-match
dosisod Dec 1, 2022
af5bca8
Add review suggestions:
dosisod Dec 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ def find_attr_initializers(
and not isinstance(stmt.rvalue, TempNode)
):
name = stmt.lvalues[0].name
if name in ("__slots__", "__match_args__"):
if name == "__slots__":
continue

if name == "__deletable__":
Expand Down
346 changes: 346 additions & 0 deletions mypyc/irbuild/match.py
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,
)
Copy link
Collaborator

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)?


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
6 changes: 1 addition & 5 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,7 @@ def prepare_class_def(

if isinstance(node.node, Var):
assert node.node.type, "Class member %s missing type" % name
if not node.node.is_classvar and name not in (
"__slots__",
"__deletable__",
"__match_args__",
):
if not node.node.is_classvar and name not in ("__slots__", "__deletable__"):
ir.attributes[name] = mapper.type_to_rtype(node.node.type)
elif isinstance(node.node, (FuncDef, Decorator)):
prepare_method_def(ir, module_name, cdef, mapper, node.node)
Expand Down
7 changes: 7 additions & 0 deletions mypyc/irbuild/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ImportFrom,
ListExpr,
Lvalue,
MatchStmt,
OperatorAssignmentStmt,
RaiseStmt,
ReturnStmt,
Expand Down Expand Up @@ -99,6 +100,8 @@
yield_from_except_op,
)

from .match import MatchVisitor

GenFunc = Callable[[], None]
ValueGenFunc = Callable[[], Value]

Expand Down Expand Up @@ -898,3 +901,7 @@ def transform_yield_from_expr(builder: IRBuilder, o: YieldFromExpr) -> Value:

def transform_await_expr(builder: IRBuilder, o: AwaitExpr) -> Value:
return emit_yield_from_or_await(builder, builder.accept(o.expr), o.line, is_await=True)


def transform_match_stmt(builder: IRBuilder, m: MatchStmt) -> None:
m.accept(MatchVisitor(builder, m))
Loading