Repository: python/mypy

Issue:
mypy appears to become inefficient for nested ifs (13+ nested ifs)
<!--
  If you're new to mypy and you're not sure whether what you're experiencing is a mypy bug, please see the "Question and Help" form
  instead.
-->

**Bug Report**

<!--
Note: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited
for this report: https://github.com/python/typeshed/issues
-->
mypy appears to become inefficient for nested ifs (see example)

**To Reproduce**

Consider the following (bad) code example:
```
cat << EOF > lint_me.py
special_overrides = True
print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
                                       'a' if kv[0] == 'a_01' else \\
                                       'b' if kv[0] == 'b_02' else \\
                                       'c' if kv[0] == 'c_03' else \\
                                       'd' if kv[0] == 'd_04' else \\
                                       'e' if kv[0] == 'e_05' else \\
                                       'f' if kv[0] == 'f_06' else \\
                                       'g' if kv[0] == 'g_07' else \\
                                       'h' if kv[0] == 'h_08' else \\
                                       'i' if kv[0] == 'i_09' else \\
                                       'j' if kv[0] == 'j_10' else \\
                                       'k' if kv[0] == 'k_11' else \\
                                       'l' if kv[0] == 'l_12' else \\
                                       'm' if kv[0] == 'm_13' else \\
                                       'n' if kv[0] == 'n_14' else \\
                                       'o' if kv[0] == 'o_15' else \\
                                       'p' if kv[0] == 'p_16' else \\
                                       'q' if kv[0] == 'q_17' else \\
                                       kv[0]))
EOF
```


**Expected Behavior**
mypy can complain about very bad code in a reasonable time

**Actual Behavior**
with each additional `if` the total runtime seems to double:
```
13 nested ifs (until m): real    0m33.827s
14 nested ifs (until n): real    1m10.013s
15 nested ifs (until o): real    2m3.904s
16 nested ifs (until p): real    4m3.223s
17 nested ifs (until q): real    8m27.333s
```

**Your Environment**

<!-- Include as many relevant details about the environment you experienced the bug in -->

- Mypy version used: `0.790`
- Mypy command-line flags: None (just `python -m mypy lint_me.py`)
- Mypy configuration options from `mypy.ini` (and other config files): None
- Python version used: `Python 3.8.1`
- Operating system and version: `Windows 10, Version 10.0.19041 Build 19041`



Discussion:
Hi, can you elaborate the issue a bit? I guess if i understand the problem well, I could try solving it.
Hello,
apologies if the description was somewhat vage. The issue is about that mypy seems to become very inefficient with an increasing nestedness of if-blocks. If you consider the example above and only have a structure of 13 nested ifs mypy takes 0m33.827s to evaluate the statement for bad-code. With 14 nested if-blocks mypy takes 1m10.013s and essentially with each additional nested if the runtime doubles. The above example has 17 nested ifs and it consumes 8m27.333s to lint the code with mypy.

In case it is not clear (because I don't know how to phrase this differently) -- here are the full-examples with the time consumed by mypy:

<details>
    <summary>Example with 13 nested ifs (mypy runtime 0m33.827s)</summary>
     
	cat << EOF > lint_me.py
	special_overrides = True
	print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
						     'a' if kv[0] == 'a_01' else \\
						     'b' if kv[0] == 'b_02' else \\
						     'c' if kv[0] == 'c_03' else \\
						     'd' if kv[0] == 'd_04' else \\
						     'e' if kv[0] == 'e_05' else \\
						     'f' if kv[0] == 'f_06' else \\
						     'g' if kv[0] == 'g_07' else \\
						     'h' if kv[0] == 'h_08' else \\
						     'i' if kv[0] == 'i_09' else \\
						     'j' if kv[0] == 'j_10' else \\
						     'k' if kv[0] == 'k_11' else \\
						     'l' if kv[0] == 'l_12' else \\
						     'm' if kv[0] == 'm_13' else \\
						     kv[0]))
	EOF

	time python3 -m mypy lint_me.py # reports 0m33.827s
</details>

