Skip to content

Commit 1f0741b

Browse files
committed
speedup typechecking of nested if expressions
Deeply nested if/else expressions have a worst-case exponential behavior. This will for instance manifest when returning literal values which cause repeated analysis of conditional branches with subtly different type context for each literal. This can be optimized by observing that a simple literal context will yield the same analysis as its fallback type, and likewise, two literals of the same fallback type will yield the same analysis. In those case we can avoid the repeated analysis and prevent the worst-case exponential behavior. Fixes #9591
1 parent ce6d243 commit 1f0741b

File tree

2 files changed

+22
-5
lines changed

2 files changed

+22
-5
lines changed

mypy/checkexpr.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
try_expanding_sum_type_to_union, tuple_fallback, make_simplified_union,
7070
true_only, false_only, erase_to_union_or_bound, function_type,
7171
callable_type, try_getting_str_literals, custom_special_method,
72-
is_literal_type_like,
72+
is_literal_type_like, simple_literal_type,
7373
)
7474
from mypy.message_registry import ErrorMessage
7575
import mypy.errorcodes as codes
@@ -3853,26 +3853,43 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F
38533853
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx,
38543854
allow_none_return=allow_none_return)
38553855

3856+
# we want to keep the narrowest value of if_type for union'ing the branches
3857+
# however, it would be silly to pass a literal as a type context. Pass the
3858+
# underlying fallback type instead.
3859+
if_type_fallback = simple_literal_type(get_proper_type(if_type)) or if_type
3860+
38563861
# Analyze the right branch using full type context and store the type
38573862
full_context_else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx,
38583863
allow_none_return=allow_none_return)
3864+
38593865
if not mypy.checker.is_valid_inferred_type(if_type):
38603866
# Analyze the right branch disregarding the left branch.
38613867
else_type = full_context_else_type
3868+
# we want to keep the narrowest value of else_type for union'ing the branches
3869+
# however, it would be silly to pass a literal as a type context. Pass the
3870+
# underlying fallback type instead.
3871+
else_type_fallback = simple_literal_type(get_proper_type(else_type)) or else_type
38623872

38633873
# If it would make a difference, re-analyze the left
38643874
# branch using the right branch's type as context.
3865-
if ctx is None or not is_equivalent(else_type, ctx):
3875+
if ctx is None or not is_equivalent(else_type_fallback, ctx):
38663876
# TODO: If it's possible that the previous analysis of
38673877
# the left branch produced errors that are avoided
38683878
# using this context, suppress those errors.
3869-
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type,
3879+
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type_fallback,
38703880
allow_none_return=allow_none_return)
38713881

3882+
elif if_type_fallback == ctx:
3883+
# There is no point re-running the analysis if if_type is equal to ctx.
3884+
# That would be an exact duplicate of the work we just did.
3885+
# This optimization is particularly important to avoid exponential blowup with nested
3886+
# if/else expressions: https://github.com/python/mypy/issues/9591
3887+
# TODO: would checking for is_proper_subtype also work and cover more cases?
3888+
else_type = full_context_else_type
38723889
else:
38733890
# Analyze the right branch in the context of the left
38743891
# branch's type.
3875-
else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type,
3892+
else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type_fallback,
38763893
allow_none_return=allow_none_return)
38773894

38783895
# Only create a union type if the type context is a union, to be mostly

mypy/typeops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def simple_literal_value_key(t: ProperType) -> Optional[Tuple[str, ...]]:
318318
return None
319319

320320

321-
def simple_literal_type(t: ProperType) -> Optional[Instance]:
321+
def simple_literal_type(t: Optional[ProperType]) -> Optional[Instance]:
322322
"""Extract the underlying fallback Instance type for a simple Literal"""
323323
if isinstance(t, Instance) and t.last_known_value is not None:
324324
t = t.last_known_value

0 commit comments

Comments
 (0)