Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mypyc] Support the i16 native integer type #15464

Merged
merged 22 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@
)

# Mypyc fixed-width native int types (compatible with builtins.int)
MYPYC_NATIVE_INT_NAMES: Final = ("mypy_extensions.i64", "mypy_extensions.i32")
MYPYC_NATIVE_INT_NAMES: Final = (
"mypy_extensions.i64",
"mypy_extensions.i32",
"mypy_extensions.i16",
)

DATACLASS_TRANSFORM_NAMES: Final = (
"typing.dataclass_transform",
Expand Down
35 changes: 25 additions & 10 deletions mypyc/codegen/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
is_dict_rprimitive,
is_fixed_width_rtype,
is_float_rprimitive,
is_int16_rprimitive,
is_int32_rprimitive,
is_int64_rprimitive,
is_int_rprimitive,
Expand Down Expand Up @@ -900,28 +901,35 @@ def emit_unbox(
self.emit_line(f" {dest} = 1;")
elif is_int64_rprimitive(typ):
# Whether we are borrowing or not makes no difference.
assert not optional # Not supported for overlapping error values
if declare_dest:
self.emit_line(f"int64_t {dest};")
self.emit_line(f"{dest} = CPyLong_AsInt64({src});")
# TODO: Handle 'optional'
# TODO: Handle 'failure'
if not isinstance(error, AssignHandler):
self.emit_unbox_failure_with_overlapping_error_value(dest, typ, failure)
elif is_int32_rprimitive(typ):
# Whether we are borrowing or not makes no difference.
assert not optional # Not supported for overlapping error values
if declare_dest:
self.emit_line(f"int32_t {dest};")
self.emit_line(f"{dest} = CPyLong_AsInt32({src});")
# TODO: Handle 'optional'
# TODO: Handle 'failure'
if not isinstance(error, AssignHandler):
self.emit_unbox_failure_with_overlapping_error_value(dest, typ, failure)
elif is_int16_rprimitive(typ):
# Whether we are borrowing or not makes no difference.
assert not optional # Not supported for overlapping error values
if declare_dest:
self.emit_line(f"int16_t {dest};")
self.emit_line(f"{dest} = CPyLong_AsInt16({src});")
if not isinstance(error, AssignHandler):
self.emit_unbox_failure_with_overlapping_error_value(dest, typ, failure)
elif is_float_rprimitive(typ):
assert not optional # Not supported for overlapping error values
if declare_dest:
self.emit_line("double {};".format(dest))
# TODO: Don't use __float__ and __index__
self.emit_line(f"{dest} = PyFloat_AsDouble({src});")
self.emit_lines(
f"if ({dest} == -1.0 && PyErr_Occurred()) {{", f"{dest} = -113.0;", "}"
)
# TODO: Handle 'optional'
# TODO: Handle 'failure'
self.emit_lines(f"if ({dest} == -1.0 && PyErr_Occurred()) {{", failure, "}")
elif isinstance(typ, RTuple):
self.declare_tuple_struct(typ)
if declare_dest:
Expand Down Expand Up @@ -1006,7 +1014,7 @@ def emit_box(
self.emit_lines(f"{declaration}{dest} = Py_None;")
if not can_borrow:
self.emit_inc_ref(dest, object_rprimitive)
elif is_int32_rprimitive(typ):
elif is_int32_rprimitive(typ) or is_int16_rprimitive(typ):
self.emit_line(f"{declaration}{dest} = PyLong_FromLong({src});")
elif is_int64_rprimitive(typ):
self.emit_line(f"{declaration}{dest} = PyLong_FromLongLong({src});")
Expand Down Expand Up @@ -1137,6 +1145,13 @@ def _emit_traceback(
if DEBUG_ERRORS:
self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");')

def emit_unbox_failure_with_overlapping_error_value(
self, dest: str, typ: RType, failure: str
) -> None:
self.emit_line(f"if ({dest} == {self.c_error_value(typ)} && PyErr_Occurred()) {{")
self.emit_line(failure)
self.emit_line("}")


def c_array_initializer(components: list[str], *, indented: bool = False) -> str:
"""Construct an initializer for a C array variable.
Expand Down
6 changes: 4 additions & 2 deletions mypyc/doc/float_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Construction
* ``float(x: int)``
* ``float(x: i64)``
* ``float(x: i32)``
* ``float(x: i16)``
* ``float(x: str)``
* ``float(x: float)`` (no-op)

Expand All @@ -28,8 +29,9 @@ Functions
---------

* ``int(f)``
* ``i32(f)`` (convert to ``i32``)
* ``i64(f)`` (convert to ``i64``)
* ``i64(f)`` (convert to 64-bit signed integer)
* ``i32(f)`` (convert to 32-bit signed integer)
* ``i16(f)`` (convert to 16-bit signed integer)
* ``abs(f)``
* ``math.sin(f)``
* ``math.cos(f)``
Expand Down
25 changes: 20 additions & 5 deletions mypyc/doc/int_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ Mypyc supports these integer types:
* ``int`` (arbitrary-precision integer)
* ``i64`` (64-bit signed integer)
* ``i32`` (32-bit signed integer)
* ``i16`` (16-bit signed integer)

``i64`` and ``i32`` are *native integer types* and must be imported
``i64``, ``i32`` and ``i16`` are *native integer types* and must be imported
from the ``mypy_extensions`` module. ``int`` corresponds to the Python
``int`` type, but uses a more efficient runtime representation (tagged
pointer). Native integer types are value types. All integer types have
optimized primitive operations, but the native integer types are more
efficient than ``int``, since they don't require range or bounds
checks.
pointer). Native integer types are value types.

All integer types have optimized primitive operations, but the native
integer types are more efficient than ``int``, since they don't
require range or bounds checks.

Operations on integers that are listed here have fast, optimized
implementations. Other integer operations use generic implementations
Expand All @@ -31,6 +33,7 @@ Construction
* ``int(x: float)``
* ``int(x: i64)``
* ``int(x: i32)``
* ``int(x: i16)``
* ``int(x: str)``
* ``int(x: str, base: int)``
* ``int(x: int)`` (no-op)
Expand All @@ -40,6 +43,7 @@ Construction
* ``i64(x: int)``
* ``i64(x: float)``
* ``i64(x: i32)``
* ``i64(x: i16)``
* ``i64(x: str)``
* ``i64(x: str, base: int)``
* ``i64(x: i64)`` (no-op)
Expand All @@ -49,10 +53,21 @@ Construction
* ``i32(x: int)``
* ``i32(x: float)``
* ``i32(x: i64)`` (truncate)
* ``i32(x: i16)``
* ``i32(x: str)``
* ``i32(x: str, base: int)``
* ``i32(x: i32)`` (no-op)

``i16`` type:

* ``i16(x: int)``
* ``i16(x: float)``
* ``i16(x: i64)`` (truncate)
* ``i16(x: i32)`` (truncate)
* ``i16(x: str)``
* ``i16(x: str, base: int)``
* ``i16(x: i16)`` (no-op)

Conversions from ``int`` to a native integer type raise
``OverflowError`` if the value is too large or small. Conversions from
a wider native integer type to a narrower one truncate the value and never
Expand Down
16 changes: 9 additions & 7 deletions mypyc/doc/using_type_annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ implementations:
* ``int`` (:ref:`native operations <int-ops>`)
* ``i64`` (:ref:`documentation <native-ints>`, :ref:`native operations <int-ops>`)
* ``i32`` (:ref:`documentation <native-ints>`, :ref:`native operations <int-ops>`)
* ``i16`` (:ref:`documentation <native-ints>`, :ref:`native operations <int-ops>`)
* ``float`` (:ref:`native operations <float-ops>`)
* ``bool`` (:ref:`native operations <bool-ops>`)
* ``str`` (:ref:`native operations <str-ops>`)
Expand Down Expand Up @@ -342,13 +343,14 @@ Examples::
Native integer types
--------------------

You can use the native integer types ``i64`` (64-bit signed integer)
and ``i32`` (32-bit signed integer) if you know that integer values
will always fit within fixed bounds. These types are faster than the
arbitrary-precision ``int`` type, since they don't require overflow
checks on operations. ``i32`` may also use less memory than ``int``
values. The types are imported from the ``mypy_extensions`` module
(installed via ``pip install mypy_extensions``).
You can use the native integer types ``i64`` (64-bit signed integer),
``i32`` (32-bit signed integer), and ``i16`` (16-bit signed integer)
if you know that integer values will always fit within fixed
bounds. These types are faster than the arbitrary-precision ``int``
type, since they don't require overflow checks on operations. ``i32``
and ``i16`` may also use less memory than ``int`` values. The types
are imported from the ``mypy_extensions`` module (installed via ``pip
install mypy_extensions``).

Example::

Expand Down
20 changes: 18 additions & 2 deletions mypyc/ir/rtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def __init__(
self.error_overlap = error_overlap
if ctype == "CPyTagged":
self.c_undefined = "CPY_INT_TAG"
elif ctype in ("int32_t", "int64_t"):
elif ctype in ("int16_t", "int32_t", "int64_t"):
# This is basically an arbitrary value that is pretty
# unlikely to overlap with a real value.
self.c_undefined = "-113"
Expand Down Expand Up @@ -290,6 +290,16 @@ def __hash__(self) -> int:

# Low level integer types (correspond to C integer types)

int16_rprimitive: Final = RPrimitive(
"int16",
is_unboxed=True,
is_refcounted=False,
is_native_int=True,
is_signed=True,
ctype="int16_t",
size=2,
error_overlap=True,
)
int32_rprimitive: Final = RPrimitive(
"int32",
is_unboxed=True,
Expand Down Expand Up @@ -432,6 +442,10 @@ def is_short_int_rprimitive(rtype: RType) -> bool:
return rtype is short_int_rprimitive


def is_int16_rprimitive(rtype: RType) -> bool:
return rtype is int16_rprimitive


def is_int32_rprimitive(rtype: RType) -> bool:
return rtype is int32_rprimitive or (
rtype is c_pyssize_t_rprimitive and rtype._ctype == "int32_t"
Expand All @@ -445,7 +459,7 @@ def is_int64_rprimitive(rtype: RType) -> bool:


def is_fixed_width_rtype(rtype: RType) -> bool:
return is_int32_rprimitive(rtype) or is_int64_rprimitive(rtype)
return is_int64_rprimitive(rtype) or is_int32_rprimitive(rtype) or is_int16_rprimitive(rtype)


def is_uint32_rprimitive(rtype: RType) -> bool:
Expand Down Expand Up @@ -536,6 +550,8 @@ def visit_rprimitive(self, t: RPrimitive) -> str:
return "8" # "8 byte integer"
elif t._ctype == "int32_t":
return "4" # "4 byte integer"
elif t._ctype == "int16_t":
return "2" # "2 byte integer"
elif t._ctype == "double":
return "F"
assert not t.is_unboxed, f"{t} unexpected unboxed type"
Expand Down
14 changes: 13 additions & 1 deletion mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
is_dict_rprimitive,
is_fixed_width_rtype,
is_float_rprimitive,
is_int16_rprimitive,
is_int32_rprimitive,
is_int64_rprimitive,
is_int_rprimitive,
Expand Down Expand Up @@ -146,6 +147,9 @@
py_vectorcall_op,
)
from mypyc.primitives.int_ops import (
int16_divide_op,
int16_mod_op,
int16_overflow,
int32_divide_op,
int32_mod_op,
int32_overflow,
Expand Down Expand Up @@ -456,6 +460,10 @@ def coerce_int_to_fixed_width(self, src: Value, target_type: RType, line: int) -
# Slow path just always generates an OverflowError
self.call_c(int32_overflow, [], line)
self.add(Unreachable())
elif is_int16_rprimitive(target_type):
# Slow path just always generates an OverflowError
self.call_c(int16_overflow, [], line)
self.add(Unreachable())
else:
assert False, target_type

Expand All @@ -469,7 +477,7 @@ def coerce_short_int_to_fixed_width(self, src: Value, target_type: RType, line:
assert False, (src.type, target_type)

def coerce_fixed_width_to_int(self, src: Value, line: int) -> Value:
if is_int32_rprimitive(src.type) and PLATFORM_SIZE == 8:
if (is_int32_rprimitive(src.type) and PLATFORM_SIZE == 8) or is_int16_rprimitive(src.type):
# Simple case -- just sign extend and shift.
extended = self.add(Extend(src, c_pyssize_t_rprimitive, signed=True))
return self.int_op(
Expand Down Expand Up @@ -2038,6 +2046,8 @@ def fixed_width_int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line:
prim = int64_divide_op
elif is_int32_rprimitive(type):
prim = int32_divide_op
elif is_int16_rprimitive(type):
prim = int16_divide_op
else:
assert False, type
return self.call_c(prim, [lhs, rhs], line)
Expand All @@ -2050,6 +2060,8 @@ def fixed_width_int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line:
prim = int64_mod_op
elif is_int32_rprimitive(type):
prim = int32_mod_op
elif is_int16_rprimitive(type):
prim = int16_mod_op
else:
assert False, type
return self.call_c(prim, [lhs, rhs], line)
Expand Down
3 changes: 3 additions & 0 deletions mypyc/irbuild/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
bytes_rprimitive,
dict_rprimitive,
float_rprimitive,
int16_rprimitive,
int32_rprimitive,
int64_rprimitive,
int_rprimitive,
Expand Down Expand Up @@ -102,6 +103,8 @@ def type_to_rtype(self, typ: Type | None) -> RType:
return int64_rprimitive
elif typ.type.fullname == "mypy_extensions.i32":
return int32_rprimitive
elif typ.type.fullname == "mypy_extensions.i16":
return int16_rprimitive
else:
return object_rprimitive
elif isinstance(typ, TupleType):
Expand Down
Loading