Skip to content

Commit 703bee9

Browse files
authored
perf: deduplicate fast_container_type and fast_dict_type items before joining (#19409)
Vastly improves #14718. Some type joins are really heavy - especially joins between overloads. This does not fully remove the problem, a collection of different pairwise equivalent overloads differing only in their names is still slow to check, but at least we will not join every such monster callable with itself multiple times.
1 parent ad76e16 commit 703bee9

File tree

4 files changed

+68
-19
lines changed

4 files changed

+68
-19
lines changed

mypy/checkexpr.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5069,31 +5069,56 @@ def fast_container_type(
50695069
module-level constant definitions.
50705070
50715071
Limitations:
5072+
50725073
- no active type context
5074+
- at least one item
50735075
- no star expressions
5074-
- the joined type of all entries must be an Instance or Tuple type
5076+
- not after deferral
5077+
- either exactly one distinct type inside,
5078+
or the joined type of all entries is an Instance or Tuple type,
50755079
"""
50765080
ctx = self.type_context[-1]
5077-
if ctx:
5081+
if ctx or not e.items:
5082+
return None
5083+
if self.chk.current_node_deferred:
5084+
# Guarantees that all items will be Any, we'll reject it anyway.
50785085
return None
50795086
rt = self.resolved_type.get(e, None)
50805087
if rt is not None:
50815088
return rt if isinstance(rt, Instance) else None
50825089
values: list[Type] = []
5090+
# Preserve join order while avoiding O(n) lookups at every iteration
5091+
values_set: set[Type] = set()
50835092
for item in e.items:
50845093
if isinstance(item, StarExpr):
50855094
# fallback to slow path
50865095
self.resolved_type[e] = NoneType()
50875096
return None
5088-
values.append(self.accept(item))
5089-
vt = join.join_type_list(values)
5090-
if not allow_fast_container_literal(vt):
5097+
5098+
typ = self.accept(item)
5099+
if typ not in values_set:
5100+
values.append(typ)
5101+
values_set.add(typ)
5102+
5103+
vt = self._first_or_join_fast_item(values)
5104+
if vt is None:
50915105
self.resolved_type[e] = NoneType()
50925106
return None
50935107
ct = self.chk.named_generic_type(container_fullname, [vt])
50945108
self.resolved_type[e] = ct
50955109
return ct
50965110

5111+
def _first_or_join_fast_item(self, items: list[Type]) -> Type | None:
5112+
if len(items) == 1 and not self.chk.current_node_deferred:
5113+
return items[0]
5114+
typ = join.join_type_list(items)
5115+
if not allow_fast_container_literal(typ):
5116+
# TODO: This is overly strict, many other types can be joined safely here.
5117+
# However, our join implementation isn't bug-free, and some joins may produce
5118+
# undesired `Any`s or even more surprising results.
5119+
return None
5120+
return typ
5121+
50975122
def check_lst_expr(self, e: ListExpr | SetExpr | TupleExpr, fullname: str, tag: str) -> Type:
50985123
# fast path
50995124
t = self.fast_container_type(e, fullname)
@@ -5254,18 +5279,30 @@ def fast_dict_type(self, e: DictExpr) -> Type | None:
52545279
module-level constant definitions.
52555280
52565281
Limitations:
5282+
52575283
- no active type context
5284+
- at least one item
52585285
- only supported star expressions are other dict instances
5259-
- the joined types of all keys and values must be Instance or Tuple types
5286+
- either exactly one distinct type (keys and values separately) inside,
5287+
or the joined type of all entries is an Instance or Tuple type
52605288
"""
52615289
ctx = self.type_context[-1]
5262-
if ctx:
5290+
if ctx or not e.items:
52635291
return None
5292+
5293+
if self.chk.current_node_deferred:
5294+
# Guarantees that all items will be Any, we'll reject it anyway.
5295+
return None
5296+
52645297
rt = self.resolved_type.get(e, None)
52655298
if rt is not None:
52665299
return rt if isinstance(rt, Instance) else None
5300+
52675301
keys: list[Type] = []
52685302
values: list[Type] = []
5303+
# Preserve join order while avoiding O(n) lookups at every iteration
5304+
keys_set: set[Type] = set()
5305+
values_set: set[Type] = set()
52695306
stargs: tuple[Type, Type] | None = None
52705307
for key, value in e.items:
52715308
if key is None:
@@ -5280,13 +5317,25 @@ def fast_dict_type(self, e: DictExpr) -> Type | None:
52805317
self.resolved_type[e] = NoneType()
52815318
return None
52825319
else:
5283-
keys.append(self.accept(key))
5284-
values.append(self.accept(value))
5285-
kt = join.join_type_list(keys)
5286-
vt = join.join_type_list(values)
5287-
if not (allow_fast_container_literal(kt) and allow_fast_container_literal(vt)):
5320+
key_t = self.accept(key)
5321+
if key_t not in keys_set:
5322+
keys.append(key_t)
5323+
keys_set.add(key_t)
5324+
value_t = self.accept(value)
5325+
if value_t not in values_set:
5326+
values.append(value_t)
5327+
values_set.add(value_t)
5328+
5329+
kt = self._first_or_join_fast_item(keys)
5330+
if kt is None:
52885331
self.resolved_type[e] = NoneType()
52895332
return None
5333+
5334+
vt = self._first_or_join_fast_item(values)
5335+
if vt is None:
5336+
self.resolved_type[e] = NoneType()
5337+
return None
5338+
52905339
if stargs and (stargs[0] != kt or stargs[1] != vt):
52915340
self.resolved_type[e] = NoneType()
52925341
return None

test-data/unit/check-generics.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2929,8 +2929,8 @@ def mix(fs: List[Callable[[S], T]]) -> Callable[[S], List[T]]:
29292929
def id(__x: U) -> U:
29302930
...
29312931
fs = [id, id, id]
2932-
reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`7) -> builtins.list[S`7]"
2933-
reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`9) -> builtins.list[S`9]"
2932+
reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`2) -> builtins.list[S`2]"
2933+
reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`4) -> builtins.list[S`4]"
29342934
[builtins fixtures/list.pyi]
29352935

