-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Try simple-minded call expression cache #19505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
a48015a
f2d15c4
c836f53
32827a8
721facc
344a84a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ | |
from mypy.checkmember import analyze_member_access, has_operator | ||
from mypy.checkstrformat import StringFormatterChecker | ||
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars | ||
from mypy.errors import ErrorWatcher, report_internal_error | ||
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error | ||
from mypy.expandtype import ( | ||
expand_type, | ||
expand_type_by_instance, | ||
|
@@ -355,9 +355,15 @@ def __init__( | |
type_state.infer_polymorphic = not self.chk.options.old_type_inference | ||
|
||
self._arg_infer_context_cache = None | ||
self.expr_cache: dict[ | ||
tuple[Expression, Type | None], | ||
tuple[int, Type, list[ErrorInfo], dict[Expression, Type]], | ||
] = {} | ||
self.in_lambda_expr = False | ||
|
||
def reset(self) -> None: | ||
self.resolved_type = {} | ||
self.expr_cache.clear() | ||
|
||
def visit_name_expr(self, e: NameExpr) -> Type: | ||
"""Type check a name expression. | ||
|
@@ -5404,6 +5410,8 @@ def find_typeddict_context( | |
|
||
def visit_lambda_expr(self, e: LambdaExpr) -> Type: | ||
"""Type check lambda expression.""" | ||
old_in_lambda = self.in_lambda_expr | ||
self.in_lambda_expr = True | ||
self.chk.check_default_args(e, body_is_trivial=False) | ||
inferred_type, type_override = self.infer_lambda_type_using_context(e) | ||
if not inferred_type: | ||
|
@@ -5424,6 +5432,7 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type: | |
ret_type = self.accept(e.expr(), allow_none_return=True) | ||
fallback = self.named_type("builtins.function") | ||
self.chk.return_types.pop() | ||
self.in_lambda_expr = old_in_lambda | ||
return callable_type(e, fallback, ret_type) | ||
else: | ||
# Type context available. | ||
|
@@ -5436,6 +5445,7 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type: | |
self.accept(e.expr(), allow_none_return=True) | ||
ret_type = self.chk.lookup_type(e.expr()) | ||
self.chk.return_types.pop() | ||
self.in_lambda_expr = old_in_lambda | ||
return replace_callable_return_type(inferred_type, ret_type) | ||
|
||
def infer_lambda_type_using_context( | ||
|
@@ -5980,6 +5990,22 @@ def accept( | |
typ = self.visit_conditional_expr(node, allow_none_return=True) | ||
elif allow_none_return and isinstance(node, AwaitExpr): | ||
typ = self.visit_await_expr(node, allow_none_return=True) | ||
# Deeply nested generic calls can deteriorate performance dramatically. | ||
# Although in most cases caching makes little difference, in worst case | ||
# it avoids exponential complexity. | ||
# TODO: figure out why caching within lambdas is fragile. | ||
elif isinstance(node, (CallExpr, ListExpr, TupleExpr)) and not ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be difficult to allow dicts and sets here? Inline dictionaries are relatively common and even heavier than lists, and sets just for consistency. Also operator exprs can be really heavy (#14978) and are fundamentally similar to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with dicts/sets is that I see around 0.3% regression on self-check when I add them (but maybe this is just noise). My reasoning is that most code has a bunch of shallow dictionaries, and for those caching is just busy-work that will never be used (note caching is not free, since mypyc is slow on creating local type maps and watchers). Anyway, I am open to considering more expression kinds to cache, but lets put those in separate PR(s). |
||
self.in_lambda_expr or self.chk.current_node_deferred | ||
): | ||
if (node, type_context) in self.expr_cache: | ||
binder_version, typ, messages, type_map = self.expr_cache[(node, type_context)] | ||
if binder_version == self.chk.binder.version: | ||
self.chk.store_types(type_map) | ||
self.msg.add_errors(messages) | ||
else: | ||
typ = self.accept_maybe_cache(node, type_context=type_context) | ||
else: | ||
typ = self.accept_maybe_cache(node, type_context=type_context) | ||
else: | ||
typ = node.accept(self) | ||
except Exception as err: | ||
|
@@ -6010,6 +6036,21 @@ def accept( | |
self.in_expression = False | ||
return result | ||
|
||
def accept_maybe_cache(self, node: Expression, type_context: Type | None = None) -> Type: | ||
binder_version = self.chk.binder.version | ||
# Micro-optimization: inline local_type_map() as it is somewhat slow in mypyc. | ||
type_map: dict[Expression, Type] = {} | ||
self.chk._type_maps.append(type_map) | ||
with self.msg.filter_errors(filter_errors=True, save_filtered_errors=True) as msg: | ||
typ = node.accept(self) | ||
messages = msg.filtered_errors() | ||
if binder_version == self.chk.binder.version and not self.chk.current_node_deferred: | ||
self.expr_cache[(node, type_context)] = (binder_version, typ, messages, type_map) | ||
self.chk._type_maps.pop() | ||
self.chk.store_types(type_map) | ||
self.msg.add_errors(messages) | ||
return typ | ||
|
||
def named_type(self, name: str) -> Instance: | ||
"""Return an instance type with type given by the name and no type | ||
arguments. Alias for TypeChecker.named_type. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -390,7 +390,7 @@ class Errors: | |
# in some cases to avoid reporting huge numbers of errors. | ||
seen_import_error = False | ||
|
||
_watchers: list[ErrorWatcher] = [] | ||
_watchers: list[ErrorWatcher] | ||
|
||
def __init__( | ||
self, | ||
|
@@ -421,6 +421,7 @@ def initialize(self) -> None: | |
self.scope = None | ||
self.target_module = None | ||
self.seen_import_error = False | ||
self._watchers = [] | ||
|
||
def reset(self) -> None: | ||
self.initialize() | ||
|
@@ -931,7 +932,8 @@ def prefer_simple_messages(self) -> bool: | |
if self.file in self.ignored_files: | ||
# Errors ignored, so no point generating fancy messages | ||
return True | ||
for _watcher in self._watchers: | ||
if self._watchers: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain this change? Watchers used to be additive and that sounded reasonable to me... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously, if any of the active watchers was ignoring errors, we could use simpler messages, but in presence of caching this is not valid anymore. For example, we can accept an expression when there is enclosing ignoring watcher, but then the caching watcher will record simple message, and if next time we, by chance, accept same expression in same type context, but without the ignoring watcher, an incorrect (i.e. way too terse) error message will be pulled from the cache. Without this change 6 tests fail because of terse/simplistic error messages are used. |
||
_watcher = self._watchers[-1] | ||
if _watcher._filter is True and _watcher._filtered is None: | ||
# Errors are filtered | ||
return True | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also discovered that in #19408. Accepting a lambda has explicit type context dependency (
infer_lambda_type_using_context
), so generic calls do really require re-accepting the lambda every time, context from outer generic may have propagated later.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, thanks for the pointer. I will update the comment.