Skip to content

Commit c8e5278

Browse files
authored
[mypyc] Generate efficient code for (some) conversions i64(x) and i32(x) (#13621)
These are now optimized for i32, i64 and int arguments. `i32(x)` truncates from `i64`, but does a range check when converting from `int`. The rationale is that implicit conversions from `int` perform range checks to avoid silently corrupting data, and explicit coercions use the same semantics. However, conversions from `i64` to `i32` must be explicit and thus there is no implicit corruption possible. Truncation is also a very fast operation, which we generally prefer when working purely on native integers. A range check would introduce some overhead. I'm not sure if this is the best approach, however, and this feels a bit inconsistent. I'll add optimized conversions from float in another PR once we support unboxed floats. Conversions from other types could also be improved in the future. Work on mypyc/mypyc#837.
1 parent 8147b0c commit c8e5278

File tree

5 files changed

+281
-3
lines changed

5 files changed

+281
-3
lines changed

mypyc/irbuild/specialize.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,29 @@
3232
TupleExpr,
3333
)
3434
from mypy.types import AnyType, TypeOfAny
35-
from mypyc.ir.ops import BasicBlock, Integer, RaiseStandardError, Register, Unreachable, Value
35+
from mypyc.ir.ops import (
36+
BasicBlock,
37+
Extend,
38+
Integer,
39+
RaiseStandardError,
40+
Register,
41+
Truncate,
42+
Unreachable,
43+
Value,
44+
)
3645
from mypyc.ir.rtypes import (
3746
RInstance,
3847
RTuple,
3948
RType,
4049
bool_rprimitive,
4150
c_int_rprimitive,
4251
dict_rprimitive,
52+
int32_rprimitive,
53+
int64_rprimitive,
4354
is_dict_rprimitive,
55+
is_int32_rprimitive,
56+
is_int64_rprimitive,
57+
is_int_rprimitive,
4458
is_list_rprimitive,
4559
list_rprimitive,
4660
set_rprimitive,
@@ -640,3 +654,37 @@ def translate_fstring(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Va
640654

641655
return join_formatted_strings(builder, None, substitutions, expr.line)
642656
return None
657+
658+
659+
@specialize_function("mypy_extensions.i64")
660+
def translate_i64(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
661+
if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS:
662+
return None
663+
arg = expr.args[0]
664+
arg_type = builder.node_type(arg)
665+
if is_int64_rprimitive(arg_type):
666+
return builder.accept(arg)
667+
elif is_int32_rprimitive(arg_type):
668+
val = builder.accept(arg)
669+
return builder.add(Extend(val, int64_rprimitive, signed=True, line=expr.line))
670+
elif is_int_rprimitive(arg_type):
671+
val = builder.accept(arg)
672+
return builder.coerce(val, int64_rprimitive, expr.line)
673+
return None
674+
675+
676+
@specialize_function("mypy_extensions.i32")
677+
def translate_i32(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
678+
if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS:
679+
return None
680+
arg = expr.args[0]
681+
arg_type = builder.node_type(arg)
682+
if is_int32_rprimitive(arg_type):
683+
return builder.accept(arg)
684+
elif is_int64_rprimitive(arg_type):
685+
val = builder.accept(arg)
686+
return builder.add(Truncate(val, int32_rprimitive, line=expr.line))
687+
elif is_int_rprimitive(arg_type):
688+
val = builder.accept(arg)
689+
return builder.coerce(val, int32_rprimitive, expr.line)
690+
return None

mypyc/test-data/irbuild-i32.test

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,72 @@ L0:
411411
y = -127
412412
z = 12
413413
return 1
414+
415+
[case testI32ExplicitConversionFromNativeInt]
416+
from mypy_extensions import i64, i32
417+
418+
def from_i32(x: i32) -> i32:
419+
return i32(x)
420+
421+
def from_i64(x: i64) -> i32:
422+
return i32(x)
423+
[out]
424+
def from_i32(x):
425+
x :: int32
426+
L0:
427+
return x
428+
def from_i64(x):
429+
x :: int64
430+
r0 :: int32
431+
L0:
432+
r0 = truncate x: int64 to int32
433+
return r0
434+
435+
[case testI32ExplicitConversionFromInt_64bit]
436+
from mypy_extensions import i32
437+
438+
def f(x: int) -> i32:
439+
return i32(x)
440+
[out]
441+
def f(x):
442+
x :: int
443+
r0 :: native_int
444+
r1, r2, r3 :: bit
445+
r4 :: native_int
446+
r5, r6 :: int32
447+
L0:
448+
r0 = x & 1
449+
r1 = r0 == 0
450+
if r1 goto L1 else goto L4 :: bool
451+
L1:
452+
r2 = x < 4294967296 :: signed
453+
if r2 goto L2 else goto L4 :: bool
454+
L2:
455+
r3 = x >= -4294967296 :: signed
456+
if r3 goto L3 else goto L4 :: bool
457+
L3:
458+
r4 = x >> 1
459+
r5 = truncate r4: native_int to int32
460+
r6 = r5
461+
goto L5
462+
L4:
463+
CPyInt32_Overflow()
464+
unreachable
465+
L5:
466+
return r6
467+
468+
[case testI32ExplicitConversionFromLiteral]
469+
from mypy_extensions import i32
470+
471+
def f() -> None:
472+
x = i32(0)
473+
y = i32(11)
474+
z = i32(-3)
475+
[out]
476+
def f():
477+
x, y, z :: int32
478+
L0:
479+
x = 0
480+
y = 11
481+
z = -3
482+
return 1

mypyc/test-data/irbuild-i64.test

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,3 +1507,70 @@ L0:
15071507
r0 = c.m(0, 0)
15081508
r1 = c.m(6, 1)
15091509
return 1
1510+
1511+
[case testI64ExplicitConversionFromNativeInt]
1512+
from mypy_extensions import i64, i32
1513+
1514+
def from_i32(x: i32) -> i64:
1515+
return i64(x)
1516+
1517+
def from_i64(x: i64) -> i64:
1518+
return i64(x)
1519+
[out]
1520+
def from_i32(x):
1521+
x :: int32
1522+
r0 :: int64
1523+
L0:
1524+
r0 = extend signed x: int32 to int64
1525+
return r0
1526+
def from_i64(x):
1527+
x :: int64
1528+
L0:
1529+
return x
1530+
1531+
[case testI64ExplicitConversionFromInt_64bit]
1532+
from mypy_extensions import i64
1533+
1534+
def f(x: int) -> i64:
1535+
return i64(x)
1536+
[out]
1537+
def f(x):
1538+
x :: int
1539+
r0 :: native_int
1540+
r1 :: bit
1541+
r2, r3 :: int64
1542+
r4 :: ptr
1543+
r5 :: c_ptr
1544+
r6 :: int64
1545+
L0:
1546+
r0 = x & 1
1547+
r1 = r0 == 0
1548+
if r1 goto L1 else goto L2 :: bool
1549+
L1:
1550+
r2 = x >> 1
1551+
r3 = r2
1552+
goto L3
1553+
L2:
1554+
r4 = x ^ 1
1555+
r5 = r4
1556+
r6 = CPyLong_AsInt64(r5)
1557+
r3 = r6
1558+
keep_alive x
1559+
L3:
1560+
return r3
1561+
1562+
[case testI64ExplicitConversionFromLiteral]
1563+
from mypy_extensions import i64
1564+
1565+
def f() -> None:
1566+
x = i64(0)
1567+
y = i64(11)
1568+
z = i64(-3)
1569+
[out]
1570+
def f():
1571+
x, y, z :: int64
1572+
L0:
1573+
x = 0
1574+
y = 11
1575+
z = -3
1576+
return 1

mypyc/test-data/run-i32.test

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import Any, Tuple
33

44
MYPY = False
55
if MYPY:
6-
from mypy_extensions import i32
6+
from mypy_extensions import i32, i64
77

88
from testutil import assertRaises
99

@@ -252,6 +252,60 @@ def test_coerce_to_and_from_int() -> None:
252252
m: int = x
253253
assert m == n
254254

255+
def test_explicit_conversion_to_i32() -> None:
256+
x = i32(5)
257+
assert x == 5
258+
y = int() - 113
259+
x = i32(y)
260+
assert x == -113
261+
n64: i64 = 1733
262+
x = i32(n64)
263+
assert x == 1733
264+
n32 = -1733
265+
x = i32(n32)
266+
assert x == -1733
267+
z = i32(x)
268+
assert z == -1733
269+
270+
def test_explicit_conversion_overflow() -> None:
271+
max_i32 = int() + 2**31 - 1
272+
x = i32(max_i32)
273+
assert x == 2**31 - 1
274+
assert int(x) == max_i32
275+
276+
min_i32 = int() - 2**31
277+
y = i32(min_i32)
278+
assert y == -2**31
279+
assert int(y) == min_i32
280+
281+
too_big = int() + 2**31
282+
with assertRaises(OverflowError):
283+
x = i32(too_big)
284+
285+
too_small = int() - 2**31 - 1
286+
with assertRaises(OverflowError):
287+
x = i32(too_small)
288+
289+
def test_i32_from_large_small_literal() -> None:
290+
x = i32(2**31 - 1)
291+
assert x == 2**31 - 1
292+
x = i32(-2**31)
293+
assert x == -2**31
294+
295+
def test_i32_truncate_from_i64() -> None:
296+
large = i64(2**32 + 157 + int())
297+
x = i32(large)
298+
assert x == 157
299+
small = i64(-2**32 - 157 + int())
300+
x = i32(small)
301+
assert x == -157
302+
large2 = i64(2**31 + int())
303+
x = i32(large2)
304+
assert x == -2**31
305+
small2 = i64(-2**31 - 1 - int())
306+
x = i32(small2)
307+
assert x == 2**31 - 1
308+
255309
def test_tuple_i32() -> None:
256310
a: i32 = 1
257311
b: i32 = 2

mypyc/test-data/run-i64.test

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import List, Any, Tuple
33

44
MYPY = False
55
if MYPY:
6-
from mypy_extensions import i64
6+
from mypy_extensions import i64, i32
77

88
from testutil import assertRaises
99

@@ -229,6 +229,46 @@ def test_coerce_to_and_from_int() -> None:
229229
m: int = x
230230
assert m == n
231231

232+
def test_explicit_conversion_to_i64() -> None:
233+
x = i64(5)
234+
assert x == 5
235+
y = int() - 113
236+
x = i64(y)
237+
assert x == -113
238+
n32: i32 = 1733
239+
x = i64(n32)
240+
assert x == 1733
241+
n32 = -1733
242+
x = i64(n32)
243+
assert x == -1733
244+
z = i64(x)
245+
assert z == -1733
246+
247+
def test_explicit_conversion_overflow() -> None:
248+
max_i64 = int() + 2**63 - 1
249+
x = i64(max_i64)
250+
assert x == 2**63 - 1
251+
assert int(x) == max_i64
252+
253+
min_i64 = int() - 2**63
254+
y = i64(min_i64)
255+
assert y == -2**63
256+
assert int(y) == min_i64
257+
258+
too_big = int() + 2**63
259+
with assertRaises(OverflowError):
260+
x = i64(too_big)
261+
262+
too_small = int() - 2**63 - 1
263+
with assertRaises(OverflowError):
264+
x = i64(too_small)
265+
266+
def test_i64_from_large_small_literal() -> None:
267+
x = i64(2**63 - 1)
268+
assert x == 2**63 - 1
269+
x = i64(-2**63)
270+
assert x == -2**63
271+
232272
def test_tuple_i64() -> None:
233273
a: i64 = 1
234274
b: i64 = 2

0 commit comments

Comments
 (0)