Repository: python/mypy

Issue:
Chained calls with medium/large dict/list literals are slow to typecheck

## 🐛 Bug Report

Variable assignments with chained calls converting dict/list literals are surprisingly slow to typecheck. Interestingly, breaking down the assignment into multiple steps often yields noticeable performance improvement.

## To Reproduce

For instance, consider:

```
from decimal import Decimal
from collections import OrderedDict

GRID = OrderedDict(sorted({
    (93, 659): Decimal('0.3000'), (93, 714): Decimal('0.2950'), (93, 749): Decimal('0.2820'), (93, 799): Decimal('0.2690'), (93, 850): Decimal('0.1990'),  # noqa: E501
    (94, 659): Decimal('0.3000'), (94, 714): Decimal('0.2900'), (94, 749): Decimal('0.2745'), (94, 799): Decimal('0.2590'), (94, 850): Decimal('0.1820'),  # noqa: E501
    (95, 659): Decimal('0.3000'), (95, 714): Decimal('0.2850'), (95, 749): Decimal('0.2620'), (95, 799): Decimal('0.2390'), (95, 850): Decimal('0.1620'),  # noqa: E501
    (96, 659): Decimal('0.3000'), (96, 714): Decimal('0.2800'), (96, 749): Decimal('0.2495'), (96, 799): Decimal('0.2190'), (96, 850): Decimal('0.1570'),  # noqa: E501
    (97, 659): Decimal('0.2800'), (97, 714): Decimal('0.2750'), (97, 749): Decimal('0.2370'), (97, 799): Decimal('0.1990'), (97, 850): Decimal('0.1520'),  # noqa: E501
    (98, 659): Decimal('0.2710'), (98, 714): Decimal('0.2650'), (98, 749): Decimal('0.2220'), (98, 799): Decimal('0.1790'), (98, 850): Decimal('0.1470'),  # noqa: E501
    (99, 659): Decimal('0.2500'), (99, 714): Decimal('0.1990'), (99, 749): Decimal('0.1975'), (99, 799): Decimal('0.1670'), (99, 850): Decimal('0.1170'),  # noqa: E501
    (100, 659): Decimal('0.2500'), (100, 714): Decimal('0.1890'), (100, 749): Decimal('0.1500'), (100, 799): Decimal('0.1084'), (100, 850): Decimal('0.1000'),  # noqa: E501
}.items()))

```

and the same broken down into two steps:

```
from decimal import Decimal
from collections import OrderedDict

GRID = {
    (93, 659): Decimal('0.3000'), (93, 714): Decimal('0.2950'), (93, 749): Decimal('0.2820'), (93, 799): Decimal('0.2690'), (93, 850): Decimal('0.1990'),  # noqa: E501
    (94, 659): Decimal('0.3000'), (94, 714): Decimal('0.2900'), (94, 749): Decimal('0.2745'), (94, 799): Decimal('0.2590'), (94, 850): Decimal('0.1820'),  # noqa: E501
    (95, 659): Decimal('0.3000'), (95, 714): Decimal('0.2850'), (95, 749): Decimal('0.2620'), (95, 799): Decimal('0.2390'), (95, 850): Decimal('0.1620'),  # noqa: E501
    (96, 659): Decimal('0.3000'), (96, 714): Decimal('0.2800'), (96, 749): Decimal('0.2495'), (96, 799): Decimal('0.2190'), (96, 850): Decimal('0.1570'),  # noqa: E501
    (97, 659): Decimal('0.2800'), (97, 714): Decimal('0.2750'), (97, 749): Decimal('0.2370'), (97, 799): Decimal('0.1990'), (97, 850): Decimal('0.1520'),  # noqa: E501
    (98, 659): Decimal('0.2710'), (98, 714): Decimal('0.2650'), (98, 749): Decimal('0.2220'), (98, 799): Decimal('0.1790'), (98, 850): Decimal('0.1470'),  # noqa: E501
    (99, 659): Decimal('0.2500'), (99, 714): Decimal('0.1990'), (99, 749): Decimal('0.1975'), (99, 799): Decimal('0.1670'), (99, 850): Decimal('0.1170'),  # noqa: E501
    (100, 659): Decimal('0.2500'), (100, 714): Decimal('0.1890'), (100, 749): Decimal('0.1500'), (100, 799): Decimal('0.1084'), (100, 850): Decimal('0.1000'),  # noqa: E501
}

GG = OrderedDict(sorted(GRID.items()))
```
## Expected Behavior

I would expect the two samples above to typecheck in roughly equivalent amounts of time.
## Actual Behavior

Variant 1 is much slower to typecheck than variant 2: roughly 700ms vs 70ms (not using mypyc-compiled version)

This seems to arise from a combination of repeated computations when typechecking overloaded methods such as `sorted`, typechecking from root to leaf instead of leaf to root, and not caching resolved types for dict/list exprs.

## Your Environment

- Mypy version used: 0.790-dev 5906a5d
- Mypy command-line flags: --ignore-missing-imports --show-traceback
- Python version used: 3.7
- Operating system and version: macOS 10.14.6



Discussion:

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -293,6 +293,7 @@ def reset(self) -> None:
         self._type_maps[1:] = []
         self._type_maps[0].clear()
         self.temp_type_map = None
