From 0b4681745c233e1b96492493de36125defd3c0b5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 29 May 2024 11:24:29 +0200 Subject: [PATCH 1/6] gh-119714: Add PyLong_GetNumBits() function --- Doc/c-api/long.rst | 12 +++++ Doc/whatsnew/3.14.rst | 3 ++ Include/cpython/longobject.h | 3 ++ Lib/test/test_capi/test_long.py | 15 ++++++ ...-05-29-11-25-26.gh-issue-119714.We9eUg.rst | 2 + Modules/_testcapi/long.c | 13 +++++ Objects/longobject.c | 53 ++++++++++++++----- 7 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index a0e111af5996d7..2d66d774a458a9 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -507,6 +507,18 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. .. versionadded:: 3.14 +.. c:function:: Py_ssize_t PyLong_GetNumBits(PyObject *obj) + + Get the number of bits of an integer. + + * Return the number of bits on success: greater than or equal to zero. + * Set an exception and return ``-1`` on error. + * Set an :exc:`OverflowError` exception, and return ``-1`` if the number + of bits doesn't fit into ``Py_ssize_t``. + + .. versionadded:: 3.14 + + .. c:function:: int PyUnstable_Long_IsCompact(const PyLongObject* op) Return 1 if *op* is compact, 0 otherwise. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index b2dd80b64a691a..3396c7eb402460 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -258,6 +258,9 @@ New Features * Add :c:func:`PyLong_GetSign` function to get the sign of :class:`int` objects. (Contributed by Sergey B Kirpichev in :gh:`116560`.) +* Add :c:func:`PyLong_GetNumBits` function to get the number of bits of an + integer. (Contributed by Victor Stinner in :gh:`119714`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/cpython/longobject.h b/Include/cpython/longobject.h index 19a6722d07734a..abc5bd5a23a440 100644 --- a/Include/cpython/longobject.h +++ b/Include/cpython/longobject.h @@ -115,3 +115,6 @@ PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v, /* For use by the gcd function in mathmodule.c */ PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *); + +// Get the number of bits of an integer +PyAPI_FUNC(Py_ssize_t) PyLong_GetNumBits(PyObject *obj); diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index 06a29b5a0505b4..f6b44809fd4e97 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -737,6 +737,21 @@ def test_long_getsign(self): # CRASHES getsign(NULL) + def test_long_getnumbits(self): + numbits = _testcapi.pylong_getnumbits + self.assertEqual(numbits(0), 0) + self.assertEqual(numbits(7), 3) + self.assertEqual(numbits(-37), 6) + self.assertEqual(numbits(684_643_661_981), 40) + self.assertEqual(numbits(2**123-1), 123) + + for invalid_type in (1.0, 1j, "abc"): + with self.subTest(invalid_type=invalid_type): + with self.assertRaises(TypeError): + numbits(invalid_type) + + # CRASHES numbits(NULL) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst b/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst new file mode 100644 index 00000000000000..12cc5aa184c075 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyLong_GetNumBits` function to get the number of bits of an +integer. Patch by Victor Stinner. diff --git a/Modules/_testcapi/long.c b/Modules/_testcapi/long.c index 2b5e85d5707522..8ddd0d952e94e2 100644 --- a/Modules/_testcapi/long.c +++ b/Modules/_testcapi/long.c @@ -117,6 +117,18 @@ pylong_aspid(PyObject *module, PyObject *arg) } +static PyObject * +pylong_getnumbits(PyObject *module, PyObject *arg) +{ + NULLABLE(arg); + Py_ssize_t bits = PyLong_GetNumBits(arg); + if (bits < 0) { + return NULL; + } + return PyLong_FromSsize_t(bits); +} + + static PyMethodDef test_methods[] = { _TESTCAPI_CALL_LONG_COMPACT_API_METHODDEF {"pylong_fromunicodeobject", pylong_fromunicodeobject, METH_VARARGS}, @@ -124,6 +136,7 @@ static PyMethodDef test_methods[] = { {"pylong_fromnativebytes", pylong_fromnativebytes, METH_VARARGS}, {"pylong_getsign", pylong_getsign, METH_O}, {"pylong_aspid", pylong_aspid, METH_O}, + {"pylong_getnumbits", pylong_getnumbits, METH_O}, {NULL}, }; diff --git a/Objects/longobject.c b/Objects/longobject.c index 054689471e7aa9..3a05aceffcfbbf 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -792,28 +792,32 @@ bit_length_digit(digit x) return _Py_bit_length((unsigned long)x); } + size_t _PyLong_NumBits(PyObject *vv) { PyLongObject *v = (PyLongObject *)vv; - size_t result = 0; - Py_ssize_t ndigits; - int msd_bits; - assert(v != NULL); assert(PyLong_Check(v)); - ndigits = _PyLong_DigitCount(v); + + Py_ssize_t ndigits = _PyLong_DigitCount(v); assert(ndigits == 0 || v->long_value.ob_digit[ndigits - 1] != 0); - if (ndigits > 0) { - digit msd = v->long_value.ob_digit[ndigits - 1]; - if ((size_t)(ndigits - 1) > SIZE_MAX / (size_t)PyLong_SHIFT) - goto Overflow; - result = (size_t)(ndigits - 1) * (size_t)PyLong_SHIFT; - msd_bits = bit_length_digit(msd); - if (SIZE_MAX - msd_bits < result) - goto Overflow; - result += msd_bits; + if (ndigits == 0) { + return 0; + } + assert(ndigits > 0); + + digit msd = v->long_value.ob_digit[ndigits - 1]; + if ((size_t)(ndigits - 1) > SIZE_MAX / (size_t)PyLong_SHIFT) { + goto Overflow; + } + size_t result = (size_t)(ndigits - 1) * (size_t)PyLong_SHIFT; + + int msd_bits = bit_length_digit(msd); + if (SIZE_MAX - msd_bits < result) { + goto Overflow; } + result += msd_bits; return result; Overflow: @@ -822,6 +826,27 @@ _PyLong_NumBits(PyObject *vv) return (size_t)-1; } + +Py_ssize_t +PyLong_GetNumBits(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %T", obj); + return -1; + } + size_t bits = _PyLong_NumBits(obj); + if (bits == (size_t)-1) { + return -1; + } + if (bits > (size_t)PY_SSIZE_T_MAX) { + PyErr_SetString(PyExc_OverflowError, + "int has too many bits to express in Py_ssize_t"); + return -1; + } + return (Py_ssize_t)bits; +} + + PyObject * _PyLong_FromByteArray(const unsigned char* bytes, size_t n, int little_endian, int is_signed) From 94811362671dd4d391ddb3a3a370c76128cb29bc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 31 May 2024 13:43:45 +0200 Subject: [PATCH 2/6] Suggest calling the bit_length() method --- Doc/c-api/long.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 2d66d774a458a9..0fcfbdabd864a0 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -516,6 +516,9 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. * Set an :exc:`OverflowError` exception, and return ``-1`` if the number of bits doesn't fit into ``Py_ssize_t``. + Calling the ``bit_length()`` method should be preferred to support integers + larger than ``Py_ssize_t`` bits and to avoid ``OverflowError``. + .. versionadded:: 3.14 From c1f631054baa5b34a0921a2d6ea8bc6fc5c12d35 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 13:26:45 +0200 Subject: [PATCH 3/6] Update Doc/c-api/long.rst Co-authored-by: Erlend E. Aasland --- Doc/c-api/long.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 0fcfbdabd864a0..6dd608d0395935 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -514,10 +514,10 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. * Return the number of bits on success: greater than or equal to zero. * Set an exception and return ``-1`` on error. * Set an :exc:`OverflowError` exception, and return ``-1`` if the number - of bits doesn't fit into ``Py_ssize_t``. + of bits doesn't fit into :c:type:`Py_ssize_t`. - Calling the ``bit_length()`` method should be preferred to support integers - larger than ``Py_ssize_t`` bits and to avoid ``OverflowError``. + Calling the :py:meth:`bit_length` method should be preferred to support integers + larger than :c:type:`Py_ssize_t` bits and to avoid :exc:`!OverflowError`. .. versionadded:: 3.14 From fd8a3f23ac673d17e57def215c6484b53de67783 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 13:26:52 +0200 Subject: [PATCH 4/6] Update Include/cpython/longobject.h Co-authored-by: Erlend E. Aasland --- Include/cpython/longobject.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/cpython/longobject.h b/Include/cpython/longobject.h index abc5bd5a23a440..0fe35663a06b3f 100644 --- a/Include/cpython/longobject.h +++ b/Include/cpython/longobject.h @@ -116,5 +116,5 @@ PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v, /* For use by the gcd function in mathmodule.c */ PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *); -// Get the number of bits of an integer +// Get the number of bits of an integer. PyAPI_FUNC(Py_ssize_t) PyLong_GetNumBits(PyObject *obj); From afc665e3b5fc5530c580a7d79d77cb591004d0b1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 13:56:54 +0200 Subject: [PATCH 5/6] Add OverflowError test --- Lib/test/test_capi/test_long.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index f6b44809fd4e97..18754802fef46e 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -1,6 +1,7 @@ import unittest import sys import test.support as support +from test.support import bigmemtest from test.support import import_helper @@ -752,6 +753,17 @@ def test_long_getnumbits(self): # CRASHES numbits(NULL) + # The test is always skipped on 64-bit platforms, it requires way too much + # memory. It's only run on 32-bit platforms. + @bigmemtest(size=(_testcapi.PY_SSIZE_T_MAX // sys.int_info.bits_per_digit + * sys.int_info.sizeof_digit) + sys.getsizeof(123), + memuse=1, dry_run=False) + def test_long_getnumbits_overflow(self, size): + numbits = _testcapi.pylong_getnumbits + big_int = 1 << _testcapi.PY_SSIZE_T_MAX + with self.assertRaises(OverflowError): + numbits(big_int) + if __name__ == "__main__": unittest.main() From b08d260ab682c40be1c1506857cba995fdb97226 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 14:26:12 +0200 Subject: [PATCH 6/6] doc: add link to int.bit_length() method --- Doc/c-api/long.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 6dd608d0395935..9e89056dabf1a5 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -516,8 +516,9 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. * Set an :exc:`OverflowError` exception, and return ``-1`` if the number of bits doesn't fit into :c:type:`Py_ssize_t`. - Calling the :py:meth:`bit_length` method should be preferred to support integers - larger than :c:type:`Py_ssize_t` bits and to avoid :exc:`!OverflowError`. + Calling the :py:meth:`int.bit_length` method should be preferred to support + integers larger than :c:type:`Py_ssize_t` bits and to avoid + :exc:`!OverflowError`. .. versionadded:: 3.14