Skip to content

gh-119714: Add PyLong_GetNumBits() function #119715

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

Closed
wants to merge 6 commits into from
Closed
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
16 changes: 16 additions & 0 deletions Doc/c-api/long.rst
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,22 @@ 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 :c:type:`Py_ssize_t`.

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


.. c:function:: int PyUnstable_Long_IsCompact(const PyLongObject* op)

Return 1 if *op* is compact, 0 otherwise.
Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
3 changes: 3 additions & 0 deletions Include/cpython/longobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
27 changes: 27 additions & 0 deletions Lib/test/test_capi/test_long.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest
import sys
import test.support as support
from test.support import bigmemtest

from test.support import import_helper

Expand Down Expand Up @@ -737,6 +738,32 @@ 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)
Comment on lines +743 to +747
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for the two overflow cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test, but it requires 1145324612.3 GiB of memory to run :-) On 32-bit systems, the test pass and only requires 273 MiB.


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)

# 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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyLong_GetNumBits` function to get the number of bits of an
integer. Patch by Victor Stinner.
13 changes: 13 additions & 0 deletions Modules/_testcapi/long.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,26 @@ 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},
{"pylong_asnativebytes", pylong_asnativebytes, METH_VARARGS},
{"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},
};

Expand Down
53 changes: 39 additions & 14 deletions Objects/longobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading