Skip to content

Commit cc5bab6

Browse files
committed
subtypes: fast path for Union/Union subtype check
Enums are exploded into Union of Literal when narrowed. Conditional branches on enum values can result in multiple distinct narrowing of the same enum which are later subject to subtype checks (most notably via `is_same_type`, when exiting frame context in the binder). Such checks would have quadratic complexity: `O(N*M)` where `N` and `M` are the number of entries in each narrowed enum variable, and led to drastic slowdown if any of the enums involved has a large number of valuees. Implemement a linear-time fast path where literals are quickly filtered, with a fallback to the slow path for more complex values. In our codebase there is one method with a chain of a dozen if statements operating on instances of an enum with a hundreds of values. Prior to the regression it was typechecked in less than 1s. After the regression it takes over 13min to typecheck. This patch fully fixes the regression for us. Fixes #13821
1 parent 2514610 commit cc5bab6

File tree

1 file changed

+37
-5
lines changed

1 file changed

+37
-5
lines changed

mypy/subtypes.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
UninhabitedType,
5858
UnionType,
5959
UnpackType,
60+
flatten_nested_unions,
6061
get_proper_type,
6162
is_named_instance,
6263
)
@@ -877,19 +878,50 @@ def visit_overloaded(self, left: Overloaded) -> bool:
877878
return False
878879

879880
def visit_union_type(self, left: UnionType) -> bool:
880-
if isinstance(self.right, Instance):
881+
if isinstance(self.right, (UnionType, Instance)):
882+
# prune literals early to avoid nasty quadratic behavior which would otherwise arise when checking
883+
# subtype relationships between slightly different narrowings of an Enum
884+
# we achieve O(N+M) instead of O(N*M)
885+
886+
right_lit_types: set[Instance] = set()
887+
right_lit_values: set[LiteralType] = set()
888+
889+
if isinstance(self.right, UnionType):
890+
for item in flatten_nested_unions(
891+
self.right.relevant_items(), handle_type_alias_type=True
892+
):
893+
p_item = get_proper_type(item)
894+
if isinstance(p_item, LiteralType):
895+
right_lit_values.add(p_item)
896+
elif isinstance(p_item, Instance):
897+
if p_item.last_known_value is None:
898+
right_lit_types.add(p_item)
899+
else:
900+
right_lit_values.add(p_item.last_known_value)
901+
elif isinstance(self.right, Instance):
902+
if self.right.last_known_value is None:
903+
right_lit_types.add(self.right)
904+
else:
905+
right_lit_values.add(self.right.last_known_value)
906+
881907
literal_types: set[Instance] = set()
882-
# avoid redundant check for union of literals
883908
for item in left.relevant_items():
884909
p_item = get_proper_type(item)
910+
if p_item in right_lit_types or p_item in right_lit_values:
911+
continue
885912
lit_type = mypy.typeops.simple_literal_type(p_item)
886913
if lit_type is not None:
887-
if lit_type in literal_types:
914+
if lit_type in right_lit_types:
888915
continue
889-
literal_types.add(lit_type)
890-
item = lit_type
916+
if isinstance(self.right, Instance):
917+
if lit_type in literal_types:
918+
continue
919+
literal_types.add(lit_type)
920+
item = lit_type
921+
891922
if not self._is_subtype(item, self.orig_right):
892923
return False
924+
893925
return True
894926
return all(self._is_subtype(item, self.orig_right) for item in left.items)
895927

0 commit comments

Comments
 (0)