Skip to content

Commit dc03478

Browse files
authored
[mypyc] Enable native integers outside tests (#14606)
I think that native integers work well enough to enable them outside tests. Require a more recent `mypy_extensions` that includes native int types, including `i64` and `i32`. Add definitions of `i64` and `i32` to the bundled stubs for `mypy_extensions`. Fork the stubs, since the definitions only make sense for mypy/mypyc. They require custom type checking logic. Other tools can treat these as aliases to `int`, which was implemented here: python/typeshed#9675 Also fix serialization of native int TypeInfos. Since we patch `builtins.int` when we process `mypy_extensions`, the patched information may not be serialized. We'll also need to perform similar patching during deserialization. Here is the performance impact to some benchmarks when using `i64` instead of `int` types (these assume some additional tweaks that should be ready soon): * richards: 33% faster * hexiom: 18% faster * deltablue: 2.6% faster Perhaps more importantly, native integers help with upcoming low-level features, such as packed arrays. Closes mypyc/mypyc#837. Remaining work can be tracked in separate issues.
1 parent 6787e51 commit dc03478

16 files changed

+154
-43
lines changed

misc/sync-typeshed.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ def check_state() -> None:
3535
def update_typeshed(typeshed_dir: str, commit: str | None) -> str:
3636
"""Update contents of local typeshed copy.
3737
38+
We maintain our own separate mypy_extensions stubs, since it's
39+
treated specially by mypy and we make assumptions about what's there.
40+
We don't sync mypy_extensions stubs here -- this is done manually.
41+
3842
Return the normalized typeshed commit hash.
3943
"""
4044
assert os.path.isdir(os.path.join(typeshed_dir, "stdlib"))
41-
assert os.path.isdir(os.path.join(typeshed_dir, "stubs"))
4245
if commit:
4346
subprocess.run(["git", "checkout", commit], check=True, cwd=typeshed_dir)
4447
commit = git_head_commit(typeshed_dir)
@@ -48,15 +51,6 @@ def update_typeshed(typeshed_dir: str, commit: str | None) -> str:
4851
shutil.rmtree(stdlib_dir)
4952
# Copy new stdlib stubs.
5053
shutil.copytree(os.path.join(typeshed_dir, "stdlib"), stdlib_dir)
51-
# Copy mypy_extensions stubs. We don't want to use a stub package, since it's
52-
# treated specially by mypy and we make assumptions about what's there.
53-
stubs_dir = os.path.join("mypy", "typeshed", "stubs")
54-
shutil.rmtree(stubs_dir)
55-
os.makedirs(stubs_dir)
56-
shutil.copytree(
57-
os.path.join(typeshed_dir, "stubs", "mypy-extensions"),
58-
os.path.join(stubs_dir, "mypy-extensions"),
59-
)
6054
shutil.copy(os.path.join(typeshed_dir, "LICENSE"), os.path.join("mypy", "typeshed"))
6155
return commit
6256

mypy-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# NOTE: this needs to be kept in sync with the "requires" list in pyproject.toml
22
typing_extensions>=3.10
3-
mypy_extensions>=0.4.3
3+
mypy_extensions>=1.0.0
44
typed_ast>=1.4.0,<2; python_version<'3.8'
55
tomli>=1.1.0; python_version<'3.11'

mypy/checkexpr.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -3354,7 +3354,10 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
33543354
is_subtype(right_type, left_type)
33553355
and isinstance(left_type, Instance)
33563356
and isinstance(right_type, Instance)
3357-
and left_type.type.alt_promote is not right_type.type
3357+
and not (
3358+
left_type.type.alt_promote is not None
3359+
and left_type.type.alt_promote.type is right_type.type
3360+
)
33583361
and lookup_definer(left_type, op_name) != lookup_definer(right_type, rev_op_name)
33593362
):
33603363
# When we do "A() + B()" where B is a subclass of A, we'll actually try calling

mypy/fixup.py