+        self.expr_checker.reset()
 
         assert self.inferred_attribute_types is None
         assert self.partial_types == []
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -177,6 +177,9 @@ class ExpressionChecker(ExpressionVisitor[Type]):
     # Type context for type inference
     type_context: List[Optional[Type]]
 
+    # cache resolved types in some cases
+    resolved_type: Dict[Expression, ProperType]
+
     strfrm_checker: StringFormatterChecker
     plugin: Plugin
 
@@ -197,6 +200,11 @@ def __init__(self,
         self.type_overrides: Dict[Expression, Type] = {}
         self.strfrm_checker = StringFormatterChecker(self, self.chk, self.msg)
 
+        self.resolved_type = {}
+
+    def reset(self) -> None:
+        self.resolved_type = {}
+
     def visit_name_expr(self, e: NameExpr) -> Type:
         """Type check a name expression.
 
@@ -3269,13 +3277,13 @@ def apply_type_arguments_to_callable(
 
     def visit_list_expr(self, e: ListExpr) -> Type:
         """Type check a list expression [...]."""
-        return self.check_lst_expr(e.items, 'builtins.list', '<list>', e)
+        return self.check_lst_expr(e, 'builtins.list', '<list>')
 
     def visit_set_expr(self, e: SetExpr) -> Type:
-        return self.check_lst_expr(e.items, 'builtins.set', '<set>', e)
+        return self.check_lst_expr(e, 'builtins.set', '<set>')
 
     def fast_container_type(
-            self, items: List[Expression], container_fullname: str
+            self, e: Union[ListExpr, SetExpr, TupleExpr], container_fullname: str
     ) -> Optional[Type]:
         """
         Fast path to determine the type of a list or set literal,
@@ -3290,21 +3298,28 @@ def fast_container_type(
         ctx = self.type_context[-1]
         if ctx:
             return None
+        rt = self.resolved_type.get(e, None)
+        if rt is not None:
+            return rt if isinstance(rt, Instance) else None
         values: List[Type] = []
-        for item in items:
+        for item in e.items:
             if isinstance(item, StarExpr):
                 # fallback to slow path
+                self.resolved_type[e] = NoneType()
                 return None
             values.append(self.accept(item))
         vt = join.join_type_list(values)
         if not allow_fast_container_literal(vt):
+            self.resolved_type[e] = NoneType()
             return None
-        return self.chk.named_generic_type(container_fullname, [vt])
+        ct = self.chk.named_generic_type(container_fullname, [vt])
+        self.resolved_type[e] = ct
+        return ct
 
-    def check_lst_expr(self, items: List[Expression], fullname: str,
-                       tag: str, context: Context) -> Type:
+    def check_lst_expr(self, e: Union[ListExpr, SetExpr, TupleExpr], fullname: str,
+                       tag: str) -> Type:
         # fast path
-        t = self.fast_container_type(items, fullname)
+        t = self.fast_container_type(e, fullname)
         if t:
             return t
 
@@ -3323,10 +3338,10 @@ def check_lst_expr(self, items: List[Expression], fullname: str,
             variables=[tv])
         out = self.check_call(constructor,
                               [(i.expr if isinstance(i, StarExpr) else i)
-                               for i in items],
+                               for i in e.items],
                               [(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS)
-                               for i in items],
-                              context)[0]
+                               for i in e.items],
+                              e)[0]
         return remove_instance_last_known_values(out)
 
     def visit_tuple_expr(self, e: TupleExpr) -> Type:
@@ -3376,7 +3391,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
                 else:
                     # A star expression that's not a Tuple.
                     # Treat the whole thing as a variable-length tuple.
-                    return self.check_lst_expr(e.items, 'builtins.tuple', '<tuple>', e)
+                    return self.check_lst_expr(e, 'builtins.tuple', '<tuple>')
             else:
                 if not type_context_items or j >= len(type_context_items):
                     tt = self.accept(item)
@@ -3402,6 +3417,9 @@ def fast_dict_type(self, e: DictExpr) -> Optional[Type]:
         ctx = self.type_context[-1]
         if ctx:
             return None
+        rt = self.resolved_type.get(e, None)
+        if rt is not None:
+            return rt if isinstance(rt, Instance) else None
         keys: List[Type] = []
         values: List[Type] = []
         stargs: Optional[Tuple[Type, Type]] = None
@@ -3415,6 +3433,7 @@ def fast_dict_type(self, e: DictExpr) -> Optional[Type]:
                 ):
                     stargs = (st.args[0], st.args[1])
                 else:
+                    self.resolved_type[e] = NoneType()
                     return None
             else:
                 keys.append(self.accept(key))
@@ -3422,10 +3441,14 @@ def fast_dict_type(self, e: DictExpr) -> Optional[Type]:
         kt = join.join_type_list(keys)
         vt = join.join_type_list(values)
         if not (allow_fast_container_literal(kt) and allow_fast_container_literal(vt)):
+            self.resolved_type[e] = NoneType()
             return None
         if stargs and (stargs[0] != kt or stargs[1] != vt):
+            self.resolved_type[e] = NoneType()
             return None
-        return self.chk.named_generic_type('builtins.dict', [kt, vt])
+        dt = self.chk.named_generic_type('builtins.dict', [kt, vt])
+        self.resolved_type[e] = dt
+        return dt
 
     def visit_dict_expr(self, e: DictExpr) -> Type:
         """Type check a dict expression.


Test patch: