Skip to content

Commit f09aa57

Browse files
authored
Try some aliases speed-up (#19810)
This makes self-check almost 2% faster on my desktop (Python 3.12, compiled -O2). Inspired by a slight regression in #19798 I decided to re-think how we detect/label the recursive types. The new algorithm is not 100% equivalent to old one, but should be much faster. The main semantic difference is this: ```python A = list[B1] B1 = list[B2] B2 = list[B1] ``` previously all three aliases where labeled as recursive, now only last two are. Which is kind of correct if you think about it for some time, there is nothing genuinely recursive in `A` by itself. As a result: * We have somewhat more verbose `reveal_type()` for recursive types after fine-grained increments. Excessive use of `get_proper_type()` in the daemon code is a known issue. I will take a look at it when I will have a chance. * I cleaned up some of relevant visitors to be more consistent with recursive aliases. * I also do couple cleanups/speedups in the type queries while I am at it. If there are no comments/objections, I will merge it later today. Then I will merge #19798, and then _maybe_ an equivalent optimization for recursive instances like `class str(Sequence[str]): ...`
1 parent 9edd29a commit f09aa57

File tree

11 files changed

+80
-121
lines changed

11 files changed

+80
-121
lines changed

mypy/constraints.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ArgKind,
2222
TypeInfo,
2323
)
24+
from mypy.type_visitor import ALL_STRATEGY, BoolTypeQuery
2425
from mypy.types import (
2526
TUPLE_LIKE_INSTANCE_NAMES,
2627
AnyType,
@@ -41,7 +42,6 @@
4142
TypeAliasType,
4243
TypedDictType,
4344
TypeOfAny,
44-
TypeQuery,
4545
TypeType,
4646
TypeVarId,
4747
TypeVarLikeType,
@@ -670,9 +670,9 @@ def is_complete_type(typ: Type) -> bool:
670670
return typ.accept(CompleteTypeVisitor())
671671

672672

673-
class CompleteTypeVisitor(TypeQuery[bool]):
673+
class CompleteTypeVisitor(BoolTypeQuery):
674674
def __init__(self) -> None:
675-
super().__init__(all)
675+
super().__init__(ALL_STRATEGY)
676676

677677
def visit_uninhabited_type(self, t: UninhabitedType) -> bool:
678678
return False

mypy/indirection.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ def find_modules(self, typs: Iterable[types.Type]) -> set[str]:
3939
def _visit(self, typ: types.Type) -> None:
4040
if isinstance(typ, types.TypeAliasType):
4141
# Avoid infinite recursion for recursive type aliases.
42-
if typ not in self.seen_aliases:
43-
self.seen_aliases.add(typ)
42+
self.seen_aliases.add(typ)
4443
typ.accept(self)
4544

4645
def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None:

mypy/mixedtraverser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def visit_class_def(self, o: ClassDef, /) -> None:
4747
if info:
4848
for base in info.bases:
4949
base.accept(self)
50+
if info.special_alias:
51+
info.special_alias.accept(self)
5052

5153
def visit_type_alias_expr(self, o: TypeAliasExpr, /) -> None:
5254
super().visit_type_alias_expr(o)

mypy/semanal_typeargs.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,11 @@ def visit_block(self, o: Block) -> None:
8383

8484
def visit_type_alias_type(self, t: TypeAliasType) -> None:
8585
super().visit_type_alias_type(t)
86-
if t in self.seen_aliases:
87-
# Avoid infinite recursion on recursive type aliases.
88-
# Note: it is fine to skip the aliases we have already seen in non-recursive
89-
# types, since errors there have already been reported.
90-
return
91-
self.seen_aliases.add(t)
86+
if t.is_recursive:
87+
if t in self.seen_aliases:
88+
# Avoid infinite recursion on recursive type aliases.
89+
return
90+
self.seen_aliases.add(t)
9291
assert t.alias is not None, f"Unfixed type alias {t.type_ref}"
9392
is_error, is_invalid = self.validate_args(
9493
t.alias.name, tuple(t.args), t.alias.alias_tvars, t
@@ -101,9 +100,12 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None:
101100
if not is_error:
102101
# If there was already an error for the alias itself, there is no point in checking
103102
# the expansion, most likely it will result in the same kind of error.
104-
get_proper_type(t).accept(self)
105-
if t.alias is not None:
106-
t.alias.accept(self)
103+
if t.args:
104+
# Since we always allow unbounded type variables in alias definitions, we need
105+
# to verify the arguments satisfy the upper bounds of the expansion as well.
106+
get_proper_type(t).accept(self)
107+
if t.is_recursive:
108+
self.seen_aliases.discard(t)
107109

108110
def visit_tuple_type(self, t: TupleType) -> None:
109111
t.items = flatten_nested_tuples(t.items)

mypy/stats.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
YieldFromExpr,
4444
)
4545
from mypy.traverser import TraverserVisitor
46+
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
4647
from mypy.typeanal import collect_all_inner_types
4748
from mypy.types import (
4849
AnyType,
@@ -52,7 +53,6 @@
5253
TupleType,
5354
Type,
5455
TypeOfAny,
55-
TypeQuery,
5656
TypeVarType,
5757
get_proper_type,
5858
get_proper_types,
@@ -453,9 +453,9 @@ def is_imprecise(t: Type) -> bool:
453453
return t.accept(HasAnyQuery())
454454

455455

456-
class HasAnyQuery(TypeQuery[bool]):
456+
class HasAnyQuery(BoolTypeQuery):
457457
def __init__(self) -> None:
458-
super().__init__(any)
458+
super().__init__(ANY_STRATEGY)
459459

460460
def visit_any(self, t: AnyType) -> bool:
461461
return not is_special_form_any(t)

mypy/test/testtypes.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,6 @@ def test_type_alias_expand_once(self) -> None:
201201
assert get_proper_type(A) == target
202202
assert get_proper_type(target) == target
203203

204-
def test_type_alias_expand_all(self) -> None:
205-
A, _ = self.fx.def_alias_1(self.fx.a)
206-
assert A.expand_all_if_possible() is None
207-
A, _ = self.fx.def_alias_2(self.fx.a)
208-
assert A.expand_all_if_possible() is None
209-
210-
B = self.fx.non_rec_alias(self.fx.a)
211-
C = self.fx.non_rec_alias(TupleType([B, B], Instance(self.fx.std_tuplei, [B])))
212-
assert C.expand_all_if_possible() == TupleType(
213-
[self.fx.a, self.fx.a], Instance(self.fx.std_tuplei, [self.fx.a])
214-
)
215-
216204
def test_recursive_nested_in_non_recursive(self) -> None:
217205
A, _ = self.fx.def_alias_1(self.fx.a)
218206
T = TypeVarType(

mypy/type_visitor.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from abc import abstractmethod
1717
from collections.abc import Iterable, Sequence
18-
from typing import Any, Callable, Final, Generic, TypeVar, cast
18+
from typing import Any, Final, Generic, TypeVar, cast
1919

2020
from mypy_extensions import mypyc_attr, trait
2121

@@ -353,16 +353,19 @@ class TypeQuery(SyntheticTypeVisitor[T]):
353353
# TODO: check that we don't have existing violations of this rule.
354354
"""
355355

356-
def __init__(self, strategy: Callable[[list[T]], T]) -> None:
357-
self.strategy = strategy
356+
def __init__(self) -> None:
358357
# Keep track of the type aliases already visited. This is needed to avoid
359358
# infinite recursion on types like A = Union[int, List[A]].
360-
self.seen_aliases: set[TypeAliasType] = set()
359+
self.seen_aliases: set[TypeAliasType] | None = None
361360
# By default, we eagerly expand type aliases, and query also types in the
362361
# alias target. In most cases this is a desired behavior, but we may want
363362
# to skip targets in some cases (e.g. when collecting type variables).
364363
self.skip_alias_target = False
365364

365+
@abstractmethod
366+
def strategy(self, items: list[T]) -> T:
367+
raise NotImplementedError
368+
366369
def visit_unbound_type(self, t: UnboundType, /) -> T:
367370
return self.query_types(t.args)
368371

@@ -440,14 +443,15 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> T:
440443
return self.query_types(t.args)
441444

442445
def visit_type_alias_type(self, t: TypeAliasType, /) -> T:
443-
# Skip type aliases already visited types to avoid infinite recursion.
444-
# TODO: Ideally we should fire subvisitors here (or use caching) if we care
445-
# about duplicates.
446-
if t in self.seen_aliases:
447-
return self.strategy([])
448-
self.seen_aliases.add(t)
449446
if self.skip_alias_target:
450447
return self.query_types(t.args)
448+
# Skip type aliases already visited types to avoid infinite recursion
449+
# (also use this as a simple-minded cache).
450+
if self.seen_aliases is None:
451+
self.seen_aliases = set()
452+
elif t in self.seen_aliases:
453+
return self.strategy([])
454+
self.seen_aliases.add(t)
451455
return get_proper_type(t).accept(self)
452456

453457
def query_types(self, types: Iterable[Type]) -> T:
@@ -580,16 +584,15 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> bool:
580584
return self.query_types(t.args)
581585

582586
def visit_type_alias_type(self, t: TypeAliasType, /) -> bool:
583-
# Skip type aliases already visited types to avoid infinite recursion.
584-
# TODO: Ideally we should fire subvisitors here (or use caching) if we care
585-
# about duplicates.
587+
if self.skip_alias_target:
588+
return self.query_types(t.args)
589+
# Skip type aliases already visited types to avoid infinite recursion
590+
# (also use this as a simple-minded cache).
586591
if self.seen_aliases is None:
587592
self.seen_aliases = set()
588593
elif t in self.seen_aliases:
589594
return self.default
590595
self.seen_aliases.add(t)
591-
if self.skip_alias_target:
592-
return self.query_types(t.args)
593596
return get_proper_type(t).accept(self)
594597

595598
def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool:

mypy/typeanal.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,9 +2377,9 @@ def has_explicit_any(t: Type) -> bool:
23772377
return t.accept(HasExplicitAny())
23782378

23792379

2380-
class HasExplicitAny(TypeQuery[bool]):
2380+
class HasExplicitAny(BoolTypeQuery):
23812381
def __init__(self) -> None:
2382-
super().__init__(any)
2382+
super().__init__(ANY_STRATEGY)
23832383

23842384
def visit_any(self, t: AnyType) -> bool:
23852385
return t.type_of_any == TypeOfAny.explicit
@@ -2418,15 +2418,11 @@ def collect_all_inner_types(t: Type) -> list[Type]:
24182418

24192419

24202420
class CollectAllInnerTypesQuery(TypeQuery[list[Type]]):
2421-
def __init__(self) -> None:
2422-
super().__init__(self.combine_lists_strategy)
2423-
24242421
def query_types(self, types: Iterable[Type]) -> list[Type]:
24252422
return self.strategy([t.accept(self) for t in types]) + list(types)
24262423

2427-
@classmethod
2428-
def combine_lists_strategy(cls, it: Iterable[list[Type]]) -> list[Type]:
2429-
return list(itertools.chain.from_iterable(it))
2424+
def strategy(self, items: Iterable[list[Type]]) -> list[Type]:
2425+
return list(itertools.chain.from_iterable(items))
24302426

24312427

24322428
def make_optional_type(t: Type) -> Type:
@@ -2556,7 +2552,6 @@ def __init__(self, api: SemanticAnalyzerCoreInterface, scope: TypeVarLikeScope)
25562552
self.scope = scope
25572553
self.type_var_likes: list[tuple[str, TypeVarLikeExpr]] = []
25582554
self.has_self_type = False
2559-
self.seen_aliases: set[TypeAliasType] | None = None
25602555
self.include_callables = True
25612556

25622557
def _seems_like_callable(self, type: UnboundType) -> bool:
@@ -2653,7 +2648,8 @@ def visit_union_type(self, t: UnionType) -> None:
26532648
self.process_types(t.items)
26542649

26552650
def visit_overloaded(self, t: Overloaded) -> None:
2656-
self.process_types(t.items) # type: ignore[arg-type]
2651+
for it in t.items:
2652+
it.accept(self)
26572653

26582654
def visit_type_type(self, t: TypeType) -> None:
26592655
t.item.accept(self)
@@ -2665,12 +2661,6 @@ def visit_placeholder_type(self, t: PlaceholderType) -> None:
26652661
return self.process_types(t.args)
26662662

26672663
def visit_type_alias_type(self, t: TypeAliasType) -> None:
2668-
# Skip type aliases in already visited types to avoid infinite recursion.
2669-
if self.seen_aliases is None:
2670-
self.seen_aliases = set()
2671-
elif t in self.seen_aliases:
2672-
return
2673-
self.seen_aliases.add(t)
26742664
self.process_types(t.args)
26752665

26762666
def process_types(self, types: list[Type] | tuple[Type, ...]) -> None:

mypy/typeops.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,12 +1114,12 @@ def get_all_type_vars(tp: Type) -> list[TypeVarLikeType]:
11141114

11151115
class TypeVarExtractor(TypeQuery[list[TypeVarLikeType]]):
11161116
def __init__(self, include_all: bool = False) -> None:
1117-
super().__init__(self._merge)
1117+
super().__init__()
11181118
self.include_all = include_all
11191119

1120-
def _merge(self, iter: Iterable[list[TypeVarLikeType]]) -> list[TypeVarLikeType]:
1120+
def strategy(self, items: Iterable[list[TypeVarLikeType]]) -> list[TypeVarLikeType]:
11211121
out = []
1122-
for item in iter:
1122+
for item in items:
11231123
out.extend(item)
11241124
return out
11251125

0 commit comments

Comments
 (0)