Skip to content

Commit 7bd6fdd

Browse files
authoredMay 17, 2022
[mypyc] Detect always defined attributes (#12600)
Use static analysis to find attributes that are always defined. Always defined attributes don't require checks on each access. This makes them faster and also reduces code size. Attributes defined in the class body and assigned to in all code paths in `__init__` are always defined. We need to know all subclasses statically to determine whether `__init__` always defines an attribute in every case, including in subclasses. The analysis looks at `__init__` methods and supports limited inter-procedural analysis over `super().__init__(...)` calls. Otherwise we rely on intra-procedural analysis to keep the analysis fast. As a side effect, `__init__` will now always be called when constructing an object. This means that `copy.copy` (and others like it) won't be supported for native classes unless `__init__` can be called without arguments. `mypyc/analysis/attrdefined.py` has more details about the algorithm in docstrings. Performance impact to selected benchmarks (with clang): - richards +28% - deltablue +10% - hexiom +1% The richards result is probably an outlier. This will also significantly help with native integers (mypyc/mypyc#837, as tracking undefined values would otherwise require extra memory use. Closes mypyc/mypyc#836.
1 parent 18a5107 commit 7bd6fdd

32 files changed

+2187
-240
lines changed
 

‎mypy/copytype.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from typing import Any, cast
2+
3+
from mypy.types import (
4+
ProperType, UnboundType, AnyType, NoneType, UninhabitedType, ErasedType, DeletedType,
5+
Instance, TypeVarType, ParamSpecType, PartialType, CallableType, TupleType, TypedDictType,
6+
LiteralType, UnionType, Overloaded, TypeType, TypeAliasType, UnpackType, Parameters,
7+
TypeVarTupleType
8+
)
9+
from mypy.type_visitor import TypeVisitor
10+
11+
12+
def copy_type(t: ProperType) -> ProperType:
13+
"""Create a shallow copy of a type.
14+
15+
This can be used to mutate the copy with truthiness information.
16+
17+
Classes compiled with mypyc don't support copy.copy(), so we need
18+
a custom implementation.
19+
"""
20+
return t.accept(TypeShallowCopier())
21+
22+
23+
class TypeShallowCopier(TypeVisitor[ProperType]):
24+
def visit_unbound_type(self, t: UnboundType) -> ProperType:
25+
return t
26+
27+
def visit_any(self, t: AnyType) -> ProperType:
28+
return self.copy_common(t, AnyType(t.type_of_any, t.source_any, t.missing_import_name))
29+
30+
def visit_none_type(self, t: NoneType) -> ProperType:
31+
return self.copy_common(t, NoneType())
32+
33+
def visit_uninhabited_type(self, t: UninhabitedType) -> ProperType:
34+
dup = UninhabitedType(t.is_noreturn)
35+
dup.ambiguous = t.ambiguous
36+
return self.copy_common(t, dup)
37+
38+
def visit_erased_type(self, t: ErasedType) -> ProperType:
39+
return self.copy_common(t, ErasedType())
40+
41+
def visit_deleted_type(self, t: DeletedType) -> ProperType:
42+
return self.copy_common(t, DeletedType(t.source))
43+
44+
def visit_instance(self, t: Instance) -> ProperType:
45+
dup = Instance(t.type, t.args, last_known_value=t.last_known_value)
46+
dup.invalid = t.invalid
47+
return self.copy_common(t, dup)
48+
49+
def visit_type_var(self, t: TypeVarType) -> ProperType:
50+
dup = TypeVarType(
51+
t.name,
52+
t.fullname,
53+
t.id,
54+
values=t.values,
55+
upper_bound=t.upper_bound,
56+
variance=t.variance,
57+
)
58+
return self.copy_common(t, dup)
59+
60+
def visit_param_spec(self, t: ParamSpecType) -> ProperType:
61+
dup = ParamSpecType(t.name, t.fullname, t.id, t.flavor, t.upper_bound, prefix=t.prefix)
62+
return self.copy_common(t, dup)
63+
64+
def visit_parameters(self, t: Parameters) -> ProperType:
65+
dup = Parameters(t.arg_types, t.arg_kinds, t.arg_names,
66+
variables=t.variables,
67+
is_ellipsis_args=t.is_ellipsis_args)
68+
return self.copy_common(t, dup)
69+
70+
def visit_type_var_tuple(self, t: TypeVarTupleType) -> ProperType:
71+
dup = TypeVarTupleType(t.name, t.fullname, t.id, t.upper_bound)
72+
return self.copy_common(t, dup)
73+
74+
def visit_unpack_type(self, t: UnpackType) -> ProperType:
75+
dup = UnpackType(t.type)
76+
return self.copy_common(t, dup)
77+
78+
def visit_partial_type(self, t: PartialType) -> ProperType:
79+
return self.copy_common(t, PartialType(t.type, t.var, t.value_type))
80+
81+
def visit_callable_type(self, t: CallableType) -> ProperType:
82+
return self.copy_common(t, t.copy_modified())
83+
84+
def visit_tuple_type(self, t: TupleType) -> ProperType:
85+
return self.copy_common(t, TupleType(t.items, t.partial_fallback, implicit=t.implicit))
86+
87+
def visit_typeddict_type(self, t: TypedDictType) -> ProperType:
88+
return self.copy_common(t, TypedDictType(t.items, t.required_keys, t.fallback))
89+
90+
def visit_literal_type(self, t: LiteralType) -> ProperType:
91+
return self.copy_common(t, LiteralType(value=t.value, fallback=t.fallback))
92+
93+
def visit_union_type(self, t: UnionType) -> ProperType:
94+
return self.copy_common(t, UnionType(t.items))
95+
96+
def visit_overloaded(self, t: Overloaded) -> ProperType:
97+
return self.copy_common(t, Overloaded(items=t.items))
98+
99+
def visit_type_type(self, t: TypeType) -> ProperType:
100+
# Use cast since the type annotations in TypeType are imprecise.
101+
return self.copy_common(t, TypeType(cast(Any, t.item)))
102+
103+
def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
104+
assert False, "only ProperTypes supported"
105+
106+
def copy_common(self, t: ProperType, t2: ProperType) -> ProperType:
107+
t2.line = t.line
108+
t2.column = t.column
109+
t2.can_be_false = t.can_be_false
110+
t2.can_be_true = t.can_be_true
111+
return t2

‎mypy/moduleinspect.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@
1212

1313

1414
class ModuleProperties:
15+
# Note that all __init__ args must have default values
1516
def __init__(self,
16-
name: str,
17-
file: Optional[str],
18-
path: Optional[List[str]],
19-
all: Optional[List[str]],
20-
is_c_module: bool,
21-
subpackages: List[str]) -> None:
17+
name: str = "",
18+
file: Optional[str] = None,
19+
path: Optional[List[str]] = None,
20+
all: Optional[List[str]] = None,
21+
is_c_module: bool = False,
22+
subpackages: Optional[List[str]] = None) -> None:
2223
self.name = name # __name__ attribute
2324
self.file = file # __file__ attribute
2425
self.path = path # __path__ attribute
2526
self.all = all # __all__ attribute
2627
self.is_c_module = is_c_module
27-
self.subpackages = subpackages
28+
self.subpackages = subpackages or []
2829

2930

3031
def is_c_module(module: ModuleType) -> bool:

‎mypy/nodes.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -668,16 +668,16 @@ class FuncItem(FuncBase):
668668
__deletable__ = ('arguments', 'max_pos', 'min_args')
669669

670670
def __init__(self,
671-
arguments: List[Argument],
672-
body: 'Block',
671+
arguments: Optional[List[Argument]] = None,
672+
body: Optional['Block'] = None,
673673
typ: 'Optional[mypy.types.FunctionLike]' = None) -> None:
674674
super().__init__()
675-
self.arguments = arguments
676-
self.arg_names = [None if arg.pos_only else arg.variable.name for arg in arguments]
675+
self.arguments = arguments or []
676+
self.arg_names = [None if arg.pos_only else arg.variable.name for arg in self.arguments]
677677
self.arg_kinds: List[ArgKind] = [arg.kind for arg in self.arguments]
678678
self.max_pos: int = (
679679
self.arg_kinds.count(ARG_POS) + self.arg_kinds.count(ARG_OPT))
680-
self.body: 'Block' = body
680+
self.body: 'Block' = body or Block([])
681681
self.type = typ
682682
self.unanalyzed_type = typ
683683
self.is_overload: bool = False
@@ -725,10 +725,11 @@ class FuncDef(FuncItem, SymbolNode, Statement):
725725
'original_def',
726726
)
727727