<details>
    <summary>Example with 14 nested ifs (mypy runtime 1m10.013s)</summary>
     
	cat << EOF > lint_me.py
	special_overrides = True
	print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
						     'a' if kv[0] == 'a_01' else \\
						     'b' if kv[0] == 'b_02' else \\
						     'c' if kv[0] == 'c_03' else \\
						     'd' if kv[0] == 'd_04' else \\
						     'e' if kv[0] == 'e_05' else \\
						     'f' if kv[0] == 'f_06' else \\
						     'g' if kv[0] == 'g_07' else \\
						     'h' if kv[0] == 'h_08' else \\
						     'i' if kv[0] == 'i_09' else \\
						     'j' if kv[0] == 'j_10' else \\
						     'k' if kv[0] == 'k_11' else \\
						     'l' if kv[0] == 'l_12' else \\
						     'm' if kv[0] == 'm_13' else \\
						     'n' if kv[0] == 'n_14' else \\
						     kv[0]))
	EOF

	time python3 -m mypy lint_me.py # reports 1m10.013s
</details>

<details>
    <summary>Example with 15 nested ifs (mypy runtime 2m3.904s)</summary>
     
	cat << EOF > lint_me.py
	special_overrides = True
	print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
						     'a' if kv[0] == 'a_01' else \\
						     'b' if kv[0] == 'b_02' else \\
						     'c' if kv[0] == 'c_03' else \\
						     'd' if kv[0] == 'd_04' else \\
						     'e' if kv[0] == 'e_05' else \\
						     'f' if kv[0] == 'f_06' else \\
						     'g' if kv[0] == 'g_07' else \\
						     'h' if kv[0] == 'h_08' else \\
						     'i' if kv[0] == 'i_09' else \\
						     'j' if kv[0] == 'j_10' else \\
						     'k' if kv[0] == 'k_11' else \\
						     'l' if kv[0] == 'l_12' else \\
						     'm' if kv[0] == 'm_13' else \\
						     'n' if kv[0] == 'n_14' else \\
						     'o' if kv[0] == 'o_15' else \\
						     kv[0]))
	EOF

	time python3 -m mypy lint_me.py # reports 2m3.904s
</details>

<details>
    <summary>Example with 16 nested ifs (mypy runtime 4m3.223s)</summary>
     
	cat << EOF > lint_me.py
	special_overrides = True
	print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
						     'a' if kv[0] == 'a_01' else \\
						     'b' if kv[0] == 'b_02' else \\
						     'c' if kv[0] == 'c_03' else \\
						     'd' if kv[0] == 'd_04' else \\
						     'e' if kv[0] == 'e_05' else \\
						     'f' if kv[0] == 'f_06' else \\
						     'g' if kv[0] == 'g_07' else \\
						     'h' if kv[0] == 'h_08' else \\
						     'i' if kv[0] == 'i_09' else \\
						     'j' if kv[0] == 'j_10' else \\
						     'k' if kv[0] == 'k_11' else \\
						     'l' if kv[0] == 'l_12' else \\
						     'm' if kv[0] == 'm_13' else \\
						     'n' if kv[0] == 'n_14' else \\
						     'o' if kv[0] == 'o_15' else \\
						     'p' if kv[0] == 'p_16' else \\
						     kv[0]))
	EOF

	time python3 -m mypy lint_me.py # reports 4m3.223s
</details>

<details>
    <summary>Example with 17 nested ifs (mypy runtime 8m27.333s)</summary>
     
	cat << EOF > lint_me.py
	special_overrides = True
	print(sorted(["c", "b", "a"], key=lambda kv: kv[0] if not special_overrides else \\
						     'a' if kv[0] == 'a_01' else \\
						     'b' if kv[0] == 'b_02' else \\
						     'c' if kv[0] == 'c_03' else \\
						     'd' if kv[0] == 'd_04' else \\
						     'e' if kv[0] == 'e_05' else \\
						     'f' if kv[0] == 'f_06' else \\
						     'g' if kv[0] == 'g_07' else \\
						     'h' if kv[0] == 'h_08' else \\
						     'i' if kv[0] == 'i_09' else \\
						     'j' if kv[0] == 'j_10' else \\
						     'k' if kv[0] == 'k_11' else \\
						     'l' if kv[0] == 'l_12' else \\
						     'm' if kv[0] == 'm_13' else \\
						     'n' if kv[0] == 'n_14' else \\
						     'o' if kv[0] == 'o_15' else \\
						     'p' if kv[0] == 'p_16' else \\
						     'q' if kv[0] == 'q_17' else \\
						     kv[0]))
	EOF

	time python3 -m mypy lint_me.py # reports 8m27.333s
</details>

For me (or the reason why I created the issue) is that it sounds wrong that the mypy runtime doubles, while the complexity of the code only grows linear. 

