From bc3101e491dc665455c1dd19b371f0a97f519dd6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 22 Dec 2025 22:18:48 +0200 Subject: [PATCH 1/4] gh-143006: Fix and optimize mixed comparison of float and int When comparing negative non-integer float and int with the same number of bits in the integer part, __neg__() in the int subclass returning not an int caused an assertion error. Now the integer is no longer negated. Also, reduced the number of temporary created Python objects. --- Lib/test/test_float.py | 18 +++++ ...-12-22-22-37-53.gh-issue-143006.ZBQwbN.rst | 2 + Objects/floatobject.c | 77 ++++++++----------- 3 files changed, 52 insertions(+), 45 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-22-37-53.gh-issue-143006.ZBQwbN.rst diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 00518abcb11b46..c03b0a09f71889 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -651,6 +651,24 @@ class F(float, H): value = F('nan') self.assertEqual(hash(value), object.__hash__(value)) + def test_issue_gh143006(self): + # When comparing negative non-integer float and int with the + # same number of bits in the integer part, __neg__() in the + # int subclass returning not an int caused an assertion error. + class EvilInt(int): + def __neg__(self): + return "" + + i = -1 << 50 + f = float(i) - 0.5 + i = EvilInt(i) + self.assertFalse(f == i) + self.assertTrue(f != i) + self.assertTrue(f < i) + self.assertTrue(f <= i) + self.assertFalse(f > i) + self.assertFalse(f >= i) + @unittest.skipUnless(hasattr(float, "__getformat__"), "requires __getformat__") class FormatFunctionsTestCase(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-22-37-53.gh-issue-143006.ZBQwbN.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-22-37-53.gh-issue-143006.ZBQwbN.rst new file mode 100644 index 00000000000000..f25620389fd5cd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-22-37-53.gh-issue-143006.ZBQwbN.rst @@ -0,0 +1,2 @@ +Fix a possible assertion error when comparing negative non-integer ``float`` +and ``int`` with the same number of bits in the integer part. diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 2cb690748d9de4..24b3eb146ebffd 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -435,27 +435,17 @@ float_richcompare(PyObject *v, PyObject *w, int op) assert(vsign != 0); /* if vsign were 0, then since wsign is * not 0, we would have taken the * vsign != wsign branch at the start */ - /* We want to work with non-negative numbers. */ - if (vsign < 0) { - /* "Multiply both sides" by -1; this also swaps the - * comparator. - */ - i = -i; - op = _Py_SwappedOp[op]; - } - assert(i > 0.0); (void) frexp(i, &exponent); /* exponent is the # of bits in v before the radix point; * we know that nbits (the # of bits in w) > 48 at this point */ if (exponent < nbits) { - i = 1.0; - j = 2.0; + j = i; + i = 0.0; goto Compare; } if (exponent > nbits) { - i = 2.0; - j = 1.0; + j = 0.0; goto Compare; } /* v and w have the same number of bits before the radix @@ -467,50 +457,47 @@ float_richcompare(PyObject *v, PyObject *w, int op) double intpart; PyObject *result = NULL; PyObject *vv = NULL; - PyObject *ww = w; - if (wsign < 0) { - ww = PyNumber_Negative(w); - if (ww == NULL) - goto Error; + fracpart = modf(i, &intpart); + if (fracpart != 0.0) { + switch (op) { + case Py_EQ: + Py_RETURN_FALSE; + case Py_NE: + Py_RETURN_TRUE; + case Py_LE: + if (vsign > 0) { + op = Py_LT; + } + break; + case Py_GE: + if (vsign < 0) { + op = Py_GT; + } + break; + case Py_LT: + if (vsign < 0) { + op = Py_LE; + } + break; + case Py_GT: + if (vsign > 0) { + op = Py_GE; + } + break; + } } - else - Py_INCREF(ww); - fracpart = modf(i, &intpart); vv = PyLong_FromDouble(intpart); if (vv == NULL) goto Error; - if (fracpart != 0.0) { - /* Shift left, and or a 1 bit into vv - * to represent the lost fraction. - */ - PyObject *temp; - - temp = _PyLong_Lshift(ww, 1); - if (temp == NULL) - goto Error; - Py_SETREF(ww, temp); - - temp = _PyLong_Lshift(vv, 1); - if (temp == NULL) - goto Error; - Py_SETREF(vv, temp); - - temp = PyNumber_Or(vv, _PyLong_GetOne()); - if (temp == NULL) - goto Error; - Py_SETREF(vv, temp); - } - - r = PyObject_RichCompareBool(vv, ww, op); + r = PyObject_RichCompareBool(vv, w, op); if (r < 0) goto Error; result = PyBool_FromLong(r); Error: Py_XDECREF(vv); - Py_XDECREF(ww); return result; } } /* else if (PyLong_Check(w)) */ From bbec97a6317a0bfa25f2127ffbef58842b245166 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 23 Dec 2025 12:34:31 +0200 Subject: [PATCH 2/4] iUpdate comments. --- Objects/floatobject.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 24b3eb146ebffd..95d1c3f6c4abe0 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -449,8 +449,8 @@ float_richcompare(PyObject *v, PyObject *w, int op) goto Compare; } /* v and w have the same number of bits before the radix - * point. Construct two ints that have the same comparison - * outcome. + * point. Construct an int from the integer part of v and + * update op if necessary, so comparing two ints has the same outcome. */ { double fracpart; @@ -467,22 +467,22 @@ float_richcompare(PyObject *v, PyObject *w, int op) Py_RETURN_TRUE; case Py_LE: if (vsign > 0) { - op = Py_LT; + op = Py_LT; // v <= w <=> trunc(v) < w } break; case Py_GE: if (vsign < 0) { - op = Py_GT; + op = Py_GT; // v >= w <=> trunc(v) > w } break; case Py_LT: if (vsign < 0) { - op = Py_LE; + op = Py_LE; // v < w <=> trunc(v) <= w } break; case Py_GT: if (vsign > 0) { - op = Py_GE; + op = Py_GE; // v > w <=> trunc(v) >= w } break; } From f4cd2f2b8ca04811b3863353a6d5464527344c60 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 24 Dec 2025 12:00:56 +0200 Subject: [PATCH 3/4] Update Objects/floatobject.c --- Objects/floatobject.c | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 95d1c3f6c4abe0..375826ab4e2196 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -465,25 +465,13 @@ float_richcompare(PyObject *v, PyObject *w, int op) Py_RETURN_FALSE; case Py_NE: Py_RETURN_TRUE; - case Py_LE: - if (vsign > 0) { - op = Py_LT; // v <= w <=> trunc(v) < w - } - break; - case Py_GE: - if (vsign < 0) { - op = Py_GT; // v >= w <=> trunc(v) > w - } - break; case Py_LT: - if (vsign < 0) { - op = Py_LE; // v < w <=> trunc(v) <= w - } + case Py_LE: + op = vsign > 0 ? Py_LT : Py_LE; break; case Py_GT: - if (vsign > 0) { - op = Py_GE; // v > w <=> trunc(v) >= w - } + case Py_GE: + op = vsign > 0 ? Py_GE : Py_GT; break; } } From ea1ce5d61cd1cb89cc03313f23c1fa95dd62357d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 24 Dec 2025 12:34:53 +0200 Subject: [PATCH 4/4] Add more explanation comments. --- Objects/floatobject.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 375826ab4e2196..579765281ca484 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -461,14 +461,24 @@ float_richcompare(PyObject *v, PyObject *w, int op) fracpart = modf(i, &intpart); if (fracpart != 0.0) { switch (op) { + /* Non-integer float never equals to an int. */ case Py_EQ: Py_RETURN_FALSE; case Py_NE: Py_RETURN_TRUE; + /* For non-integer float, v <= w <=> v < w. + * If v > 0: trunc(v) < v < trunc(v) + 1 + * v < w => trunc(v) < w + * trunc(v) < w => trunc(v) + 1 <= w => v < w + * If v < 0: trunc(v) - 1 < v < trunc(v) + * v < w => trunc(v) - 1 < w => trunc(v) <= w + * trunc(v) <= w => v < w + */ case Py_LT: case Py_LE: op = vsign > 0 ? Py_LT : Py_LE; break; + /* The same as above, but with opposite directions. */ case Py_GT: case Py_GE: op = vsign > 0 ? Py_GE : Py_GT;