728+
# Note that all __init__ args must have default values
728729
def __init__(self,
729-
name: str, # Function name
730-
arguments: List[Argument],
731-
body: 'Block',
730+
name: str = '', # Function name
731+
arguments: Optional[List[Argument]] = None,
732+
body: Optional['Block'] = None,
732733
typ: 'Optional[mypy.types.FunctionLike]' = None) -> None:
733734
super().__init__(arguments, body, typ)
734735
self._name = name

‎mypy/stubtest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,6 @@ def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> Optional[nodes.
895895
896896
Returns None if we can't figure out what that would be. For convenience, this function also
897897
accepts FuncItems.
898-
899898
"""
900899
if isinstance(dec, nodes.FuncItem):
901900
return dec
@@ -917,6 +916,7 @@ def apply_decorator_to_funcitem(
917916
return func
918917
if decorator.fullname == "builtins.classmethod":
919918
assert func.arguments[0].variable.name in ("cls", "metacls")
919+
# FuncItem is written so that copy.copy() actually works, even when compiled
920920
ret = copy.copy(func)
921921
# Remove the cls argument, since it's not present in inspect.signature of classmethods
922922
ret.arguments = ret.arguments[1:]

‎mypy/typeops.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
TupleType, Instance, FunctionLike, Type, CallableType, TypeVarLikeType, Overloaded,
1515
TypeVarType, UninhabitedType, FormalArgument, UnionType, NoneType,
1616
AnyType, TypeOfAny, TypeType, ProperType, LiteralType, get_proper_type, get_proper_types,
17-
copy_type, TypeAliasType, TypeQuery, ParamSpecType, Parameters,
18-
ENUM_REMOVED_PROPS
17+
TypeAliasType, TypeQuery, ParamSpecType, Parameters, ENUM_REMOVED_PROPS
1918
)
2019
from mypy.nodes import (
2120
FuncBase, FuncItem, FuncDef, OverloadedFuncDef, TypeInfo, ARG_STAR, ARG_STAR2, ARG_POS,
2221
Expression, StrExpr, Var, Decorator, SYMBOL_FUNCBASE_TYPES
2322
)
2423
from mypy.maptype import map_instance_to_supertype
2524
from mypy.expandtype import expand_type_by_instance, expand_type
25+
from mypy.copytype import copy_type
2626

2727
from mypy.typevars import fill_typevars
2828

‎mypy/types.py

-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Classes for representing mypy types."""
22

3-
import copy
43
import sys
54
from abc import abstractmethod
65

@@ -2893,16 +2892,6 @@ def is_named_instance(t: Type, fullnames: Union[str, Tuple[str, ...]]) -> bool:
28932892
return isinstance(t, Instance) and t.type.fullname in fullnames
28942893

28952894

2896-
TP = TypeVar('TP', bound=Type)
2897-
2898-
2899-
def copy_type(t: TP) -> TP:
2900-
"""
2901-
Build a copy of the type; used to mutate the copy with truthiness information
2902-
"""
2903-
return copy.copy(t)
2904-
2905-
29062895
class InstantiateAliasVisitor(TypeTranslator):
29072896
def __init__(self, vars: List[str], subs: List[Type]) -> None:
29082897
self.replacements = {v: s for (v, s) in zip(vars, subs)}

0 commit comments

Comments
 (0)
Please sign in to comment.