+7
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ def visit_type_info(self, info: TypeInfo) -> None:
8787
info.declared_metaclass.accept(self.type_fixer)
8888
if info.metaclass_type:
8989
info.metaclass_type.accept(self.type_fixer)
90+
if info.alt_promote:
91+
info.alt_promote.accept(self.type_fixer)
92+
instance = Instance(info, [])
93+
# Hack: We may also need to add a backwards promotion (from int to native int),
94+
# since it might not be serialized.
95+
if instance not in info.alt_promote.type._promote:
96+
info.alt_promote.type._promote.append(instance)
9097
if info._mro_refs:
9198
info.mro = [
9299
lookup_fully_qualified_typeinfo(

mypy/meet.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
167167
if (
168168
isinstance(narrowed, Instance)
169169
and narrowed.type.alt_promote
170-
and narrowed.type.alt_promote is declared.type
170+
and narrowed.type.alt_promote.type is declared.type
171171
):
172172
# Special case: 'int' can't be narrowed down to a native int type such as
173173
# i64, since they have different runtime representations.
@@ -715,10 +715,10 @@ def visit_instance(self, t: Instance) -> ProperType:
715715
return NoneType()
716716
else:
717717
alt_promote = t.type.alt_promote
718-
if alt_promote and alt_promote is self.s.type:
718+
if alt_promote and alt_promote.type is self.s.type:
719719
return t
720720
alt_promote = self.s.type.alt_promote
721-
if alt_promote and alt_promote is t.type:
721+
if alt_promote and alt_promote.type is t.type:
722722
return self.s
723723
if is_subtype(t, self.s):
724724
return t

mypy/nodes.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -2932,7 +2932,7 @@ class is generic then it will be a type constructor of higher kind.
29322932
# This results in some unintuitive results, such as that even
29332933
# though i64 is compatible with int and int is compatible with
29342934
# float, i64 is *not* compatible with float.
2935-
alt_promote: TypeInfo | None
2935+
alt_promote: mypy.types.Instance | None
29362936

29372937
# Representation of a Tuple[...] base class, if the class has any
29382938
# (e.g., for named tuples). If this is not None, the actual Type
@@ -3230,6 +3230,7 @@ def serialize(self) -> JsonDict:
32303230
"bases": [b.serialize() for b in self.bases],
32313231
"mro": [c.fullname for c in self.mro],
32323232
"_promote": [p.serialize() for p in self._promote],
3233+
"alt_promote": None if self.alt_promote is None else self.alt_promote.serialize(),
32333234
"declared_metaclass": (
32343235
None if self.declared_metaclass is None else self.declared_metaclass.serialize()
32353236
),
@@ -3266,6 +3267,11 @@ def deserialize(cls, data: JsonDict) -> TypeInfo:
32663267
assert isinstance(t, mypy.types.ProperType)
32673268
_promote.append(t)
32683269
ti._promote = _promote
3270+
ti.alt_promote = (
3271+
None
3272+
if data["alt_promote"] is None
3273+
else mypy.types.Instance.deserialize(data["alt_promote"])
3274+
)
32693275
ti.declared_metaclass = (
32703276
None
32713277
if data["declared_metaclass"] is None

mypy/semanal_classprop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,6 @@ def add_type_promotion(
181181
int_sym = builtin_names["int"]
182182
assert isinstance(int_sym.node, TypeInfo)
183183
int_sym.node._promote.append(Instance(defn.info, []))
184-
defn.info.alt_promote = int_sym.node
184+
defn.info.alt_promote = Instance(int_sym.node, [])
185185
if promote_targets:
186186
defn.info._promote.extend(promote_targets)

mypy/subtypes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def visit_instance(self, left: Instance) -> bool:
455455
# Special case: Low-level integer types are compatible with 'int'. We can't
456456
# use promotions, since 'int' is already promoted to low-level integer types,
457457
# and we can't have circular promotions.
458-
if left.type.alt_promote is right.type:
458+
if left.type.alt_promote and left.type.alt_promote.type is right.type:
459459
return True
460460
rname = right.type.fullname
461461
# Always try a nominal check if possible,
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = "0.4.*"
1+
version = "1.0.*"
22

33
[tool.stubtest]
44
ignore_missing_stub = false

mypy/typeshed/stubs/mypy-extensions/mypy_extensions.pyi

+80-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
# These stubs are forked from typeshed, since we use some definitions that only make
2+
# sense in the context of mypy/mypyc (in particular, native int types such as i64).
3+
14
import abc
25
import sys
36
from _collections_abc import dict_items, dict_keys, dict_values
47
from _typeshed import IdentityFunction, Self
58
from collections.abc import Mapping
6-
from typing import Any, ClassVar, Generic, TypeVar, overload, type_check_only
7-
from typing_extensions import Never
9+
from typing import Any, ClassVar, Generic, SupportsInt, TypeVar, overload, type_check_only
10+
from typing_extensions import Never, SupportsIndex
11+
from _typeshed import ReadableBuffer, SupportsTrunc
812

913
_T = TypeVar("_T")
1014
_U = TypeVar("_U")
@@ -68,3 +72,77 @@ def trait(cls: _T) -> _T: ...
6872
def mypyc_attr(*attrs: str, **kwattrs: object) -> IdentityFunction: ...
6973

7074
class FlexibleAlias(Generic[_T, _U]): ...
75+
76+
# Native int types such as i64 are magical and support implicit
77+
# coercions to/from int using special logic in mypy. We generally only
78+
# include operations here for which we have specialized primitives.
79+
80+
class i64:
81+
@overload
82+
def __new__(cls, __x: str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc = ...) -> i64: ...
83+
@overload
84+
def __new__(cls, __x: str | bytes | bytearray, base: SupportsIndex) -> i64: ...
85+
86+
def __add__(self, x: i64) -> i64: ...
87+
def __radd__(self, x: i64) -> i64: ...
88+
def __sub__(self, x: i64) -> i64: ...
89+
def __rsub__(self, x: i64) -> i64: ...
90+
def __mul__(self, x: i64) -> i64: ...
91+
def __rmul__(self, x: i64) -> i64: ...
92+
def __floordiv__(self, x: i64) -> i64: ...
93+
def __rfloordiv__(self, x: i64) -> i64: ...
94+
def __mod__(self, x: i64) -> i64: ...
95+
def __rmod__(self, x: i64) -> i64: ...
96+
def __and__(self, x: i64) -> i64: ...
97+
def __rand__(self, x: i64) -> i64: ...
98+
def __or__(self, x: i64) -> i64: ...
99+
def __ror__(self, x: i64) -> i64: ...
100+
def __xor__(self, x: i64) -> i64: ...
101+
def __rxor__(self, x: i64) -> i64: ...
102+
def __lshift__(self, x: i64) -> i64: ...
103+
def __rlshift__(self, x: i64) -> i64: ...
104+
def __rshift__(self, x: i64) -> i64: ...
105+
def __rrshift__(self, x: i64) -> i64: ...
106+
def __neg__(self) -> i64: ...
107+
def __invert__(self) -> i64: ...
108+
def __pos__(self) -> i64: ...
109+
def __lt__(self, x: i64) -> bool: ...
110+
def __le__(self, x: i64) -> bool: ...
111+
def __ge__(self, x: i64) -> bool: ...
112+
def __gt__(self, x: i64) -> bool: ...
113+
def __index__(self) -> int: ...
114+
115+
class i32:
116+
@overload
117+
def __new__(cls, __x: str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc = ...) -> i32: ...
118+
@overload
119+
def __new__(cls, __x: str | bytes | bytearray, base: SupportsIndex) -> i32: ...
120+
121+
def __add__(self, x: i32) -> i32: ...
122+
def __radd__(self, x: i32) -> i32: ...
123+
def __sub__(self, x: i32) -> i32: ...
124+
def __rsub__(self, x: i32) -> i32: ...
125+
def __mul__(self, x: i32) -> i32: ...
126+
def __rmul__(self, x: i32) -> i32: ...
127+
def __floordiv__(self, x: i32) -> i32: ...
128+
def __rfloordiv__(self, x: i32) -> i32: ...
129+
def __mod__(self, x: i32) -> i32: ...
130+
def __rmod__(self, x: i32) -> i32: ...
131+
def __and__(self, x: i32) -> i32: ...
132+
def __rand__(self, x: i32) -> i32: ...
133+
def __or__(self, x: i32) -> i32: ...
134+
def __ror__(self, x: i32) -> i32: ...
135+
def __xor__(self, x: i32) -> i32: ...
136+
def __rxor__(self, x: i32) -> i32: ...
137+
def __lshift__(self, x: i32) -> i32: ...
138+
def __rlshift__(self, x: i32) -> i32: ...
139+
def __rshift__(self, x: i32) -> i32: ...
140+
def __rrshift__(self, x: i32) -> i32: ...
141+
def __neg__(self) -> i32: ...
142+
def __invert__(self) -> i32: ...
143+
def __pos__(self) -> i32: ...
144+
def __lt__(self, x: i32) -> bool: ...
145+
def __le__(self, x: i32) -> bool: ...
146+
def __ge__(self, x: i32) -> bool: ...
147+
def __gt__(self, x: i32) -> bool: ...
148+
def __index__(self) -> int: ...

mypyc/test-data/run-i32.test

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
[case testI32BasicOps]
22
from typing import Any, Tuple
33

4-
MYPY = False
5-
if MYPY:
6-
from mypy_extensions import i32, i64
4+
from mypy_extensions import i32, i64
75

86
from testutil import assertRaises
97

mypyc/test-data/run-i64.test

+5-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
[case testI64BasicOps]
22
from typing import List, Any, Tuple, Union
33

4-
MYPY = False
5-
if MYPY:
6-
from mypy_extensions import i64, i32
4+
from mypy_extensions import i64, i32
75

86
from testutil import assertRaises
97

@@ -517,13 +515,9 @@ def test_isinstance() -> None:
517515
from typing import Any, Tuple
518516
import sys
519517

520-
from mypy_extensions import mypyc_attr
518+
from mypy_extensions import mypyc_attr, i64
521519
from typing_extensions import Final
522520

523-
MYPY = False
524-
if MYPY:
525-
from mypy_extensions import i64
526-
527521
from testutil import assertRaises
528522

529523
def maybe_raise(n: i64, error: bool) -> i64:
@@ -911,9 +905,7 @@ from typing_extensions import Final
911905

912906
MAGIC: Final = -113
913907

914-
MYPY = False
915-
if MYPY:
916-
from mypy_extensions import i64
908+
from mypy_extensions import i64
917909

918910
def f(x: i64, y: i64 = 5) -> i64:
919911
return x + y
@@ -1211,9 +1203,7 @@ def test_magic_default() -> None:
12111203
[case testI64UndefinedLocal]
12121204
from typing_extensions import Final
12131205

1214-
MYPY = False
1215-
if MYPY:
1216-
from mypy_extensions import i64, i32
1206+
from mypy_extensions import i64, i32
12171207

12181208
from testutil import assertRaises
12191209

@@ -1346,9 +1336,7 @@ def test_many_locals() -> None:
13461336
from typing import Any
13471337
from typing_extensions import Final
13481338

1349-
MYPY = False
1350-
if MYPY:
1351-
from mypy_extensions import i64, trait
1339+
from mypy_extensions import i64, trait
13521340

13531341
from testutil import assertRaises
13541342

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires = [
77
"wheel >= 0.30.0",
88
# the following is from mypy-requirements.txt
99
"typing_extensions>=3.10",
10-
"mypy_extensions>=0.4.3",
10+
"mypy_extensions>=1.0.0",
1111
"typed_ast>=1.4.0,<2; python_version<'3.8'",
1212
"tomli>=1.1.0; python_version<'3.11'",
1313
# the following is from build-requirements.txt

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def run(self):
213213
install_requires=[
214214
"typed_ast >= 1.4.0, < 2; python_version<'3.8'",
215215
"typing_extensions>=3.10",
216-
"mypy_extensions >= 0.4.3",
216+
"mypy_extensions >= 1.0.0",
217217
"tomli>=1.1.0; python_version<'3.11'",
218218
],
219219
# Same here.

test-data/unit/check-incremental.test

+13
Original file line numberDiff line numberDiff line change
@@ -6359,3 +6359,16 @@ from m import Foo
63596359
[file m.py]
63606360
from missing_module import Meta # type: ignore[import]
63616361
class Foo(metaclass=Meta): ...
6362+
6363+
[case testIncrementalNativeInt]
6364+
import a
6365+
[file a.py]
6366+
from mypy_extensions import i64
6367+
x: i64 = 0
6368+
[file a.py.2]
6369+
from mypy_extensions import i64
6370+
x: i64 = 0
6371+
y: int = x
6372+
[builtins fixtures/tuple.pyi]
6373+
[out]
6374+
[out2]

test-data/unit/pythoneval.test

+24
Original file line numberDiff line numberDiff line change
@@ -1877,3 +1877,27 @@ for value in enum_iter(socket.SocketKind):
18771877
_testEnumIterMetaInference.py:8: note: Revealed type is "typing.Iterator[_E`-1]"
18781878
_testEnumIterMetaInference.py:9: note: Revealed type is "_E`-1"
18791879
_testEnumIterMetaInference.py:13: note: Revealed type is "socket.SocketKind"
1880+
1881+
[case testNativeIntTypes]
1882+
# Spot check various native int operations with full stubs.
1883+
from mypy_extensions import i64, i32
1884+
1885+
x: i64 = 0
1886+
y: int = x
1887+
x = i64(0)
1888+
y = int(x)
1889+
i64()
1890+
i64("12")
1891+
i64("ab", 16)
1892+
i64(1.2)
1893+
float(i64(1))
1894+
1895+
i64(1) + i32(2) # Error
1896+
reveal_type(x + y)
1897+
reveal_type(y + x)
1898+
a = [0]
1899+
a[x]
1900+
[out]
1901+
_testNativeIntTypes.py:14: error: Unsupported operand types for + ("i64" and "i32")
1902+
_testNativeIntTypes.py:15: note: Revealed type is "mypy_extensions.i64"
1903+
_testNativeIntTypes.py:16: note: Revealed type is "mypy_extensions.i64"

0 commit comments

Comments
 (0)