Skip to content

Commit b43e0d3

Browse files
authored
Add negative subtype caches (#14884)
A possible solution for #14867 (I just copy everything from positive caches).
1 parent d328c22 commit b43e0d3

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

mypy/subtypes.py

+8
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ def visit_instance(self, left: Instance) -> bool:
444444
if isinstance(right, Instance):
445445
if type_state.is_cached_subtype_check(self._subtype_kind, left, right):
446446
return True
447+
if type_state.is_cached_negative_subtype_check(self._subtype_kind, left, right):
448+
return False
447449
if not self.subtype_context.ignore_promotions:
448450
for base in left.type.mro:
449451
if base._promote and any(
@@ -598,11 +600,17 @@ def check_mixed(
598600
nominal = False
599601
if nominal:
600602
type_state.record_subtype_cache_entry(self._subtype_kind, left, right)
603+
else:
604+
type_state.record_negative_subtype_cache_entry(self._subtype_kind, left, right)
601605
return nominal
602606
if right.type.is_protocol and is_protocol_implementation(
603607
left, right, proper_subtype=self.proper_subtype
604608
):
605609
return True
610+
# We record negative cache entry here, and not in the protocol check like we do for
611+
# positive cache, to avoid accidentally adding a type that is not a structural
612+
# subtype, but is a nominal subtype (involving type: ignore override).
613+
type_state.record_negative_subtype_cache_entry(self._subtype_kind, left, right)
606614
return False
607615
if isinstance(right, TypeType):
608616
item = right.item

mypy/typestate.py

+42
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from mypy.server.trigger import make_trigger
1313
from mypy.types import Instance, Type, TypeVarId, get_proper_type
1414

15+
MAX_NEGATIVE_CACHE_TYPES: Final = 1000
16+
MAX_NEGATIVE_CACHE_ENTRIES: Final = 10000
17+
1518
# Represents that the 'left' instance is a subtype of the 'right' instance
1619
SubtypeRelationship: _TypeAlias = Tuple[Instance, Instance]
1720

@@ -42,6 +45,9 @@ class TypeState:
4245
# We need the caches, since subtype checks for structural types are very slow.
4346
_subtype_caches: Final[SubtypeCache]
4447

48+
# Same as above but for negative subtyping results.
49+
_negative_subtype_caches: Final[SubtypeCache]
50+
4551
# This contains protocol dependencies generated after running a full build,
4652
# or after an update. These dependencies are special because:
4753
# * They are a global property of the program; i.e. some dependencies for imported
@@ -95,6 +101,7 @@ class TypeState:
95101

96102
def __init__(self) -> None:
97103
self._subtype_caches = {}
104+
self._negative_subtype_caches = {}
98105
self.proto_deps = {}
99106
self._attempted_protocols = {}
100107
self._checked_against_members = {}
@@ -128,11 +135,14 @@ def get_assumptions(self, is_proper: bool) -> list[tuple[Type, Type]]:
128135
def reset_all_subtype_caches(self) -> None:
129136
"""Completely reset all known subtype caches."""
130137
self._subtype_caches.clear()
138+
self._negative_subtype_caches.clear()
131139

132140
def reset_subtype_caches_for(self, info: TypeInfo) -> None:
133141
"""Reset subtype caches (if any) for a given supertype TypeInfo."""
134142
if info in self._subtype_caches:
135143
self._subtype_caches[info].clear()
144+
if info in self._negative_subtype_caches:
145+
self._negative_subtype_caches[info].clear()
136146

137147
def reset_all_subtype_caches_for(self, info: TypeInfo) -> None:
138148
"""Reset subtype caches (if any) for a given supertype TypeInfo and its MRO."""
@@ -154,6 +164,23 @@ def is_cached_subtype_check(self, kind: SubtypeKind, left: Instance, right: Inst
154164
return False
155165
return (left, right) in subcache
156166

167+
def is_cached_negative_subtype_check(
168+
self, kind: SubtypeKind, left: Instance, right: Instance
169+
) -> bool:
170+
if left.last_known_value is not None or right.last_known_value is not None:
171+
# If there is a literal last known value, give up. There
172+
# will be an unbounded number of potential types to cache,
173+
# making caching less effective.
174+
return False
175+
info = right.type
176+
cache = self._negative_subtype_caches.get(info)
177+
if cache is None:
178+
return False
179+
subcache = cache.get(kind)
180+
if subcache is None:
181+
return False
182+
return (left, right) in subcache
183+
157184
def record_subtype_cache_entry(
158185
self, kind: SubtypeKind, left: Instance, right: Instance
159186
) -> None:
@@ -164,6 +191,21 @@ def record_subtype_cache_entry(
164191
cache = self._subtype_caches.setdefault(right.type, dict())
165192
cache.setdefault(kind, set()).add((left, right))
166193

194+
def record_negative_subtype_cache_entry(
195+
self, kind: SubtypeKind, left: Instance, right: Instance
196+
) -> None:
197+
if left.last_known_value is not None or right.last_known_value is not None:
198+
# These are unlikely to match, due to the large space of
199+
# possible values. Avoid uselessly increasing cache sizes.
200+
return
201+
if len(self._negative_subtype_caches) > MAX_NEGATIVE_CACHE_TYPES:
202+
self._negative_subtype_caches.clear()
203+
cache = self._negative_subtype_caches.setdefault(right.type, dict())
204+
subcache = cache.setdefault(kind, set())
205+
if len(subcache) > MAX_NEGATIVE_CACHE_ENTRIES:
206+
subcache.clear()
207+
cache.setdefault(kind, set()).add((left, right))
208+
167209
def reset_protocol_deps(self) -> None:
168210
"""Reset dependencies after a full run or before a daemon shutdown."""
169211
self.proto_deps = {}

0 commit comments

Comments
 (0)