Skip to content

Commit e145ba8

Browse files
authored
Speed up typechecking of dict, set and list expressions (#9477)
Typechecking of dict, set, and list literals currentlly goes through typechecking of the generic dict/set/list constructor internally. This is usually fine but becomes horrendously slow when the number of items is large: - for generic methods, `infer_arg_types_in_context` is called twice - `infer_arg_types_in_context` is O(n**2) where `n` is the number of arguments, which, in the case of a literal, is the number of items. Add an `O(n)` fast path for deriving the type of simple container literal expressions. This fast path only handle a subset of cases but it provides a tremendous speedup for the relatively common case of large literal constants. The real-world example that motivated this change is a 1889 lines long dict constant representing the parsed value of a mock JSON response from a 3rd party service, where typechecking previously took upwards of 50s and is now down to under 1s with this fast path.
1 parent aed5642 commit e145ba8

File tree

1 file changed

+81
-0
lines changed

1 file changed

+81
-0
lines changed

mypy/checkexpr.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,8 +3218,42 @@ def visit_list_expr(self, e: ListExpr) -> Type:
32183218
def visit_set_expr(self, e: SetExpr) -> Type:
32193219
return self.check_lst_expr(e.items, 'builtins.set', '<set>', e)
32203220

3221+
def fast_container_type(
3222+
self, items: List[Expression], container_fullname: str
3223+
) -> Optional[Type]:
3224+
"""
3225+
Fast path to determine the type of a list or set literal,
3226+
based on the list of entries. This mostly impacts large
3227+
module-level constant definitions.
3228+
3229+
Limitations:
3230+
- no active type context
3231+
- no star expressions
3232+
- the joined type of all entries must be an Instance type
3233+
"""
3234+
ctx = self.type_context[-1]
3235+
if ctx:
3236+
return None
3237+
values = [] # type: List[Type]
3238+
for item in items:
3239+
if isinstance(item, StarExpr):
3240+
# fallback to slow path
3241+
return None
3242+
values.append(self.accept(item))
3243+
vt = join.join_type_list(values)
3244+
if not isinstance(vt, Instance):
3245+
return None
3246+
# TODO: update tests instead?
3247+
vt.erased = True
3248+
return self.chk.named_generic_type(container_fullname, [vt])
3249+
32213250
def check_lst_expr(self, items: List[Expression], fullname: str,
32223251
tag: str, context: Context) -> Type:
3252+
# fast path
3253+
t = self.fast_container_type(items, fullname)
3254+
if t:
3255+
return t
3256+
32233257
# Translate into type checking a generic function call.
32243258
# Used for list and set expressions, as well as for tuples
32253259
# containing star expressions that don't refer to a
@@ -3301,6 +3335,48 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
33013335
fallback_item = AnyType(TypeOfAny.special_form)
33023336
return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item]))
33033337

3338+
def fast_dict_type(self, e: DictExpr) -> Optional[Type]:
3339+
"""
3340+
Fast path to determine the type of a dict literal,
3341+
based on the list of entries. This mostly impacts large
3342+
module-level constant definitions.
3343+
3344+
Limitations:
3345+
- no active type context
3346+
- only supported star expressions are other dict instances
3347+
- the joined types of all keys and values must be Instance types
3348+
"""
3349+
ctx = self.type_context[-1]
3350+
if ctx:
3351+
return None
3352+
keys = [] # type: List[Type]
3353+
values = [] # type: List[Type]
3354+
stargs = None # type: Optional[Tuple[Type, Type]]
3355+
for key, value in e.items:
3356+
if key is None:
3357+
st = get_proper_type(self.accept(value))
3358+
if (
3359+
isinstance(st, Instance)
3360+
and st.type.fullname == 'builtins.dict'
3361+
and len(st.args) == 2
3362+
):
3363+
stargs = (st.args[0], st.args[1])
3364+
else:
3365+
return None
3366+
else:
3367+
keys.append(self.accept(key))
3368+
values.append(self.accept(value))
3369+
kt = join.join_type_list(keys)
3370+
vt = join.join_type_list(values)
3371+
if not (isinstance(kt, Instance) and isinstance(vt, Instance)):
3372+
return None
3373+
if stargs and (stargs[0] != kt or stargs[1] != vt):
3374+
return None
3375+
# TODO: update tests instead?
3376+
kt.erased = True
3377+
vt.erased = True
3378+
return self.chk.named_generic_type('builtins.dict', [kt, vt])
3379+
33043380
def visit_dict_expr(self, e: DictExpr) -> Type:
33053381
"""Type check a dict expression.
33063382
@@ -3319,6 +3395,11 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
33193395
)
33203396
return typeddict_context.copy_modified()
33213397

3398+
# fast path attempt
3399+
dt = self.fast_dict_type(e)
3400+
if dt:
3401+
return dt
3402+
33223403
# Collect function arguments, watching out for **expr.
33233404
args = [] # type: List[Expression] # Regular "key: value"
33243405
stargs = [] # type: List[Expression] # For "**expr"

0 commit comments

Comments
 (0)