In the end this might even expected and considered a "won't fix".
Thanks for the clarification. Can you assign the issue to me? I am interested in finding solution to it.
I don't seem to have permission to assign the issue. I'd guess everyone who stumbles over it will see your comment that you intent to work on it :-)
@PurvaT we generally don't assign issues, but feel free to get started!
When I run this code on the Windows platform, I got the following error -  

C:\Users\dhananjay\Desktop>time python3 -m mypy lint_me.py
The system cannot accept the time entered.
Enter the new time:

I am unable to run the given code.
`time` is a unix command that measures the execution time of a process. Unless you have cygwin installed (or any additional packages that provide this command) this won't work like that on windows.

On Windows you could try:
```
@echo off
echo %time% < nul
python3 -m mypy lint_me.py
echo %time% < nul
```
I used the above lines to run the code on windows. I got the following output - 

1) if up to 'n':
  15:33:51.70
  15:33:52.03
   Total = 0.33 sec

2) if up to 'o':
  15:39:59.07
  15:39:59.47
   Total = 0.40 sec

3) if up to 'p':
  15:40:56.53
  15:40:56.90
   Total = 0.37 sec

4) if up to 'q':
  15:41:52.22
  15:41:52.63
   Total = 0.41 sec

I think the running time highly depends on the system as we are measuring a program running time not the asymptotic run rime.  Program execution time highly depends on underlying hardware.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -69,7 +69,7 @@
     try_expanding_sum_type_to_union, tuple_fallback, make_simplified_union,
     true_only, false_only, erase_to_union_or_bound, function_type,
     callable_type, try_getting_str_literals, custom_special_method,
-    is_literal_type_like,
+    is_literal_type_like, simple_literal_type,
 )
 from mypy.message_registry import ErrorMessage
 import mypy.errorcodes as codes
@@ -3874,26 +3874,43 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F
         if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx,
                                            allow_none_return=allow_none_return)
 
+        # we want to keep the narrowest value of if_type for union'ing the branches
+        # however, it would be silly to pass a literal as a type context. Pass the
+        # underlying fallback type instead.
+        if_type_fallback = simple_literal_type(get_proper_type(if_type)) or if_type
+
         # Analyze the right branch using full type context and store the type
         full_context_else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx,
                                                           allow_none_return=allow_none_return)
+
         if not mypy.checker.is_valid_inferred_type(if_type):
             # Analyze the right branch disregarding the left branch.
             else_type = full_context_else_type
+            # we want to keep the narrowest value of else_type for union'ing the branches
+            # however, it would be silly to pass a literal as a type context. Pass the
+            # underlying fallback type instead.
+            else_type_fallback = simple_literal_type(get_proper_type(else_type)) or else_type
 
             # If it would make a difference, re-analyze the left
             # branch using the right branch's type as context.
-            if ctx is None or not is_equivalent(else_type, ctx):
+            if ctx is None or not is_equivalent(else_type_fallback, ctx):
                 # TODO: If it's possible that the previous analysis of
                 # the left branch produced errors that are avoided
                 # using this context, suppress those errors.
-                if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type,
+                if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type_fallback,
                                                    allow_none_return=allow_none_return)
 
+        elif if_type_fallback == ctx:
+            # There is no point re-running the analysis if if_type is equal to ctx.
+            # That would  be an exact duplicate of the work we just did.
+            # This optimization is particularly important to avoid exponential blowup with nested
+            # if/else expressions: https://github.com/python/mypy/issues/9591
+            # TODO: would checking for is_proper_subtype also work and cover more cases?
+            else_type = full_context_else_type
         else:
             # Analyze the right branch in the context of the left
             # branch's type.
-            else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type,
+            else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type_fallback,
                                                  allow_none_return=allow_none_return)
 
         # Only create a union type if the type context is a union, to be mostly
diff --git a/mypy/typeops.py b/mypy/typeops.py
--- a/mypy/typeops.py
+++ b/mypy/typeops.py
@@ -318,7 +318,7 @@ def simple_literal_value_key(t: ProperType) -> Optional[Tuple[str, ...]]:
     return None
 
 
-def simple_literal_type(t: ProperType) -> Optional[Instance]:
+def simple_literal_type(t: Optional[ProperType]) -> Optional[Instance]:
     """Extract the underlying fallback Instance type for a simple Literal"""
     if isinstance(t, Instance) and t.last_known_value is not None:
         t = t.last_known_value


Test patch: