gh-69639: Add mixed-mode rules for complex arithmetic (C-like) (GH-124829)

"Generally, mixed-mode arithmetic combining real and complex variables should
be performed directly, not by first coercing the real to complex, lest the sign
of zero be rendered uninformative; the same goes for combinations of pure
imaginary quantities with complex variables." (c) Kahan, W: Branch cuts for
complex elementary functions.

This patch implements mixed-mode arithmetic rules, combining real and
complex variables as specified by C standards since C99 (in particular,
there is no special version for the true division with real lhs
operand).  Most C compilers implementing C99+ Annex G have only these
special rules (without support for imaginary type, which is going to be
deprecated in C2y).
This commit is contained in:
Sergey B Kirpichev 2024-11-26 18:57:39 +03:00 committed by GitHub
parent dcf629213b
commit 987311d42e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 449 additions and 98 deletions

View file

@ -8,6 +8,7 @@
#include "Python.h"
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_complexobject.h" // _PyComplex_FormatAdvancedWriter()
#include "pycore_floatobject.h" // _Py_convert_int_to_double()
#include "pycore_long.h" // _PyLong_GetZero()
#include "pycore_object.h" // _PyObject_Init()
#include "pycore_pymath.h" // _Py_ADJUST_ERANGE2()
@ -34,6 +35,20 @@ _Py_c_sum(Py_complex a, Py_complex b)
return r;
}
Py_complex
_Py_cr_sum(Py_complex a, double b)
{
Py_complex r = a;
r.real += b;
return r;
}
static inline Py_complex
_Py_rc_sum(double a, Py_complex b)
{
return _Py_cr_sum(b, a);
}
Py_complex
_Py_c_diff(Py_complex a, Py_complex b)
{
@ -43,6 +58,23 @@ _Py_c_diff(Py_complex a, Py_complex b)
return r;
}
Py_complex
_Py_cr_diff(Py_complex a, double b)
{
Py_complex r = a;
r.real -= b;
return r;
}
Py_complex
_Py_rc_diff(double a, Py_complex b)
{
Py_complex r;
r.real = a - b.real;
r.imag = -b.imag;
return r;
}
Py_complex
_Py_c_neg(Py_complex a)
{
@ -61,6 +93,21 @@ _Py_c_prod(Py_complex a, Py_complex b)
return r;
}
Py_complex
_Py_cr_prod(Py_complex a, double b)
{
Py_complex r = a;
r.real *= b;
r.imag *= b;
return r;
}
static inline Py_complex
_Py_rc_prod(double a, Py_complex b)
{
return _Py_cr_prod(b, a);
}
/* Avoid bad optimization on Windows ARM64 until the compiler is fixed */
#ifdef _M_ARM64
#pragma optimize("", off)
@ -143,6 +190,64 @@ _Py_c_quot(Py_complex a, Py_complex b)
return r;
}
Py_complex
_Py_cr_quot(Py_complex a, double b)
{
Py_complex r = a;
if (b) {
r.real /= b;
r.imag /= b;
}
else {
errno = EDOM;
r.real = r.imag = 0.0;
}
return r;
}
/* an equivalent of _Py_c_quot() function, when 1st argument is real */
Py_complex
_Py_rc_quot(double a, Py_complex b)
{
Py_complex r;
const double abs_breal = b.real < 0 ? -b.real : b.real;
const double abs_bimag = b.imag < 0 ? -b.imag : b.imag;
if (abs_breal >= abs_bimag) {
if (abs_breal == 0.0) {
errno = EDOM;
r.real = r.imag = 0.0;
}
else {
const double ratio = b.imag / b.real;
const double denom = b.real + b.imag * ratio;
r.real = a / denom;
r.imag = (-a * ratio) / denom;
}
}
else if (abs_bimag >= abs_breal) {
const double ratio = b.real / b.imag;
const double denom = b.real * ratio + b.imag;
assert(b.imag != 0.0);
r.real = (a * ratio) / denom;
r.imag = (-a) / denom;
}
else {
r.real = r.imag = Py_NAN;
}
if (isnan(r.real) && isnan(r.imag) && isfinite(a)
&& (isinf(abs_breal) || isinf(abs_bimag)))
{
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*x);
r.imag = 0.0 * (-a*y);
}
return r;
}
#ifdef _M_ARM64
#pragma optimize("", on)
#endif
@ -474,84 +579,91 @@ complex_hash(PyComplexObject *v)
}
/* This macro may return! */
#define TO_COMPLEX(obj, c) \
if (PyComplex_Check(obj)) \
c = ((PyComplexObject *)(obj))->cval; \
else if (to_complex(&(obj), &(c)) < 0) \
#define TO_COMPLEX(obj, c) \
if (PyComplex_Check(obj)) \
c = ((PyComplexObject *)(obj))->cval; \
else if (real_to_complex(&(obj), &(c)) < 0) \
return (obj)
static int
to_complex(PyObject **pobj, Py_complex *pc)
real_to_double(PyObject **pobj, double *dbl)
{
PyObject *obj = *pobj;
pc->real = pc->imag = 0.0;
if (PyLong_Check(obj)) {
pc->real = PyLong_AsDouble(obj);
if (pc->real == -1.0 && PyErr_Occurred()) {
*pobj = NULL;
return -1;
}
return 0;
}
if (PyFloat_Check(obj)) {
pc->real = PyFloat_AsDouble(obj);
return 0;
*dbl = PyFloat_AS_DOUBLE(obj);
}
*pobj = Py_NewRef(Py_NotImplemented);
return -1;
}
static PyObject *
complex_add(PyObject *v, PyObject *w)
{
Py_complex result;
Py_complex a, b;
TO_COMPLEX(v, a);
TO_COMPLEX(w, b);
result = _Py_c_sum(a, b);
return PyComplex_FromCComplex(result);
}
static PyObject *
complex_sub(PyObject *v, PyObject *w)
{
Py_complex result;
Py_complex a, b;
TO_COMPLEX(v, a);
TO_COMPLEX(w, b);
result = _Py_c_diff(a, b);
return PyComplex_FromCComplex(result);
}
static PyObject *
complex_mul(PyObject *v, PyObject *w)
{
Py_complex result;
Py_complex a, b;
TO_COMPLEX(v, a);
TO_COMPLEX(w, b);
result = _Py_c_prod(a, b);
return PyComplex_FromCComplex(result);
}
static PyObject *
complex_div(PyObject *v, PyObject *w)
{
Py_complex quot;
Py_complex a, b;
TO_COMPLEX(v, a);
TO_COMPLEX(w, b);
errno = 0;
quot = _Py_c_quot(a, b);
if (errno == EDOM) {
PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
return NULL;
else if (_Py_convert_int_to_double(pobj, dbl) < 0) {
return -1;
}
return PyComplex_FromCComplex(quot);
return 0;
}
static int
real_to_complex(PyObject **pobj, Py_complex *pc)
{
pc->imag = 0.0;
return real_to_double(pobj, &(pc->real));
}
/* Complex arithmetic rules implement special mixed-mode case where combining
a pure-real (float or int) value and a complex value is performed directly
without first coercing the real value to a complex value.
Let us consider the addition as an example, assuming that ints are implicitly
converted to floats. We have the following rules (up to variants with changed
order of operands):
complex(a, b) + complex(c, d) = complex(a + c, b + d)
float(a) + complex(b, c) = complex(a + b, c)
Similar rules are implemented for subtraction, multiplication and division.
See C11's Annex G, sections G.5.1 and G.5.2.
*/
#define COMPLEX_BINOP(NAME, FUNC) \
static PyObject * \
complex_##NAME(PyObject *v, PyObject *w) \
{ \
Py_complex a; \
errno = 0; \
if (PyComplex_Check(w)) { \
Py_complex b = ((PyComplexObject *)w)->cval; \
if (PyComplex_Check(v)) { \
a = ((PyComplexObject *)v)->cval; \
a = _Py_c_##FUNC(a, b); \
} \
else if (real_to_double(&v, &a.real) < 0) { \
return v; \
} \
else { \
a = _Py_rc_##FUNC(a.real, b); \
} \
} \
else if (!PyComplex_Check(v)) { \
Py_RETURN_NOTIMPLEMENTED; \
} \
else { \
a = ((PyComplexObject *)v)->cval; \
double b; \
if (real_to_double(&w, &b) < 0) { \
return w; \
} \
a = _Py_cr_##FUNC(a, b); \
} \
if (errno == EDOM) { \
PyErr_SetString(PyExc_ZeroDivisionError, \
"division by zero"); \
return NULL; \
} \
return PyComplex_FromCComplex(a); \
}
COMPLEX_BINOP(add, sum)
COMPLEX_BINOP(mul, prod)
COMPLEX_BINOP(sub, diff)
COMPLEX_BINOP(div, quot)
static PyObject *
complex_pow(PyObject *v, PyObject *w, PyObject *z)
{