29362936
[case testInferenceAgainstGenericCurry]
@@ -3118,11 +3118,11 @@ def dec4_bound(f: Callable[[I], List[T]]) -> Callable[[I], T]:
31183118
reveal_type(dec1(lambda x: x)) # N: Revealed type is "def [T] (T`3) -> builtins.list[T`3]"
31193119
reveal_type(dec2(lambda x: x)) # N: Revealed type is "def [S] (S`5) -> builtins.list[S`5]"
31203120
reveal_type(dec3(lambda x: x[0])) # N: Revealed type is "def [S] (S`8) -> S`8"
3121-
reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`12) -> S`12"
3121+
reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`11) -> S`11"
31223122
reveal_type(dec1(lambda x: 1)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]"
31233123
reveal_type(dec5(lambda x: x)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]"
3124-
reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`20) -> builtins.list[S`20]"
3125-
reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`24]) -> T`24"
3124+
reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`19) -> builtins.list[S`19]"
3125+
reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`23]) -> T`23"
31263126
dec4_bound(lambda x: x) # E: Value of type variable "I" of "dec4_bound" cannot be "list[T]"
31273127
[builtins fixtures/list.pyi]
31283128

test-data/unit/check-redefine2.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ def f() -> None:
10731073
while int():
10741074
x = [x]
10751075

1076-
reveal_type(x) # N: Revealed type is "Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]]]]]"
1076+
reveal_type(x) # N: Revealed type is "Union[Any, builtins.list[Any]]"
10771077

10781078
[case testNewRedefinePartialNoneEmptyList]
10791079
# flags: --allow-redefinition-new --local-partial-types

test-data/unit/check-selftype.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2018,7 +2018,7 @@ class Ben(Object):
20182018
}
20192019
@classmethod
20202020
def doit(cls) -> Foo:
2021-
reveal_type(cls.MY_MAP) # N: Revealed type is "builtins.dict[builtins.str, def [Self <: __main__.Foo] (self: Self`4) -> Self`4]"
2021+
reveal_type(cls.MY_MAP) # N: Revealed type is "builtins.dict[builtins.str, def [Self <: __main__.Foo] (self: Self`1) -> Self`1]"
20222022
foo_method = cls.MY_MAP["foo"]
20232023
return foo_method(Foo())
20242024
[builtins fixtures/isinstancelist.pyi]

0 commit comments

Comments
 (0)