Micro-optimization: Make ArgKind a regular class instead of enum
Mypyc doesn't generate very efficient code for enums, so switch to a
regular class. We can later revert the change if/when we can improve
enum support in mypyc.
Operations related to ArgKind were pretty prominent in the op trace
log (#19457).
By itself this improves performance by ~1.7%, based on
`perf_compare.py`, which is significant:
```
master 4.168s (0.0%) | stdev 0.037s
HEAD 4.098s (-1.7%) | stdev 0.028s
```
This is a part of a set of micro-optimizations that improve self check
performance by ~5.5%.
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 8223ccf..a8740d4 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -34,6 +34,7 @@
from mypy.messages import MessageBuilder, format_type
from mypy.nodes import (
ARG_NAMED,
+ ARG_NAMED_OPT,
ARG_POS,
ARG_STAR,
ARG_STAR2,
@@ -1000,7 +1001,7 @@
return CallableType(
list(callee.items.values()),
[
- ArgKind.ARG_NAMED if name in callee.required_keys else ArgKind.ARG_NAMED_OPT
+ ARG_NAMED if name in callee.required_keys else ARG_NAMED_OPT
for name in callee.items
],
list(callee.items.keys()),
@@ -1074,7 +1075,7 @@
# TypedDict. This is a bit arbitrary, but in most cases will work better than
# trying to infer a union or a join.
[args[0] for args in kwargs.values()],
- [ArgKind.ARG_NAMED] * len(kwargs),
+ [ARG_NAMED] * len(kwargs),
context,
list(kwargs.keys()),
None,
diff --git a/mypy/nodes.py b/mypy/nodes.py
index fc2656c..d61e112 100644
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -6,8 +6,18 @@
from abc import abstractmethod
from collections import defaultdict
from collections.abc import Iterator, Sequence
-from enum import Enum, unique
-from typing import TYPE_CHECKING, Any, Callable, Final, Optional, TypeVar, Union, cast
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ ClassVar,
+ Final,
+ Optional,
+ TypeVar,
+ Union,
+ cast,
+ final,
+)
from typing_extensions import TypeAlias as _TypeAlias, TypeGuard
from mypy_extensions import trait
@@ -873,7 +883,7 @@
"name": self._name,
"fullname": self._fullname,
"arg_names": self.arg_names,
- "arg_kinds": [int(x.value) for x in self.arg_kinds],
+ "arg_kinds": [x.value for x in self.arg_kinds],
"type": None if self.type is None else self.type.serialize(),
"flags": get_flags(self, FUNCDEF_FLAGS),
"abstract_status": self.abstract_status,
@@ -904,7 +914,7 @@
set_flags(ret, data["flags"])
# NOTE: ret.info is set in the fixup phase.
ret.arg_names = data["arg_names"]
- ret.arg_kinds = [ArgKind(x) for x in data["arg_kinds"]]
+ ret.arg_kinds = [ArgKind.by_value(x) for x in data["arg_kinds"]]
ret.abstract_status = data["abstract_status"]
ret.dataclass_transform_spec = (
DataclassTransformSpec.deserialize(data["dataclass_transform_spec"])
@@ -1963,21 +1973,35 @@
return visitor.visit_member_expr(self)
-# Kinds of arguments
-@unique
-class ArgKind(Enum):
- # Positional argument
- ARG_POS = 0
- # Positional, optional argument (functions only, not calls)
- ARG_OPT = 1
- # *arg argument
- ARG_STAR = 2
- # Keyword argument x=y in call, or keyword-only function arg
- ARG_NAMED = 3
- # **arg argument
- ARG_STAR2 = 4
- # In an argument list, keyword-only and also optional
- ARG_NAMED_OPT = 5
+@final
+class ArgKind:
+ """Kinds of arguments.
+
+ NOTE: This isn't an enum due to mypyc performance limitations.
+ """
+
+ _sealed: ClassVar[bool] = False # Hack to ensure enum-like behavior
+
+ def __init__(self, name: str, value: int) -> None:
+ assert not ArgKind._sealed
+ self.name: Final = name
+ self.value: Final = value
+
+ @staticmethod
+ def by_value(value: int) -> ArgKind:
+ if value == ARG_POS.value:
+ return ARG_POS
+ elif value == ARG_OPT.value:
+ return ARG_OPT
+ elif value == ARG_STAR.value:
+ return ARG_STAR
+ elif value == ARG_NAMED.value:
+ return ARG_NAMED
+ elif value == ARG_STAR2.value:
+ return ARG_STAR2
+ else:
+ assert value == ARG_NAMED_OPT.value
+ return ARG_NAMED_OPT
def is_positional(self, star: bool = False) -> bool:
return self == ARG_POS or self == ARG_OPT or (star and self == ARG_STAR)
@@ -1995,12 +2019,29 @@
return self == ARG_STAR or self == ARG_STAR2
-ARG_POS: Final = ArgKind.ARG_POS
-ARG_OPT: Final = ArgKind.ARG_OPT
-ARG_STAR: Final = ArgKind.ARG_STAR
-ARG_NAMED: Final = ArgKind.ARG_NAMED
-ARG_STAR2: Final = ArgKind.ARG_STAR2
-ARG_NAMED_OPT: Final = ArgKind.ARG_NAMED_OPT
+# Positional argument
+ARG_POS: Final = ArgKind("ARG_POS", 0)
+# Positional, optional argument (functions only, not calls)
+ARG_OPT: Final = ArgKind("ARG_OPT", 1)
+# *arg argument
+ARG_STAR: Final = ArgKind("ARG_STAR", 2)
+# Keyword argument x=y in call, or keyword-only function arg
+ARG_NAMED: Final = ArgKind("ARG_NAMED", 3)
+# **arg argument
+ARG_STAR2: Final = ArgKind("ARG_STAR2", 4)
+# In an argument list, keyword-only and also optional
+ARG_NAMED_OPT: Final = ArgKind("ARG_NAMED_OPT", 5)
+
+ArgKind._sealed = True # Make sure no new ArgKinds can be created
+
+ALL_ARG_KINDS: Final[tuple[ArgKind, ...]] = (
+ ARG_POS,
+ ARG_OPT,
+ ARG_STAR,
+ ARG_NAMED,
+ ARG_STAR2,
+ ARG_NAMED_OPT,
+)
class CallExpr(Expression):
diff --git a/mypy/plugins/functools.py b/mypy/plugins/functools.py
index c8b370f..ef62cf4 100644
--- a/mypy/plugins/functools.py
+++ b/mypy/plugins/functools.py
@@ -10,10 +10,13 @@
from mypy.argmap import map_actuals_to_formals
from mypy.erasetype import erase_typevars
from mypy.nodes import (
+ ARG_NAMED,
+ ARG_NAMED_OPT,
+ ARG_OPT,
ARG_POS,
+ ARG_STAR,
ARG_STAR2,
SYMBOL_FUNCBASE_TYPES,
- ArgKind,
Argument,
CallExpr,
NameExpr,
@@ -217,11 +220,7 @@
# special_sig="partial" allows omission of args/kwargs typed with ParamSpec
defaulted = fn_type.copy_modified(
arg_kinds=[
- (
- ArgKind.ARG_OPT
- if k == ArgKind.ARG_POS
- else (ArgKind.ARG_NAMED_OPT if k == ArgKind.ARG_NAMED else k)
- )
+ (ARG_OPT if k == ARG_POS else (ARG_NAMED_OPT if k == ARG_NAMED else k))
for k in fn_type.arg_kinds
],
ret_type=ret_type,
@@ -284,19 +283,19 @@
# true when PEP 646 things are happening. See testFunctoolsPartialTypeVarTuple
arg_type = fn_type.arg_types[i]
- if not actuals or fn_type.arg_kinds[i] in (ArgKind.ARG_STAR, ArgKind.ARG_STAR2):
+ if not actuals or fn_type.arg_kinds[i] in (ARG_STAR, ARG_STAR2):
partial_kinds.append(fn_type.arg_kinds[i])
partial_types.append(arg_type)
partial_names.append(fn_type.arg_names[i])
else:
assert actuals
- if any(actual_arg_kinds[j] in (ArgKind.ARG_POS, ArgKind.ARG_STAR) for j in actuals):
+ if any(actual_arg_kinds[j] in (ARG_POS, ARG_STAR) for j in actuals):
# Don't add params for arguments passed positionally
continue
# Add defaulted params for arguments passed via keyword
kind = actual_arg_kinds[actuals[0]]
- if kind == ArgKind.ARG_NAMED or kind == ArgKind.ARG_STAR2:
- kind = ArgKind.ARG_NAMED_OPT
+ if kind == ARG_NAMED or kind == ARG_STAR2:
+ kind = ARG_NAMED_OPT
partial_kinds.append(kind)
partial_types.append(arg_type)
partial_names.append(fn_type.arg_names[i])
@@ -322,9 +321,9 @@
if partially_applied.param_spec():
assert ret.extra_attrs is not None # copy_with_extra_attr above ensures this
attrs = ret.extra_attrs.copy()
- if ArgKind.ARG_STAR in actual_arg_kinds:
+ if ARG_STAR in actual_arg_kinds:
attrs.immutable.add("__mypy_partial_paramspec_args_bound")
- if ArgKind.ARG_STAR2 in actual_arg_kinds:
+ if ARG_STAR2 in actual_arg_kinds:
attrs.immutable.add("__mypy_partial_paramspec_kwargs_bound")
ret.extra_attrs = attrs
return ret
diff --git a/mypy/semanal.py b/mypy/semanal.py
index 01b7f49..14604f9 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1041,7 +1041,7 @@
self.pop_type_args(defn.type_args)
def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType:
- if not typ.arg_kinds or typ.arg_kinds[-1] is not ArgKind.ARG_STAR2:
+ if not typ.arg_kinds or typ.arg_kinds[-1] is not ARG_STAR2:
return typ
last_type = typ.arg_types[-1]
if not isinstance(last_type, UnpackType):
diff --git a/mypy/types.py b/mypy/types.py
index 05b02ac..51f9c1b 100644
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -22,6 +22,8 @@
import mypy.nodes
from mypy.bogus_type import Bogus
from mypy.nodes import (
+ ARG_NAMED,
+ ARG_NAMED_OPT,
ARG_POS,
ARG_STAR,
ARG_STAR2,
@@ -1782,7 +1784,7 @@
return {
".class": "Parameters",
"arg_types": [t.serialize() for t in self.arg_types],
- "arg_kinds": [int(x.value) for x in self.arg_kinds],
+ "arg_kinds": [x.value for x in self.arg_kinds],
"arg_names": self.arg_names,
"variables": [tv.serialize() for tv in self.variables],
"imprecise_arg_kinds": self.imprecise_arg_kinds,
@@ -1793,7 +1795,7 @@
assert data[".class"] == "Parameters"
return Parameters(
[deserialize_type(t) for t in data["arg_types"]],
- [ArgKind(x) for x in data["arg_kinds"]],
+ [ArgKind.by_value(x) for x in data["arg_kinds"]],
data["arg_names"],
variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]],
imprecise_arg_kinds=data["imprecise_arg_kinds"],
@@ -2162,7 +2164,7 @@
last_type = get_proper_type(self.arg_types[-1])
assert isinstance(last_type, TypedDictType)
extra_kinds = [
- ArgKind.ARG_NAMED if name in last_type.required_keys else ArgKind.ARG_NAMED_OPT
+ ARG_NAMED if name in last_type.required_keys else ARG_NAMED_OPT
for name in last_type.items
]
new_arg_kinds = self.arg_kinds[:-1] + extra_kinds
@@ -2283,7 +2285,7 @@
return {
".class": "CallableType",
"arg_types": [t.serialize() for t in self.arg_types],
- "arg_kinds": [int(x.value) for x in self.arg_kinds],
+ "arg_kinds": [x.value for x in self.arg_kinds],
"arg_names": self.arg_names,
"ret_type": self.ret_type.serialize(),
"fallback": self.fallback.serialize(),
@@ -2307,7 +2309,7 @@
# TODO: Set definition to the containing SymbolNode?
return CallableType(
[deserialize_type(t) for t in data["arg_types"]],
- [ArgKind(x) for x in data["arg_kinds"]],
+ [ArgKind.by_value(x) for x in data["arg_kinds"]],
data["arg_names"],
deserialize_type(data["ret_type"]),
Instance.deserialize(data["fallback"]),
diff --git a/mypyc/codegen/emitwrapper.py b/mypyc/codegen/emitwrapper.py
index cd16842..ec91ff4 100644
--- a/mypyc/codegen/emitwrapper.py
+++ b/mypyc/codegen/emitwrapper.py
@@ -14,7 +14,16 @@
from collections.abc import Sequence
-from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, ARG_OPT, ARG_POS, ARG_STAR, ARG_STAR2, ArgKind
+from mypy.nodes import (
+ ALL_ARG_KINDS,
+ ARG_NAMED,
+ ARG_NAMED_OPT,
+ ARG_OPT,
+ ARG_POS,
+ ARG_STAR,
+ ARG_STAR2,
+ ArgKind,
+)
from mypy.operators import op_methods_to_symbols, reverse_op_method_names, reverse_op_methods
from mypyc.codegen.emit import AssignHandler, Emitter, ErrorHandler, GotoHandler, ReturnHandler
from mypyc.common import (
@@ -88,7 +97,7 @@
def make_arg_groups(args: list[RuntimeArg]) -> dict[ArgKind, list[RuntimeArg]]:
"""Group arguments by kind."""
- return {k: [arg for arg in args if arg.kind == k] for k in ArgKind}
+ return {k: [arg for arg in args if arg.kind == k] for k in ALL_ARG_KINDS}
def reorder_arg_groups(groups: dict[ArgKind, list[RuntimeArg]]) -> list[RuntimeArg]:
diff --git a/mypyc/ir/func_ir.py b/mypyc/ir/func_ir.py
index 881ac59..83c0712 100644
--- a/mypyc/ir/func_ir.py
+++ b/mypyc/ir/func_ir.py
@@ -6,7 +6,17 @@
from collections.abc import Sequence
from typing import Final
-from mypy.nodes import ARG_POS, ArgKind, Block, FuncDef
+from mypy.nodes import (
+ ARG_NAMED,
+ ARG_NAMED_OPT,
+ ARG_OPT,
+ ARG_POS,
+ ARG_STAR,
+ ARG_STAR2,
+ ArgKind,
+ Block,
+ FuncDef,
+)
from mypyc.common import BITMAP_BITS, JsonDict, bitmap_name, get_id_from_name, short_id_from_name
from mypyc.ir.ops import (
Assign,
@@ -60,7 +70,7 @@
return {
"name": self.name,
"type": self.type.serialize(),
- "kind": int(self.kind.value),
+ "kind": self.kind.value,
"pos_only": self.pos_only,
}
@@ -69,7 +79,7 @@
return RuntimeArg(
data["name"],
deserialize_type(data["type"], ctx),
- ArgKind(data["kind"]),
+ ArgKind.by_value(data["kind"]),
data["pos_only"],
)
@@ -394,12 +404,12 @@
_ARG_KIND_TO_INSPECT: Final = {
- ArgKind.ARG_POS: inspect.Parameter.POSITIONAL_OR_KEYWORD,
- ArgKind.ARG_OPT: inspect.Parameter.POSITIONAL_OR_KEYWORD,
- ArgKind.ARG_STAR: inspect.Parameter.VAR_POSITIONAL,
- ArgKind.ARG_NAMED: inspect.Parameter.KEYWORD_ONLY,
- ArgKind.ARG_STAR2: inspect.Parameter.VAR_KEYWORD,
- ArgKind.ARG_NAMED_OPT: inspect.Parameter.KEYWORD_ONLY,
+ ARG_POS: inspect.Parameter.POSITIONAL_OR_KEYWORD,
+ ARG_OPT: inspect.Parameter.POSITIONAL_OR_KEYWORD,
+ ARG_STAR: inspect.Parameter.VAR_POSITIONAL,
+ ARG_NAMED: inspect.Parameter.KEYWORD_ONLY,
+ ARG_STAR2: inspect.Parameter.VAR_KEYWORD,
+ ARG_NAMED_OPT: inspect.Parameter.KEYWORD_ONLY,
}
# Sentinel indicating a value that cannot be represented in a text signature.
@@ -418,7 +428,7 @@
# currently sees 'self' as being positional-or-keyword and '__x' as positional-only.
pos_only_idx = -1
for idx, arg in enumerate(sig.args):
- if arg.pos_only and arg.kind in (ArgKind.ARG_POS, ArgKind.ARG_OPT):
+ if arg.pos_only and arg.kind in (ARG_POS, ARG_OPT):
pos_only_idx = idx
for idx, arg in enumerate(sig.args):
if arg.name.startswith(("__bitmap", "__mypyc")):
diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py
index 90506ad..ac4b844 100644
--- a/mypyc/irbuild/function.py
+++ b/mypyc/irbuild/function.py
@@ -17,6 +17,8 @@
from typing import NamedTuple
from mypy.nodes import (
+ ARG_OPT,
+ ARG_POS,
ArgKind,
ClassDef,
Decorator,
@@ -907,7 +909,7 @@
decl = builder.mapper.func_to_decl[fitem]
arg_info = get_args(builder, decl.sig.args, line)
args = [callable_class] + arg_info.args
- arg_kinds = [ArgKind.ARG_POS] + arg_info.arg_kinds
+ arg_kinds = [ARG_POS] + arg_info.arg_kinds
arg_names = arg_info.arg_names
arg_names.insert(0, "self")
ret_val = builder.builder.call(callable_class_decl, args, arg_kinds, arg_names, line)
@@ -935,7 +937,7 @@
line = -1
with builder.enter_method(fn_info.callable_class.ir, "register", object_rprimitive):
cls_arg = builder.add_argument("cls", object_rprimitive)
- func_arg = builder.add_argument("func", object_rprimitive, ArgKind.ARG_OPT)
+ func_arg = builder.add_argument("func", object_rprimitive, ARG_OPT)
ret_val = builder.call_c(register_function, [builder.self(), cls_arg, func_arg], line)
builder.add(Return(ret_val, line))