Skip to content

Commit

Permalink
Add negative subtype caches (#14884)
Browse files Browse the repository at this point in the history
A possible solution for #14867 (I
just copy everything from positive caches).
  • Loading branch information
ilevkivskyi authored Apr 11, 2023
1 parent d328c22 commit b43e0d3
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 0 deletions.
8 changes: 8 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ def visit_instance(self, left: Instance) -> bool:
if isinstance(right, Instance):
if type_state.is_cached_subtype_check(self._subtype_kind, left, right):
return True
if type_state.is_cached_negative_subtype_check(self._subtype_kind, left, right):
return False
if not self.subtype_context.ignore_promotions:
for base in left.type.mro:
if base._promote and any(
Expand Down Expand Up @@ -598,11 +600,17 @@ def check_mixed(
nominal = False
if nominal:
type_state.record_subtype_cache_entry(self._subtype_kind, left, right)
else:
type_state.record_negative_subtype_cache_entry(self._subtype_kind, left, right)
return nominal
if right.type.is_protocol and is_protocol_implementation(
left, right, proper_subtype=self.proper_subtype
):
return True
# We record negative cache entry here, and not in the protocol check like we do for
# positive cache, to avoid accidentally adding a type that is not a structural
# subtype, but is a nominal subtype (involving type: ignore override).
type_state.record_negative_subtype_cache_entry(self._subtype_kind, left, right)
return False
if isinstance(right, TypeType):
item = right.item
Expand Down
42 changes: 42 additions & 0 deletions mypy/typestate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from mypy.server.trigger import make_trigger
from mypy.types import Instance, Type, TypeVarId, get_proper_type

MAX_NEGATIVE_CACHE_TYPES: Final = 1000
MAX_NEGATIVE_CACHE_ENTRIES: Final = 10000

# Represents that the 'left' instance is a subtype of the 'right' instance
SubtypeRelationship: _TypeAlias = Tuple[Instance, Instance]

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

# Same as above but for negative subtyping results.
_negative_subtype_caches: Final[SubtypeCache]

# This contains protocol dependencies generated after running a full build,
# or after an update. These dependencies are special because:
# * They are a global property of the program; i.e. some dependencies for imported
Expand Down Expand Up @@ -95,6 +101,7 @@ class TypeState:

def __init__(self) -> None:
self._subtype_caches = {}
self._negative_subtype_caches = {}
self.proto_deps = {}
self._attempted_protocols = {}
self._checked_against_members = {}
Expand Down Expand Up @@ -128,11 +135,14 @@ def get_assumptions(self, is_proper: bool) -> list[tuple[Type, Type]]:
def reset_all_subtype_caches(self) -> None:
"""Completely reset all known subtype caches."""
self._subtype_caches.clear()
self._negative_subtype_caches.clear()

def reset_subtype_caches_for(self, info: TypeInfo) -> None:
"""Reset subtype caches (if any) for a given supertype TypeInfo."""
if info in self._subtype_caches:
self._subtype_caches[info].clear()
if info in self._negative_subtype_caches:
self._negative_subtype_caches[info].clear()

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

def is_cached_negative_subtype_check(
self, kind: SubtypeKind, left: Instance, right: Instance
) -> bool:
if left.last_known_value is not None or right.last_known_value is not None:
# If there is a literal last known value, give up. There
# will be an unbounded number of potential types to cache,
# making caching less effective.
return False
info = right.type
cache = self._negative_subtype_caches.get(info)
if cache is None:
return False
subcache = cache.get(kind)
if subcache is None:
return False
return (left, right) in subcache

def record_subtype_cache_entry(
self, kind: SubtypeKind, left: Instance, right: Instance
) -> None:
Expand All @@ -164,6 +191,21 @@ def record_subtype_cache_entry(
cache = self._subtype_caches.setdefault(right.type, dict())
cache.setdefault(kind, set()).add((left, right))

def record_negative_subtype_cache_entry(
self, kind: SubtypeKind, left: Instance, right: Instance
) -> None:
if left.last_known_value is not None or right.last_known_value is not None:
# These are unlikely to match, due to the large space of
# possible values. Avoid uselessly increasing cache sizes.
return
if len(self._negative_subtype_caches) > MAX_NEGATIVE_CACHE_TYPES:
self._negative_subtype_caches.clear()
cache = self._negative_subtype_caches.setdefault(right.type, dict())
subcache = cache.setdefault(kind, set())
if len(subcache) > MAX_NEGATIVE_CACHE_ENTRIES:
subcache.clear()
cache.setdefault(kind, set()).add((left, right))

def reset_protocol_deps(self) -> None:
"""Reset dependencies after a full run or before a daemon shutdown."""
self.proto_deps = {}
Expand Down

0 comments on commit b43e0d3

Please sign in to comment.