Skip to content

Commit e778a58

Browse files
authored
[mypyc] Support type narrowing of native int types using "int" (#14524)
Now `isinstance(x, int)` can be used to narrow a union type that includes a native int type. In mypyc unions there is no runtime distinction between different integer types -- everything is represented at runtime as boxed `int` values anyway. Also test narrowing a native int using the same native int type. Work on mypyc/mypyc#837.
1 parent bac9e77 commit e778a58

File tree

8 files changed

+150
-15
lines changed

8 files changed

+150
-15
lines changed

mypy/checker.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
)
179179
from mypy.types import (
180180
ANY_STRATEGY,
181+
MYPYC_NATIVE_INT_NAMES,
181182
OVERLOAD_NAMES,
182183
AnyType,
183184
BoolTypeQuery,
@@ -4517,10 +4518,7 @@ def analyze_range_native_int_type(self, expr: Expression) -> Type | None:
45174518
ok = True
45184519
for arg in expr.args:
45194520
argt = get_proper_type(self.lookup_type(arg))
4520-
if isinstance(argt, Instance) and argt.type.fullname in (
4521-
"mypy_extensions.i64",
4522-
"mypy_extensions.i32",
4523-
):
4521+
if isinstance(argt, Instance) and argt.type.fullname in MYPYC_NATIVE_INT_NAMES:
45244522
if native_int is None:
45254523
native_int = argt
45264524
elif argt != native_int:

mypy/meet.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from mypy.typeops import is_recursive_pair, make_simplified_union, tuple_fallback
1717
from mypy.types import (
18+
MYPYC_NATIVE_INT_NAMES,
1819
AnyType,
1920
CallableType,
2021
DeletedType,
@@ -475,6 +476,9 @@ def _type_object_overlap(left: Type, right: Type) -> bool:
475476
):
476477
return True
477478

479+
if right.type.fullname == "builtins.int" and left.type.fullname in MYPYC_NATIVE_INT_NAMES:
480+
return True
481+
478482
# Two unrelated types cannot be partially overlapping: they're disjoint.
479483
if left.type.has_base(right.type.fullname):
480484
left = map_instance_to_supertype(left, right.type)

mypy/semanal_classprop.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
Var,
2323
)
2424
from mypy.options import Options
25-
from mypy.types import Instance, ProperType
25+
from mypy.types import MYPYC_NATIVE_INT_NAMES, Instance, ProperType
2626

2727
# Hard coded type promotions (shared between all Python versions).
2828
# These add extra ad-hoc edges to the subtyping relation. For example,
@@ -177,7 +177,7 @@ def add_type_promotion(
177177
# Special case the promotions between 'int' and native integer types.
178178
# These have promotions going both ways, such as from 'int' to 'i64'
179179
# and 'i64' to 'int', for convenience.
180-
if defn.fullname == "mypy_extensions.i64" or defn.fullname == "mypy_extensions.i32":
180+
if defn.fullname in MYPYC_NATIVE_INT_NAMES:
181181
int_sym = builtin_names["int"]
182182
assert isinstance(int_sym.node, TypeInfo)
183183
int_sym.node._promote.append(Instance(defn.info, []))

mypy/subtypes.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from mypy.options import Options
2828
from mypy.state import state
2929
from mypy.types import (
30+
MYPYC_NATIVE_INT_NAMES,
3031
TUPLE_LIKE_INSTANCE_NAMES,
3132
TYPED_NAMEDTUPLE_NAMES,
3233
AnyType,
@@ -1793,14 +1794,19 @@ def covers_at_runtime(item: Type, supertype: Type) -> bool:
17931794
erase_type(item), supertype, ignore_promotions=True, erase_instances=True
17941795
):
17951796
return True
1796-
if isinstance(supertype, Instance) and supertype.type.is_protocol:
1797-
# TODO: Implement more robust support for runtime isinstance() checks, see issue #3827.
1798-
if is_proper_subtype(item, supertype, ignore_promotions=True):
1799-
return True
1800-
if isinstance(item, TypedDictType) and isinstance(supertype, Instance):
1801-
# Special case useful for selecting TypedDicts from unions using isinstance(x, dict).
1802-
if supertype.type.fullname == "builtins.dict":
1803-
return True
1797+
if isinstance(supertype, Instance):
1798+
if supertype.type.is_protocol:
1799+
# TODO: Implement more robust support for runtime isinstance() checks, see issue #3827.
1800+
if is_proper_subtype(item, supertype, ignore_promotions=True):
1801+
return True
1802+
if isinstance(item, TypedDictType):
1803+
# Special case useful for selecting TypedDicts from unions using isinstance(x, dict).
1804+
if supertype.type.fullname == "builtins.dict":
1805+
return True
1806+
elif isinstance(item, Instance) and supertype.type.fullname == "builtins.int":
1807+
# "int" covers all native int types
1808+
if item.type.fullname in MYPYC_NATIVE_INT_NAMES:
1809+
return True
18041810
# TODO: Add more special cases.
18051811
return False
18061812

mypy/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@
150150
"typing_extensions.Never",
151151
)
152152

