From 06ac53d4c15d1b865c7e6879f884cb887bd67d6f Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 22 May 2024 05:10:19 +0300 Subject: [PATCH 1/4] gh-119372: recover inf's and zeros in _Py_c_quot In some cases, previosly computed as (nan+nanj), we could recover meaningful component values in the result, see e.g. the C11, Annex G.5.2, routine _Cdivd(): >>> from cmath import inf, infj, nanj >>> (1+1j)/(inf+infj) # was (nan+nanj) 0j >>> (inf+nanj)/complex(2**1000, 2**-1000) (inf-infj) --- Lib/test/test_complex.py | 21 ++++++++++++++++ ...-05-22-12-49-03.gh-issue-119372.PXig1R.rst | 2 ++ Objects/complexobject.c | 25 +++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-22-12-49-03.gh-issue-119372.PXig1R.rst diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index fa3017b24e16c8..d0bf46e630f7a7 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -73,6 +73,10 @@ def assertFloatsAreIdentical(self, x, y): msg += ': zeros have different signs' self.fail(msg.format(x, y)) + def assertComplexesAreIdentical(self, x, y): + self.assertFloatsAreIdentical(x.real, y.real) + self.assertFloatsAreIdentical(x.imag, y.imag) + def assertClose(self, x, y, eps=1e-9): """Return true iff complexes x and y "are close".""" self.assertCloseAbs(x.real, y.real, eps) @@ -118,6 +122,23 @@ def test_truediv(self): self.assertTrue(isnan(z.real)) self.assertTrue(isnan(z.imag)) + self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(0+1j)'), complex('(nan-infj)')) + + # test recover of infs if numerator has infinities and denominator is finite + self.assertComplexesAreIdentical(complex('(inf-infj)')/(1+0j), complex('(inf-infj)')) + self.assertComplexesAreIdentical(complex('(inf+infj)')/(0.0+1j), complex('(inf-infj)')) + self.assertComplexesAreIdentical(complex('(nan+infj)')/complex(2**1000, 2**-1000), complex('(inf+infj)')) + self.assertComplexesAreIdentical(complex('(inf+nanj)')/complex(2**1000, 2**-1000), complex('(inf-infj)')) + + # test recover of zeros if denominator is infinite + self.assertComplexesAreIdentical((1+1j)/complex('(inf+infj)'), (0.0+0j)) + self.assertComplexesAreIdentical((1+1j)/complex('(inf-infj)'), (0.0+0j)) + self.assertComplexesAreIdentical((1+1j)/complex('(-inf+infj)'), complex('(0.0-0j)')) + self.assertComplexesAreIdentical((1+1j)/complex('(-inf-infj)'), complex('(-0+0j)')) + self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(inf+infj)'), complex('(nan+nanj)')) + self.assertComplexesAreIdentical(complex('(1+infj)')/complex('(inf+infj)'), complex('(nan+nanj)')) + self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(1+infj)'), complex('(nan+nanj)')) + def test_truediv_zero_division(self): for a, b in ZERO_DIVISION: with self.assertRaises(ZeroDivisionError): diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-22-12-49-03.gh-issue-119372.PXig1R.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-22-12-49-03.gh-issue-119372.PXig1R.rst new file mode 100644 index 00000000000000..aa628299abbd95 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-22-12-49-03.gh-issue-119372.PXig1R.rst @@ -0,0 +1,2 @@ +Correct invalid corner cases in complex division (resulted in ``(nan+nanj)`` +output), e.g. ``1/complex('(inf+infj)')``. Patch by Sergey B Kirpichev. diff --git a/Objects/complexobject.c b/Objects/complexobject.c index d8b0e84da5df4a..45781c292eabc3 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -88,8 +88,7 @@ _Py_c_quot(Py_complex a, Py_complex b) * numerators and denominator by whichever of {b.real, b.imag} has * larger magnitude. The earliest reference I found was to CACM * Algorithm 116 (Complex Division, Robert L. Smith, Stanford - * University). As usual, though, we're still ignoring all IEEE - * endcases. + * University). */ Py_complex r; /* the result */ const double abs_breal = b.real < 0 ? -b.real : b.real; @@ -120,6 +119,28 @@ _Py_c_quot(Py_complex a, Py_complex b) /* At least one of b.real or b.imag is a NaN */ r.real = r.imag = Py_NAN; } + + /* Recover infinities and zeros that computed as nan+nanj. See e.g. + the C11, Annex G.5.2, routine _Cdivd(). */ + if (isnan(r.real) && isnan(r.imag)) { + if ((isinf(a.real) || isinf(a.imag)) + && isfinite(b.real) && isfinite(b.imag)) + { + const double x = copysign(isinf(a.real) ? 1.0 : 0.0, a.real); + const double y = copysign(isinf(a.imag) ? 1.0 : 0.0, a.imag); + r.real = Py_INFINITY * (x*b.real + y*b.imag); + r.imag = Py_INFINITY * (y*b.real - x*b.imag); + } + else if ((fmax(abs_breal, abs_bimag) == Py_INFINITY) + && isfinite(a.real) && isfinite(a.imag)) + { + const double x = copysign(isinf(b.real) ? 1.0 : 0.0, b.real); + const double y = copysign(isinf(b.imag) ? 1.0 : 0.0, b.imag); + r.real = 0.0 * (a.real*x + a.imag*y); + r.imag = 0.0 * (a.imag*x - a.real*y); + } + } + return r; } #ifdef _M_ARM64 From fc3f91d90c63656556717ab697ace20da660f802 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 23 May 2024 19:00:27 +0300 Subject: [PATCH 2/4] Tests reformatted --- Lib/test/test_complex.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index d0bf46e630f7a7..a22466e7b27fe4 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -122,22 +122,32 @@ def test_truediv(self): self.assertTrue(isnan(z.real)) self.assertTrue(isnan(z.imag)) - self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(0+1j)'), complex('(nan-infj)')) - - # test recover of infs if numerator has infinities and denominator is finite - self.assertComplexesAreIdentical(complex('(inf-infj)')/(1+0j), complex('(inf-infj)')) - self.assertComplexesAreIdentical(complex('(inf+infj)')/(0.0+1j), complex('(inf-infj)')) - self.assertComplexesAreIdentical(complex('(nan+infj)')/complex(2**1000, 2**-1000), complex('(inf+infj)')) - self.assertComplexesAreIdentical(complex('(inf+nanj)')/complex(2**1000, 2**-1000), complex('(inf-infj)')) + self.assertComplexesAreIdentical(complex(INF, 1)/(0.0+1j), + complex(NAN, -INF)) + + # test recover of infs if numerator has infs and denominator is finite + self.assertComplexesAreIdentical(complex(INF, -INF)/(1+0j), + complex(INF, -INF)) + self.assertComplexesAreIdentical(complex(INF, INF)/(0.0+1j), + complex(INF, -INF)) + self.assertComplexesAreIdentical(complex(NAN, INF)/complex(2**1000, 2**-1000), + complex(INF, INF)) + self.assertComplexesAreIdentical(complex(INF, NAN)/complex(2**1000, 2**-1000), + complex(INF, -INF)) # test recover of zeros if denominator is infinite - self.assertComplexesAreIdentical((1+1j)/complex('(inf+infj)'), (0.0+0j)) - self.assertComplexesAreIdentical((1+1j)/complex('(inf-infj)'), (0.0+0j)) - self.assertComplexesAreIdentical((1+1j)/complex('(-inf+infj)'), complex('(0.0-0j)')) - self.assertComplexesAreIdentical((1+1j)/complex('(-inf-infj)'), complex('(-0+0j)')) - self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(inf+infj)'), complex('(nan+nanj)')) - self.assertComplexesAreIdentical(complex('(1+infj)')/complex('(inf+infj)'), complex('(nan+nanj)')) - self.assertComplexesAreIdentical(complex('(inf+1j)')/complex('(1+infj)'), complex('(nan+nanj)')) + self.assertComplexesAreIdentical((1+1j)/complex(INF, INF), (0.0+0j)) + self.assertComplexesAreIdentical((1+1j)/complex(INF, -INF), (0.0+0j)) + self.assertComplexesAreIdentical((1+1j)/complex(-INF, INF), + complex(0.0, -0.0)) + self.assertComplexesAreIdentical((1+1j)/complex(-INF, -INF), + complex(-0.0, 0)) + self.assertComplexesAreIdentical((INF+1j)/complex(INF, INF), + complex(NAN, NAN)) + self.assertComplexesAreIdentical(complex(1, INF)/complex(INF, INF), + complex(NAN, NAN)) + self.assertComplexesAreIdentical(complex(INF, 1)/complex(1, INF), + complex(NAN, NAN)) def test_truediv_zero_division(self): for a, b in ZERO_DIVISION: From 99669a19d736aa800892640e7b6e6430fd0c2409 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 27 May 2024 18:07:56 +0300 Subject: [PATCH 3/4] Address review: use Py_IS_* macros --- Objects/complexobject.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 45781c292eabc3..5c07128ce6069a 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -122,20 +122,20 @@ _Py_c_quot(Py_complex a, Py_complex b) /* Recover infinities and zeros that computed as nan+nanj. See e.g. the C11, Annex G.5.2, routine _Cdivd(). */ - if (isnan(r.real) && isnan(r.imag)) { - if ((isinf(a.real) || isinf(a.imag)) - && isfinite(b.real) && isfinite(b.imag)) + if (Py_IS_NAN(r.real) && Py_IS_NAN(r.imag)) { + if ((Py_IS_INFINITY(a.real) || Py_IS_INFINITY(a.imag)) + && Py_IS_FINITE(b.real) && Py_IS_FINITE(b.imag)) { - const double x = copysign(isinf(a.real) ? 1.0 : 0.0, a.real); - const double y = copysign(isinf(a.imag) ? 1.0 : 0.0, a.imag); + const double x = copysign(Py_IS_INFINITY(a.real) ? 1.0 : 0.0, a.real); + const double y = copysign(Py_IS_INFINITY(a.imag) ? 1.0 : 0.0, a.imag); r.real = Py_INFINITY * (x*b.real + y*b.imag); r.imag = Py_INFINITY * (y*b.real - x*b.imag); } - else if ((fmax(abs_breal, abs_bimag) == Py_INFINITY) - && isfinite(a.real) && isfinite(a.imag)) + else if ((Py_IS_INFINITY(abs_breal) || Py_IS_INFINITY(abs_bimag)) + && Py_IS_FINITE(a.real) && Py_IS_FINITE(a.imag)) { - const double x = copysign(isinf(b.real) ? 1.0 : 0.0, b.real); - const double y = copysign(isinf(b.imag) ? 1.0 : 0.0, b.imag); + const double x = copysign(Py_IS_INFINITY(b.real) ? 1.0 : 0.0, b.real); + const double y = copysign(Py_IS_INFINITY(b.imag) ? 1.0 : 0.0, b.imag); r.real = 0.0 * (a.real*x + a.imag*y); r.imag = 0.0 * (a.imag*x - a.real*y); } From 1c9ccc4d70f697dca04d44f268f10341d6618ea1 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 30 May 2024 18:07:26 +0300 Subject: [PATCH 4/4] Partially revert 99669a1 (don't use Py_IS_* macros) --- Objects/complexobject.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 630cb15bb540d6..ca5ed403167035 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -122,20 +122,20 @@ _Py_c_quot(Py_complex a, Py_complex b) /* Recover infinities and zeros that computed as nan+nanj. See e.g. the C11, Annex G.5.2, routine _Cdivd(). */ - if (Py_IS_NAN(r.real) && Py_IS_NAN(r.imag)) { - if ((Py_IS_INFINITY(a.real) || Py_IS_INFINITY(a.imag)) - && Py_IS_FINITE(b.real) && Py_IS_FINITE(b.imag)) + if (isnan(r.real) && isnan(r.imag)) { + if ((isinf(a.real) || isinf(a.imag)) + && isfinite(b.real) && isfinite(b.imag)) { - const double x = copysign(Py_IS_INFINITY(a.real) ? 1.0 : 0.0, a.real); - const double y = copysign(Py_IS_INFINITY(a.imag) ? 1.0 : 0.0, a.imag); + const double x = copysign(isinf(a.real) ? 1.0 : 0.0, a.real); + const double y = copysign(isinf(a.imag) ? 1.0 : 0.0, a.imag); r.real = Py_INFINITY * (x*b.real + y*b.imag); r.imag = Py_INFINITY * (y*b.real - x*b.imag); } - else if ((Py_IS_INFINITY(abs_breal) || Py_IS_INFINITY(abs_bimag)) - && Py_IS_FINITE(a.real) && Py_IS_FINITE(a.imag)) + else if ((isinf(abs_breal) || isinf(abs_bimag)) + && isfinite(a.real) && isfinite(a.imag)) { - const double x = copysign(Py_IS_INFINITY(b.real) ? 1.0 : 0.0, b.real); - const double y = copysign(Py_IS_INFINITY(b.imag) ? 1.0 : 0.0, b.imag); + const double x = copysign(isinf(b.real) ? 1.0 : 0.0, b.real); + const double y = copysign(isinf(b.imag) ? 1.0 : 0.0, b.imag); r.real = 0.0 * (a.real*x + a.imag*y); r.imag = 0.0 * (a.imag*x - a.real*y); }