Skip to content

Commit 1f8621c

Browse files
authored
subtypes: fast path for Union/Union subtype check (#14277)
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 values. Implement 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 61a21ba commit 1f8621c

File tree

2 files changed

+39
-0
lines changed

2 files changed

+39
-0
lines changed

mypy/subtypes.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
UninhabitedType,
5858
UnionType,
5959
UnpackType,
60+
_flattened,
6061
get_proper_type,
6162
is_named_instance,
6263
)
@@ -891,6 +892,35 @@ def visit_union_type(self, left: UnionType) -> bool:
891892
if not self._is_subtype(item, self.orig_right):
892893
return False
893894
return True
895+
896+
elif isinstance(self.right, UnionType):
897+
# prune literals early to avoid nasty quadratic behavior which would otherwise arise when checking
898+
# subtype relationships between slightly different narrowings of an Enum
899+
# we achieve O(N+M) instead of O(N*M)
900+
901+
fast_check: set[ProperType] = set()
902+
903+
for item in _flattened(self.right.relevant_items()):
904+
p_item = get_proper_type(item)
905+
if isinstance(p_item, LiteralType):
906+
fast_check.add(p_item)
907+
elif isinstance(p_item, Instance):
908+
if p_item.last_known_value is None:
909+
fast_check.add(p_item)
910+
else:
911+
fast_check.add(p_item.last_known_value)
912+
913+
for item in left.relevant_items():
914+
p_item = get_proper_type(item)
915+
if p_item in fast_check:
916+
continue
917+
lit_type = mypy.typeops.simple_literal_type(p_item)
918+
if lit_type in fast_check:
919+
continue
920+
if not self._is_subtype(item, self.orig_right):
921+
return False
922+
return True
923+
894924
return all(self._is_subtype(item, self.orig_right) for item in left.items)
895925

896926
def visit_partial_type(self, left: PartialType) -> bool:

mypy/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3346,6 +3346,15 @@ def has_recursive_types(typ: Type) -> bool:
33463346
return typ.accept(_has_recursive_type)
33473347

33483348

3349+
def _flattened(types: Iterable[Type]) -> Iterable[Type]:
3350+
for t in types:
3351+
tp = get_proper_type(t)
3352+
if isinstance(tp, UnionType):
3353+
yield from _flattened(tp.items)
3354+
else:
3355+
yield t
3356+
3357+
33493358
def flatten_nested_unions(
33503359
types: Iterable[Type], handle_type_alias_type: bool = True
33513360
) -> list[Type]:

0 commit comments

Comments
 (0)