Skip to content

Commit 811b2f1

Browse files
Merge branch 'master' into unwrap-new-types
2 parents f23bfa2 + bd94bcb commit 811b2f1

File tree

12 files changed

+185
-56
lines changed

12 files changed

+185
-56
lines changed

mypy/binder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ def __init__(self, options: Options) -> None:
138138
# flexible inference of variable types (--allow-redefinition-new).
139139
self.bind_all = options.allow_redefinition_new
140140

141+
# This tracks any externally visible changes in binder to invalidate
142+
# expression caches when needed.
143+
self.version = 0
144+
141145
def _get_id(self) -> int:
142146
self.next_id += 1
143147
return self.next_id
@@ -158,6 +162,7 @@ def push_frame(self, conditional_frame: bool = False) -> Frame:
158162
return f
159163

160164
def _put(self, key: Key, type: Type, from_assignment: bool, index: int = -1) -> None:
165+
self.version += 1
161166
self.frames[index].types[key] = CurrentType(type, from_assignment)
162167

163168
def _get(self, key: Key, index: int = -1) -> CurrentType | None:
@@ -185,6 +190,7 @@ def put(self, expr: Expression, typ: Type, *, from_assignment: bool = True) -> N
185190
self._put(key, typ, from_assignment)
186191

187192
def unreachable(self) -> None:
193+
self.version += 1
188194
self.frames[-1].unreachable = True
189195

190196
def suppress_unreachable_warnings(self) -> None:

