Skip to content

Commit b673366

Browse files
hugues-affJukkaL
authored andcommitted
speedup typechecking of nested if expressions (#12700)
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 6db3d96 commit b673366

File tree

2 files changed

+22
-5
lines changed

2 files changed

+22
-5
lines changed

mypy/checkexpr.py

+21-4
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
@@ -3874,26 +3874,43 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F
38743874
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx,
38753875
allow_none_return=allow_none_return)
38763876

3877+
# we want to keep the narrowest value of if_type for union'ing the branches
3878+
# however, it would be silly to pass a literal as a type context. Pass the
3879+
# underlying fallback type instead.
3880+
if_type_fallback = simple_literal_type(get_proper_type(if_type)) or if_type
3881+
38773882
# Analyze the right branch using full type context and store the type
38783883
full_context_else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx,
38793884
allow_none_return=allow_none_return)
3885+
38803886
if not mypy.checker.is_valid_inferred_type(if_type):
38813887
# Analyze the right branch disregarding the left branch.
38823888
else_type = full_context_else_type
3889+
# we want to keep the narrowest value of else_type for union'ing the branches
3890+
# however, it would be silly to pass a literal as a type context. Pass the
3891+
# underlying fallback type instead.
3892+
else_type_fallback = simple_literal_type(get_proper_type(else_type)) or else_type
38833893

38843894
# If it would make a difference, re-analyze the left
38853895
# branch using the right branch's type as context.
3886-
if ctx is None or not is_equivalent(else_type, ctx):
3896+
if ctx is None or not is_equivalent(else_type_fallback, ctx):
38873897
# TODO: If it's possible that the previous analysis of
38883898
# the left branch produced errors that are avoided
38893899
# using this context, suppress those errors.
3890-
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type,
3900+
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type_fallback,
38913901
allow_none_return=allow_none_return)
38923902

3903+
elif if_type_fallback == ctx:
3904+
# There is no point re-running the analysis if if_type is equal to ctx.
3905+
# That would be an exact duplicate of the work we just did.
3906+
# This optimization is particularly important to avoid exponential blowup with nested
3907+
# if/else expressions: https://github.com/python/mypy/issues/9591
3908+
# TODO: would checking for is_proper_subtype also work and cover more cases?
3909+
else_type = full_context_else_type
38933910
else:
38943911
# Analyze the right branch in the context of the left
38953912
# branch's type.
3896-
else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type,
3913+
else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type_fallback,
38973914
allow_none_return=allow_none_return)
38983915

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

mypy/typeops.py

+1-1
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)