From 9e3d54aaac70a65a633fa0142d05cc8c4c88ac71 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 15:48:05 +0000 Subject: [PATCH 01/16] [mypyc] feat: new primitive for `int.bit_length` --- mypyc/lib-rt/int_ops.c | 21 +++++++++++++++++++++ mypyc/primitives/int_ops.py | 9 +++++++++ mypyc/test-data/irbuild-int.test | 11 +++++++++++ mypyc/test-data/run-integers.test | 10 ++++++++++ 4 files changed, 51 insertions(+) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index e2c302eea576..8cd48aeb7ec1 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -581,3 +581,24 @@ double CPyTagged_TrueDivide(CPyTagged x, CPyTagged y) { } return 1.0; } + +// int.bit_length() +CPyTagged CPyInt_BitLength(CPyTagged self) { + PyObject *pyint = CPyTagged_StealAsObject(self); + if (!PyLong_Check(pyint)) { + Py_DECREF(pyint); + PyErr_SetString(PyExc_TypeError, "self must be int"); + return CPY_INT_TAG; + } + PyObject *res = PyObject_CallMethod(pyint, "bit_length", NULL); + Py_DECREF(pyint); + if (!res) { + return CPY_INT_TAG; + } + long value = PyLong_AsLong(res); + Py_DECREF(res); + if (value == -1 && PyErr_Occurred()) { + return CPY_INT_TAG; + } + return value << 1; +} diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index d723c9b63a86..0d0cd64e9bd6 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -305,3 +305,12 @@ def int_unary_op(name: str, c_function_name: str) -> PrimitiveDescription: c_function_name="PyLong_Check", error_kind=ERR_NEVER, ) + +# int.bit_length() +method_op( + name="bit_length", + arg_types=[int_rprimitive], + return_type=int_rprimitive, + c_function_name="CPyInt_BitLength", + error_kind=ERR_MAGIC, +) diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index bdf9127b722a..739adece8bf4 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -210,3 +210,14 @@ L0: r0 = CPyTagged_Invert(n) x = r0 return x + +[case testIntBitLength] +def f(x: int) -> int: + return x.bit_length() +[out] +def f(x): + x :: int + r0 :: int +L0: + r0 = int_bit_length x + return r0 diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 1163c9d942f7..6746d56389b7 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -572,3 +572,13 @@ class subc(int): [file userdefinedint.py] class int: pass + +[case testBitLength] +def bit_length(n: int) -> int: + return n.bit_length() +def test_bit_length() -> None: + assert bit_length(0) == 0 + assert bit_length(1) == 1 + assert bit_length(255) == 8 + assert bit_length(256) == 9 + assert bit_length(-256) == 9 \ No newline at end of file From 24a7ae7ac042bc2d7e120896be9b2a0ef8ac4b48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 15:52:37 +0000 Subject: [PATCH 02/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/test-data/run-integers.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 6746d56389b7..58eefc47e215 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -581,4 +581,4 @@ def test_bit_length() -> None: assert bit_length(1) == 1 assert bit_length(255) == 8 assert bit_length(256) == 9 - assert bit_length(-256) == 9 \ No newline at end of file + assert bit_length(-256) == 9 From 7dbbb7776b7932c2085f6e70800a6ff87f34397d Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 16 Aug 2025 11:58:58 -0400 Subject: [PATCH 03/16] Update int_ops.py --- mypyc/primitives/int_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 0d0cd64e9bd6..43cfef7d32e0 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -31,7 +31,7 @@ str_rprimitive, void_rtype, ) -from mypyc.primitives.registry import binary_op, custom_op, function_op, load_address_op, unary_op +from mypyc.primitives.registry import binary_op, custom_op, function_op, load_address_op, method_op, unary_op # Constructors for builtins.int and native int types have the same behavior. In # interpreted mode, native int types are just aliases to 'int'. From b133de3b581f5255df41a146583d9030679b239f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:00:24 +0000 Subject: [PATCH 04/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/primitives/int_ops.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 43cfef7d32e0..d2dd623b9389 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -31,7 +31,14 @@ str_rprimitive, void_rtype, ) -from mypyc.primitives.registry import binary_op, custom_op, function_op, load_address_op, method_op, unary_op +from mypyc.primitives.registry import ( + binary_op, + custom_op, + function_op, + load_address_op, + method_op, + unary_op, +) # Constructors for builtins.int and native int types have the same behavior. In # interpreted mode, native int types are just aliases to 'int'. From 9d4116ed4bc76ad672e10b95cf7d9bfdf72efee5 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:02:29 +0000 Subject: [PATCH 05/16] add headers --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/int_ops.c | 2 +- mypyc/primitives/int_ops.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 1881aa97f308..1382cc0167b8 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -139,6 +139,7 @@ CPyTagged CPyTagged_Remainder_(CPyTagged left, CPyTagged right); CPyTagged CPyTagged_BitwiseLongOp_(CPyTagged a, CPyTagged b, char op); CPyTagged CPyTagged_Rshift_(CPyTagged left, CPyTagged right); CPyTagged CPyTagged_Lshift_(CPyTagged left, CPyTagged right); +CPyTagged CPyTagged_BitLength(CPyTagged self); PyObject *CPyTagged_Str(CPyTagged n); CPyTagged CPyTagged_FromFloat(double f); diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 8cd48aeb7ec1..2371fe43df94 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -583,7 +583,7 @@ double CPyTagged_TrueDivide(CPyTagged x, CPyTagged y) { } // int.bit_length() -CPyTagged CPyInt_BitLength(CPyTagged self) { +CPyTagged CPyTagged_BitLength(CPyTagged self) { PyObject *pyint = CPyTagged_StealAsObject(self); if (!PyLong_Check(pyint)) { Py_DECREF(pyint); diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 0d0cd64e9bd6..320c1f117906 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -311,6 +311,6 @@ def int_unary_op(name: str, c_function_name: str) -> PrimitiveDescription: name="bit_length", arg_types=[int_rprimitive], return_type=int_rprimitive, - c_function_name="CPyInt_BitLength", + c_function_name="CPyTagged_BitLength", error_kind=ERR_MAGIC, ) From f718507f38cc44c1de95dfc4e0c7b32135ed306d Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:09:46 +0000 Subject: [PATCH 06/16] fixture --- mypyc/test-data/fixtures/ir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index 661ae50fd5f3..f90a548d4f0e 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -81,6 +81,7 @@ def __lt__(self, n: int) -> bool: pass def __gt__(self, n: int) -> bool: pass def __le__(self, n: int) -> bool: pass def __ge__(self, n: int) -> bool: pass + def bit_length(self) -> int: pass class str: @overload From bb0a5eb2096fb5aedfe2b9589dd20ae89e5e2ad2 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:21:37 +0000 Subject: [PATCH 07/16] fix ir --- mypyc/test-data/irbuild-int.test | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index 739adece8bf4..184c66fafb7c 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -216,8 +216,7 @@ def f(x: int) -> int: return x.bit_length() [out] def f(x): - x :: int - r0 :: int + x, r0 :: int L0: - r0 = int_bit_length x + r0 = CPyTagged_BitLength(x) return r0 From ae266ad9f31cf2df9d1133087f19450725577c83 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:37:26 +0000 Subject: [PATCH 08/16] implement c --- mypyc/lib-rt/int_ops.c | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 2371fe43df94..7d5f28929582 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -584,21 +584,36 @@ double CPyTagged_TrueDivide(CPyTagged x, CPyTagged y) { // int.bit_length() CPyTagged CPyTagged_BitLength(CPyTagged self) { + // Handle zero + if (self == 0) { + return 0; + } + + // Fast path for small (tagged) ints + if (CPyTagged_CheckShort(self)) { + Py_ssize_t val = CPyTagged_ShortAsSsize_t(self); + Py_ssize_t absval = val < 0 ? -val : val; + int bits = 0; + while (absval) { + absval >>= 1; + bits++; + } + return bits << 1; + } + + // Slow path for big ints PyObject *pyint = CPyTagged_StealAsObject(self); if (!PyLong_Check(pyint)) { Py_DECREF(pyint); PyErr_SetString(PyExc_TypeError, "self must be int"); return CPY_INT_TAG; } - PyObject *res = PyObject_CallMethod(pyint, "bit_length", NULL); + int bits = _PyLong_NumBits((PyLongObject *)pyint); Py_DECREF(pyint); - if (!res) { - return CPY_INT_TAG; - } - long value = PyLong_AsLong(res); - Py_DECREF(res); - if (value == -1 && PyErr_Occurred()) { + if (bits < 0) { + // _PyLong_NumBits sets an error on failure return CPY_INT_TAG; } - return value << 1; + return bits << 1; } + From e83203b807c7182217e10f7b2898081402ffded5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:41:59 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/lib-rt/int_ops.c | 1 - 1 file changed, 1 deletion(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 7d5f28929582..f46326fb2656 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -616,4 +616,3 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { } return bits << 1; } - From 1545a8df1b96f46cef4adb794d0d0e13d35e9973 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:44:27 +0000 Subject: [PATCH 10/16] more run tests --- mypyc/test-data/run-integers.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 58eefc47e215..43766f5a8cc7 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -582,3 +582,9 @@ def test_bit_length() -> None: assert bit_length(255) == 8 assert bit_length(256) == 9 assert bit_length(-256) == 9 + # Large positive int + assert bit_length(1 << 70) == 71 + # Large negative int + assert bit_length(-(1 << 70)) == 71 + # Large int with all bits set + assert bit_length((1 << 100) - 1) == 100 From 0854952b34a987d3f344fcbe7d76179dd6f343c8 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 16:46:55 +0000 Subject: [PATCH 11/16] fix c --- mypyc/lib-rt/int_ops.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 7d5f28929582..ac37cf76931a 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -608,7 +608,7 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { PyErr_SetString(PyExc_TypeError, "self must be int"); return CPY_INT_TAG; } - int bits = _PyLong_NumBits((PyLongObject *)pyint); + int bits = _PyLong_NumBits(pyint); Py_DECREF(pyint); if (bits < 0) { // _PyLong_NumBits sets an error on failure From be93fe749b1ed9c2ea4e80ad06fab73ef257440a Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 21:34:13 +0000 Subject: [PATCH 12/16] optimize fast path for short --- mypyc/lib-rt/int_ops.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 53090c48265a..a55ad842f07f 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -594,9 +594,16 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { Py_ssize_t val = CPyTagged_ShortAsSsize_t(self); Py_ssize_t absval = val < 0 ? -val : val; int bits = 0; - while (absval) { - absval >>= 1; - bits++; + if (absval) { +#if defined(__GNUC__) || defined(__clang__) + bits = (int)(sizeof(absval) * 8) - __builtin_clzll((unsigned long long)absval); +#else + // Fallback to loop if no builtin + while (absval) { + absval >>= 1; + bits++; + } +#endif } return bits << 1; } From d7b0a0c26df295997ad4ada43fbdab00a5a96fc3 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 16 Aug 2025 21:56:20 +0000 Subject: [PATCH 13/16] fix macro --- mypyc/lib-rt/int_ops.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index a55ad842f07f..31dd1549029c 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -15,6 +15,17 @@ #define CPyLong_FromSsize_t PyLong_FromSsize_t #endif +#if defined(__GNUC__) || defined(__clang__) +# if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || (defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ == 8) +# define CPY_CLZ(x) __builtin_clzll((unsigned long long)(x)) +# define CPY_BITS 64 +# else +# define CPY_CLZ(x) __builtin_clz((unsigned int)(x)) +# define CPY_BITS 32 +# endif +#endif + + CPyTagged CPyTagged_FromSsize_t(Py_ssize_t value) { // We use a Python object if the value shifted left by 1 is too // large for Py_ssize_t @@ -596,7 +607,7 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { int bits = 0; if (absval) { #if defined(__GNUC__) || defined(__clang__) - bits = (int)(sizeof(absval) * 8) - __builtin_clzll((unsigned long long)absval); + bits = (int)(CPY_BITS - CPY_CLZ(absval)); #else // Fallback to loop if no builtin while (absval) { From 1eb2c7efcb57592e0cf4e949963ccc1a4c84ec0c Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:12:17 -0400 Subject: [PATCH 14/16] Update run-integers.test --- mypyc/test-data/run-integers.test | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 43766f5a8cc7..c32e609ad8de 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -576,15 +576,17 @@ class int: [case testBitLength] def bit_length(n: int) -> int: return n.bit_length() +def bit_length_python(n: int) -> int: + return getattr(n, "bit_length")() def test_bit_length() -> None: - assert bit_length(0) == 0 - assert bit_length(1) == 1 - assert bit_length(255) == 8 - assert bit_length(256) == 9 - assert bit_length(-256) == 9 + assert bit_length(0) == bit_length_python(0) + assert bit_length(1) == bit_length_python(1) + assert bit_length(255) == bit_length_python(255) + assert bit_length(256) == bit_length_python(256) + assert bit_length(-256) == bit_length_python(-256) # Large positive int - assert bit_length(1 << 70) == 71 + assert bit_length(1 << 70) == bit_length_python(1 << 70) # Large negative int - assert bit_length(-(1 << 70)) == 71 + assert bit_length(-(1 << 70)) == bit_length_python(-(1 << 70)) # Large int with all bits set - assert bit_length((1 << 100) - 1) == 100 + assert bit_length((1 << 100) - 1) == bit_length_python((1 << 100) - 1) From 71c30a0ce2e00ba0cd1ffc90b425fa074b0ac10d Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 19 Aug 2025 16:55:39 +0000 Subject: [PATCH 15/16] msc --- mypyc/lib-rt/int_ops.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 31dd1549029c..369254d0ad11 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -5,6 +5,10 @@ #include #include "CPy.h" +#ifdef _MSC_VER +#include +#endif + #ifndef _WIN32 // On 64-bit Linux and macOS, ssize_t and long are both 64 bits, and // PyLong_FromLong is faster than PyLong_FromSsize_t, so use the faster one @@ -606,7 +610,19 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { Py_ssize_t absval = val < 0 ? -val : val; int bits = 0; if (absval) { -#if defined(__GNUC__) || defined(__clang__) +#if defined(_MSC_VER) + #if defined(_WIN64) + unsigned long idx; + if (_BitScanReverse64(&idx, (unsigned __int64)absval)) { + bits = (int)(idx + 1); + } + #else + unsigned long idx; + if (_BitScanReverse(&idx, (unsigned long)absval)) { + bits = (int)(idx + 1); + } + #endif +#elif defined(__GNUC__) || defined(__clang__) bits = (int)(CPY_BITS - CPY_CLZ(absval)); #else // Fallback to loop if no builtin From 243cc9a635575b24288b13addf901134130cda34 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:49:03 -0400 Subject: [PATCH 16/16] Update int_ops.c --- mypyc/lib-rt/int_ops.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 369254d0ad11..dd76955320cb 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -637,11 +637,6 @@ CPyTagged CPyTagged_BitLength(CPyTagged self) { // Slow path for big ints PyObject *pyint = CPyTagged_StealAsObject(self); - if (!PyLong_Check(pyint)) { - Py_DECREF(pyint); - PyErr_SetString(PyExc_TypeError, "self must be int"); - return CPY_INT_TAG; - } int bits = _PyLong_NumBits(pyint); Py_DECREF(pyint); if (bits < 0) {