mypy/build.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def default_flush_errors(
194194
result.errors = messages
195195
return result
196196
except CompileError as e:
197-
# CompileErrors raised from an errors object carry all of the
197+
# CompileErrors raised from an errors object carry all the
198198
# messages that have not been reported out by error streaming.
199199
# Patch it up to contain either none or all none of the messages,
200200
# depending on whether we are flushing errors.
@@ -802,11 +802,11 @@ def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
802802
res.append((pri, sub_id, imp.line))
803803
else:
804804
all_are_submodules = False
805-
# Add cur_id as a dependency, even if all of the
805+
# Add cur_id as a dependency, even if all the
806806
# imports are submodules. Processing import from will try
807807
# to look through cur_id, so we should depend on it.
808-
# As a workaround for for some bugs in cycle handling (#4498),
809-
# if all of the imports are submodules, do the import at a lower
808+
# As a workaround for some bugs in cycle handling (#4498),
809+
# if all the imports are submodules, do the import at a lower
810810
# priority.
811811
pri = import_priority(imp, PRI_HIGH if not all_are_submodules else PRI_LOW)
812812
res.append((pri, cur_id, imp.line))
@@ -929,7 +929,7 @@ def write_deps_cache(
929929
) -> None:
930930
"""Write cache files for fine-grained dependencies.
931931
932-
Serialize fine-grained dependencies map for fine grained mode.
932+
Serialize fine-grained dependencies map for fine-grained mode.
933933
934934
Dependencies on some module 'm' is stored in the dependency cache
935935
file m.deps.json. This entails some spooky action at a distance:
@@ -943,7 +943,7 @@ def write_deps_cache(
943943
fine-grained dependencies in a global cache file:
944944
* We take a snapshot of current sources to later check consistency
945945
between the fine-grained dependency cache and module cache metadata
946-
* We store the mtime of all of the dependency files to verify they
946+
* We store the mtime of all the dependency files to verify they
947947
haven't changed
948948
"""
949949
metastore = manager.metastore
@@ -1111,7 +1111,7 @@ def read_deps_cache(manager: BuildManager, graph: Graph) -> dict[str, FgDepMeta]
11111111
if deps_meta is None:
11121112
return None
11131113
meta_snapshot = deps_meta["snapshot"]
1114-
# Take a snapshot of the source hashes from all of the metas we found.
1114+
# Take a snapshot of the source hashes from all the metas we found.
11151115
# (Including the ones we rejected because they were out of date.)
11161116
# We use this to verify that they match up with the proto_deps.
11171117
current_meta_snapshot = {

mypy/checker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,6 @@ def reset(self) -> None:
449449
self.binder = ConditionalTypeBinder(self.options)
450450
self._type_maps[1:] = []
451451
self._type_maps[0].clear()
452-
self.temp_type_map = None
453452
self.expr_checker.reset()
454453
self.deferred_nodes = []
455454
self.partial_types = []
@@ -3024,6 +3023,8 @@ def visit_block(self, b: Block) -> None:
30243023
break
30253024
else:
30263025
self.accept(s)
3026+
# Clear expression cache after each statement to avoid unlimited growth.
3027+
self.expr_checker.expr_cache.clear()
30273028

30283029
def should_report_unreachable_issues(self) -> bool:
30293030
return (
@@ -4005,7 +4006,7 @@ def check_multi_assignment_from_union(
40054006
for t, lv in zip(transposed, self.flatten_lvalues(lvalues)):
40064007
# We can access _type_maps directly since temporary type maps are
40074008
# only created within expressions.
4008-
t.append(self._type_maps[0].pop(lv, AnyType(TypeOfAny.special_form)))
4009+
t.append(self._type_maps[-1].pop(lv, AnyType(TypeOfAny.special_form)))
40094010
union_types = tuple(make_simplified_union(col) for col in transposed)
40104011
for expr, items in assignments.items():
40114012
# Bind a union of types collected in 'assignments' to every expression.
@@ -4664,6 +4665,8 @@ def replace_partial_type(
46644665
) -> None:
46654666
"""Replace the partial type of var with a non-partial type."""
46664667
var.type = new_type
4668+
# Updating a partial type should invalidate expression caches.
4669+
self.binder.version += 1
46674670
del partial_types[var]
46684671
if self.options.allow_redefinition_new:
46694672
# When using --allow-redefinition-new, binder tracks all types of

mypy/checkexpr.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from mypy.checkmember import analyze_member_access, has_operator
2020
from mypy.checkstrformat import StringFormatterChecker
2121
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
22-
from mypy.errors import ErrorWatcher, report_internal_error
22+
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error
2323
from mypy.expandtype import (
2424
expand_type,
2525
expand_type_by_instance,
@@ -355,9 +355,15 @@ def __init__(
355355
type_state.infer_polymorphic = not self.chk.options.old_type_inference
356356

357357
self._arg_infer_context_cache = None
358+
self.expr_cache: dict[
359+
tuple[Expression, Type | None],
360+
tuple[int, Type, list[ErrorInfo], dict[Expression, Type]],
361+
] = {}
362+
self.in_lambda_expr = False
358363

359364
def reset(self) -> None:
360365
self.resolved_type = {}
366+
self.expr_cache.clear()
361367

362368
def visit_name_expr(self, e: NameExpr) -> Type:
363369
"""Type check a name expression.
@@ -5402,6 +5408,8 @@ def find_typeddict_context(
54025408

54035409
def visit_lambda_expr(self, e: LambdaExpr) -> Type:
54045410
"""Type check lambda expression."""
5411+
old_in_lambda = self.in_lambda_expr
5412+
self.in_lambda_expr = True
54055413
self.chk.check_default_args(e, body_is_trivial=False)
54065414
inferred_type, type_override = self.infer_lambda_type_using_context(e)
54075415
if not inferred_type:
@@ -5422,6 +5430,7 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type:
54225430
ret_type = self.accept(e.expr(), allow_none_return=True)
54235431
fallback = self.named_type("builtins.function")
54245432
self.chk.return_types.pop()
5433+
self.in_lambda_expr = old_in_lambda
54255434
return callable_type(e, fallback, ret_type)
54265435
else:
54275436
# Type context available.
@@ -5434,6 +5443,7 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type:
54345443
self.accept(e.expr(), allow_none_return=True)
54355444
ret_type = self.chk.lookup_type(e.expr())
54365445
self.chk.return_types.pop()
5446+
self.in_lambda_expr = old_in_lambda
54375447
return replace_callable_return_type(inferred_type, ret_type)
54385448

54395449
def infer_lambda_type_using_context(
@@ -5978,6 +5988,24 @@ def accept(
59785988
typ = self.visit_conditional_expr(node, allow_none_return=True)
59795989
elif allow_none_return and isinstance(node, AwaitExpr):
59805990
typ = self.visit_await_expr(node, allow_none_return=True)
5991+
# Deeply nested generic calls can deteriorate performance dramatically.
5992+
# Although in most cases caching makes little difference, in worst case
5993+
# it avoids exponential complexity.
5994+
# We cannot use cache inside lambdas, because they skip immediate type
5995+
# context, and use enclosing one, see infer_lambda_type_using_context().
5996+
# TODO: consider using cache for more expression kinds.
5997+
elif isinstance(node, (CallExpr, ListExpr, TupleExpr)) and not (
5998+
self.in_lambda_expr or self.chk.current_node_deferred
5999+
):
6000+
if (node, type_context) in self.expr_cache:
6001+
binder_version, typ, messages, type_map = self.expr_cache[(node, type_context)]
6002+
if binder_version == self.chk.binder.version:
6003+
self.chk.store_types(type_map)
6004+
self.msg.add_errors(messages)
6005+
else:
6006+
typ = self.accept_maybe_cache(node, type_context=type_context)
6007+
else:
6008+
typ = self.accept_maybe_cache(node, type_context=type_context)
59816009
else:
59826010
typ = node.accept(self)
59836011
except Exception as err:
@@ -6008,6 +6036,21 @@ def accept(
60086036
self.in_expression = False
60096037
return result
60106038

6039+
def accept_maybe_cache(self, node: Expression, type_context: Type | None = None) -> Type:
6040+
binder_version = self.chk.binder.version
6041+
# Micro-optimization: inline local_type_map() as it is somewhat slow in mypyc.
6042+
type_map: dict[Expression, Type] = {}
6043+
self.chk._type_maps.append(type_map)
6044+
with self.msg.filter_errors(filter_errors=True, save_filtered_errors=True) as msg:
6045+
typ = node.accept(self)
6046+
messages = msg.filtered_errors()
6047+
if binder_version == self.chk.binder.version and not self.chk.current_node_deferred:
6048+
self.expr_cache[(node, type_context)] = (binder_version, typ, messages, type_map)
6049+
self.chk._type_maps.pop()
6050+
self.chk.store_types(type_map)
6051+
self.msg.add_errors(messages)
6052+
return typ
6053+
60116054
def named_type(self, name: str) -> Instance:
60126055
"""Return an instance type with type given by the name and no type
60136056
arguments. Alias for TypeChecker.named_type.

mypy/errors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ class Errors:
390390
# in some cases to avoid reporting huge numbers of errors.
391391
seen_import_error = False
392392

393-
_watchers: list[ErrorWatcher] = []
393+
_watchers: list[ErrorWatcher]
394394

395395
def __init__(
396396
self,
@@ -421,6 +421,7 @@ def initialize(self) -> None:
421421
self.scope = None
422422
self.target_module = None
423423
self.seen_import_error = False
424+
self._watchers = []
424425

425426
def reset(self) -> None:
426427
self.initialize()
@@ -931,7 +932,8 @@ def prefer_simple_messages(self) -> bool:
931932
if self.file in self.ignored_files:
932933
# Errors ignored, so no point generating fancy messages
933934
return True
934-
for _watcher in self._watchers:
935+
if self._watchers:
936+
_watcher = self._watchers[-1]
935937
if _watcher._filter is True and _watcher._filtered is None:
936938
# Errors are filtered
937939
return True

mypyc/build.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,12 @@ def build_using_shared_lib(
270270
) -> list[Extension]:
271271
"""Produce the list of extension modules when a shared library is needed.
272272
273-
This creates one shared library extension module that all of the
274-
others import and then one shim extension module for each
275-
module in the build, that simply calls an initialization function
273+
This creates one shared library extension module that all the
274+
others import, and one shim extension module for each
275+
module in the build. Each shim simply calls an initialization function
276276
in the shared library.
277277
278-
The shared library (which lib_name is the name of) is a python
278+
The shared library (which lib_name is the name of) is a Python
279279
extension module that exports the real initialization functions in
280280
Capsules stored in module attributes.
281281
"""
@@ -511,7 +511,7 @@ def mypycify(
511511
separate: Should compiled modules be placed in separate extension modules.
512512
If False, all modules are placed in a single shared library.
513513
If True, every module is placed in its own library.
514-
Otherwise separate should be a list of
514+
Otherwise, separate should be a list of
515515
(file name list, optional shared library name) pairs specifying
516516
groups of files that should be placed in the same shared library
517517
(while all other modules will be placed in its own library).

0 commit comments

Comments
 (0)