From d08dd8cfe52615fe0434d5234da3cb5d5550001b Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Mon, 2 Nov 2026 15:57:09 +0100 Subject: [PATCH] Fix float floordiv rounding Fixes #1274 --- .../src/tests/test_float.py | 61 +++++++++++++++++++ .../objects/floats/FloatBuiltins.java | 25 +++++++- .../PyNumberFloorDivideFastPathsBase.java | 3 +- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_float.py b/graalpython/com.oracle.graal.python.test/src/tests/test_float.py index ae3a90f55b..bfd86d6646 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_float.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_float.py @@ -51,6 +51,67 @@ class BasicTests(unittest.TestCase): + def test_floor_divmod_rounding(self): + cases = [ + (1.0, 0.1, 9.0, 0.09999999999999995), + (-1.0, -0.1, 9.0, -0.09999999999999995), + (-1.0, 0.1, -10.0, 5.551115123125783e-17), + (1.0, -0.1, -10.0, -5.551115123125783e-17), + (-32.0, -0.8, 39.0, -0.7999999999999983), + (1, 0.1, 9.0, 0.09999999999999995), + (1.0, 3, 0.0, 1.0), + (1.0, 0.25, 4.0, 0.0), + (1.0, -0.25, -4.0, -0.0), + # The quotient derived from the remainder is 6.999999999999999. + (54.3632596643497, 7.401038915431183, 7.0, 2.5559872563314165), + (1e308, 1e-308, INF, 3.498445546245627e-309), + ] + for a, b, q, r in cases: + with self.subTest(a=a, b=b): + self.assertEqual(a // b, q) + self.assertEqual(float.__floordiv__(float(a), b), q) + self.assertEqual(a % b, r) + self.assertEqual(divmod(a, b), (q, r)) + self.assertIs(type(a // b), float) + if q != INF: + self.assertAlmostEqual(q * b + r, a) + + def test_floor_divmod_signed_zero(self): + for a in (0.0, -0.0, 1.0, -1.0, 1e-300, -1e-300): + for b in (2.0, -2.0, 1e300, -1e300, INF, -INF): + with self.subTest(a=a, b=b): + expected_q = -1.0 if a != 0.0 and (a < 0) != (b < 0) else copysign(0.0, a / b) + q, r = divmod(a, b) + self.assertEqual(q, expected_q) + self.assertEqual(a // b, expected_q) + self.assertEqual(copysign(1.0, q), copysign(1.0, expected_q)) + self.assertEqual(copysign(1.0, a // b), copysign(1.0, expected_q)) + self.assertEqual(r, a % b) + self.assertEqual(copysign(1.0, r), copysign(1.0, b)) + + def test_floor_divmod_nonfinite(self): + for a in (INF, -INF, NAN): + for b in (1.0, -1.0, INF, -INF, NAN): + with self.subTest(a=a, b=b): + self.assertTrue(isnan(a // b)) + q, r = divmod(a, b) + self.assertTrue(isnan(q)) + self.assertTrue(isnan(r)) + for a in (0.0, 1.0, -1.0): + self.assertTrue(isnan(a // NAN)) + q, r = divmod(a, NAN) + self.assertTrue(isnan(q)) + self.assertTrue(isnan(r)) + + def test_floor_divmod_zero_divisor(self): + for a in (0.0, 1.0, -1.0, INF, NAN): + for b in (0.0, -0.0): + with self.subTest(a=a, b=b): + with self.assertRaises(ZeroDivisionError): + a // b + with self.assertRaises(ZeroDivisionError): + divmod(a, b) + def test_is_integer(self): assert not (3.14).is_integer() assert not (-3.14).is_integer() diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java index 858b8102b5..bc72641616 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java @@ -574,11 +574,30 @@ public static PowNode create() { @Slot(value = SlotKind.nb_floor_divide, isComplex = true) @GenerateNodeFactory - abstract static class FloorDivNode extends AbstractNumericBinaryBuiltin { + public abstract static class FloorDivNode extends AbstractNumericBinaryBuiltin { @Override protected Object op(double left, double right) { raiseDivisionByZero(right == 0); - return Math.floor(left / right); + return floorDiv(left, right); + } + + public static double floorDiv(double left, double right) { + // See cpython://Objects/floatobject.c#_float_div_mod. + double mod = left % right; + // Derive the quotient from the remainder: left / right may round up to an integer. + double div = (left - mod) / right; + if (mod != 0.0 && (right < 0) != (mod < 0)) { + div -= 1.0; + } + if (div != 0.0) { + // Floating-point arithmetic may leave div just below an integer. + double floordiv = Math.floor(div); + if (div - floordiv > 0.5) { + floordiv += 1.0; + } + return floordiv; + } + return Math.copySign(0.0, left / right); } } @@ -590,7 +609,7 @@ abstract static class DivModNode extends AbstractNumericBinaryBuiltin { protected PTuple op(double left, double right) { raiseDivisionByZero(right == 0); PythonLanguage language = PythonLanguage.get(this); - return PFactory.createTuple(language, new Object[]{Math.floor(left / right), ModNode.mod(left, right)}); + return PFactory.createTuple(language, new Object[]{FloorDivNode.floorDiv(left, right), ModNode.mod(left, right)}); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/fastpath/PyNumberFloorDivideFastPathsBase.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/fastpath/PyNumberFloorDivideFastPathsBase.java index 09efdb2d1d..750a0639d3 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/fastpath/PyNumberFloorDivideFastPathsBase.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/fastpath/PyNumberFloorDivideFastPathsBase.java @@ -40,6 +40,7 @@ */ package com.oracle.graal.python.lib.fastpath; +import com.oracle.graal.python.builtins.objects.floats.FloatBuiltins; import com.oracle.graal.python.nodes.expression.BinaryOpNode; import com.oracle.graal.python.nodes.truffle.PythonIntToLongTypes; import com.oracle.truffle.api.dsl.GenerateCached; @@ -80,7 +81,7 @@ public static double doDL(double left, long right) { @Specialization(guards = "!isZero(right)") public static double doDD(double left, double right) { - return Math.floor(left / right); + return FloatBuiltins.FloorDivNode.floorDiv(left, right); } public static boolean specialCase(int left, int right) {