153+
# Mypyc fixed-width native int types (compatible with builtins.int)
154+
MYPYC_NATIVE_INT_NAMES: Final = ("mypy_extensions.i64", "mypy_extensions.i32")
155+
153156
DATACLASS_TRANSFORM_NAMES: Final = (
154157
"typing.dataclass_transform",
155158
"typing_extensions.dataclass_transform",

mypyc/test-data/irbuild-i64.test

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,3 +1834,67 @@ L0:
18341834
r0 = CPyLong_FromFloat(x)
18351835
r1 = unbox(int64, r0)
18361836
return r1
1837+
1838+
[case testI64IsinstanceNarrowing]
1839+
from typing import Union
1840+
from mypy_extensions import i64
1841+
1842+
class C:
1843+
a: i64
1844+
1845+
def narrow1(x: Union[C, i64]) -> i64:
1846+
if isinstance(x, i64):
1847+
return x
1848+
return x.a
1849+
1850+
def narrow2(x: Union[C, i64]) -> i64:
1851+
if isinstance(x, int):
1852+
return x
1853+
return x.a
1854+
[out]
1855+
def narrow1(x):
1856+
x :: union[__main__.C, int64]
1857+
r0 :: object
1858+
r1 :: int32
1859+
r2 :: bit
1860+
r3 :: bool
1861+
r4 :: int64
1862+
r5 :: __main__.C
1863+
r6 :: int64
1864+
L0:
1865+
r0 = load_address PyLong_Type
1866+
r1 = PyObject_IsInstance(x, r0)
1867+
r2 = r1 >= 0 :: signed
1868+
r3 = truncate r1: int32 to builtins.bool
1869+
if r3 goto L1 else goto L2 :: bool
1870+
L1:
1871+
r4 = unbox(int64, x)
1872+
return r4
1873+
L2:
1874+
r5 = borrow cast(__main__.C, x)
1875+
r6 = r5.a
1876+
keep_alive x
1877+
return r6
1878+
def narrow2(x):
1879+
x :: union[__main__.C, int64]
1880+
r0 :: object
1881+
r1 :: int32
1882+
r2 :: bit
1883+
r3 :: bool
1884+
r4 :: int64
1885+
r5 :: __main__.C
1886+
r6 :: int64
1887+
L0:
1888+
r0 = load_address PyLong_Type
1889+
r1 = PyObject_IsInstance(x, r0)
1890+
r2 = r1 >= 0 :: signed
1891+
r3 = truncate r1: int32 to builtins.bool
1892+
if r3 goto L1 else goto L2 :: bool
1893+
L1:
1894+
r4 = unbox(int64, x)
1895+
return r4
1896+
L2:
1897+
r5 = borrow cast(__main__.C, x)
1898+
r6 = r5.a
1899+
keep_alive x
1900+
return r6

mypyc/test-data/run-i64.test

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[case testI64BasicOps]
2-
from typing import List, Any, Tuple
2+
from typing import List, Any, Tuple, Union
33

44
MYPY = False
55
if MYPY:
@@ -497,6 +497,22 @@ def test_for_loop() -> None:
497497
assert n == 9
498498
assert sum([x * x for x in range(i64(4 + int()))]) == 1 + 4 + 9
499499

500+
def narrow1(x: Union[str, i64]) -> i64:
501+
if isinstance(x, i64):
502+
return x
503+
return len(x)
504+
505+
def narrow2(x: Union[str, i64]) -> i64:
506+
if isinstance(x, int):
507+
return x
508+
return len(x)
509+
510+
def test_isinstance() -> None:
511+
assert narrow1(123) == 123
512+
assert narrow1("foobar") == 6
513+
assert narrow2(123) == 123
514+
assert narrow2("foobar") == 6
515+
500516
[case testI64ErrorValuesAndUndefined]
501517
from typing import Any, Tuple
502518
import sys

test-data/unit/check-native-int.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,47 @@ from mypy_extensions import i64, i32
184184
reveal_type([a for a in range(i64(5))]) # N: Revealed type is "builtins.list[mypy_extensions.i64]"
185185
[reveal_type(a) for a in range(0, i32(5))] # N: Revealed type is "mypy_extensions.i32"
186186
[builtins fixtures/primitives.pyi]
187+
188+
[case testNativeIntNarrowing]
189+
from typing import Union
190+
from mypy_extensions import i64, i32
191+
192+
def narrow_i64(x: Union[str, i64]) -> None:
193+
if isinstance(x, i64):
194+
reveal_type(x) # N: Revealed type is "mypy_extensions.i64"
195+
else:
196+
reveal_type(x) # N: Revealed type is "builtins.str"
197+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i64]"
198+
199+
if isinstance(x, str):
200+
reveal_type(x) # N: Revealed type is "builtins.str"
201+
else:
202+
reveal_type(x) # N: Revealed type is "mypy_extensions.i64"
203+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i64]"
204+
205+
if isinstance(x, int):
206+
reveal_type(x) # N: Revealed type is "mypy_extensions.i64"
207+
else:
208+
reveal_type(x) # N: Revealed type is "builtins.str"
209+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i64]"
210+
211+
def narrow_i32(x: Union[str, i32]) -> None:
212+
if isinstance(x, i32):
213+
reveal_type(x) # N: Revealed type is "mypy_extensions.i32"
214+
else:
215+
reveal_type(x) # N: Revealed type is "builtins.str"
216+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i32]"
217+
218+
if isinstance(x, str):
219+
reveal_type(x) # N: Revealed type is "builtins.str"
220+
else:
221+
reveal_type(x) # N: Revealed type is "mypy_extensions.i32"
222+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i32]"
223+
224+
if isinstance(x, int):
225+
reveal_type(x) # N: Revealed type is "mypy_extensions.i32"
226+
else:
227+
reveal_type(x) # N: Revealed type is "builtins.str"
228+
reveal_type(x) # N: Revealed type is "Union[builtins.str, mypy_extensions.i32]"
229+
230+
[builtins fixtures/primitives.pyi]

0 commit comments

Comments
 (0)