Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-69639: add mixed-mode rules for complex arithmetic (C-like) #124829

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3980363
gh-69639: add mixed-mode rules for complex arithmetic (C-like)
skirpichev Apr 23, 2024
46521c3
address review:
skirpichev Oct 2, 2024
1323f4d
Update Objects/complexobject.c
skirpichev Oct 2, 2024
5021a9b
address review: -> real_to_double
skirpichev Oct 2, 2024
c7308ef
Add _Py_cd_* and _Py_dc_* functions
skirpichev Oct 2, 2024
ee1aa01
+ what's new
skirpichev Oct 2, 2024
da566dd
+ trying to document new coersion rules in the reference
skirpichev Oct 2, 2024
714b731
address review: expressions.rst
skirpichev Oct 3, 2024
7a04eb0
address review: reword what's new
skirpichev Oct 3, 2024
1d42c10
cleanup: just one macro
skirpichev Oct 4, 2024
d6e9d14
renamed c-api helpers: _dc_ -> _rc_ and _cd_ -> _cr_
skirpichev Oct 6, 2024
2db0072
1d42c10202 +1
skirpichev Oct 6, 2024
46bed69
+ tests for semi-private C-API
skirpichev Oct 6, 2024
d6e4504
Merge branch 'master' into complex-float-arith-69639
skirpichev Oct 6, 2024
cc298e5
Apply suggestions from code review
skirpichev Oct 8, 2024
ee22926
Apply suggestions from code review
skirpichev Oct 13, 2024
220c551
Apply suggestions from code review
skirpichev Oct 14, 2024
f068384
Apply suggestions from code review
skirpichev Oct 14, 2024
8cdd514
Merge branch 'main' into complex-float-arith-69639
skirpichev Oct 30, 2024
7fb1be1
+ move news
skirpichev Oct 30, 2024
badf492
address review:
skirpichev Oct 31, 2024
3ef5287
Merge branch 'master' into complex-float-arith-69639
skirpichev Oct 31, 2024
38762b3
address review: rename symbols in commentary to avoid clash with macro
skirpichev Oct 31, 2024
e685bd9
Update Objects/complexobject.c
skirpichev Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Doc/library/cmath.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ the function is then applied to the result of the conversion.
imaginary axis we look at the sign of the real part.

For example, the :func:`cmath.sqrt` function has a branch cut along the
negative real axis. An argument of ``complex(-2.0, -0.0)`` is treated as
negative real axis. An argument of ``-2-0j`` is treated as
though it lies *below* the branch cut, and so gives a result on the negative
imaginary axis::

>>> cmath.sqrt(complex(-2.0, -0.0))
>>> cmath.sqrt(-2-0j)
-1.4142135623730951j

But an argument of ``complex(-2.0, 0.0)`` is treated as though it lies above
But an argument of ``-2+0j`` is treated as though it lies above
the branch cut::

>>> cmath.sqrt(complex(-2.0, 0.0))
>>> cmath.sqrt(-2+0j)
1.4142135623730951j


Expand Down Expand Up @@ -63,9 +63,9 @@ rectangular coordinates to polar coordinates and back.
along the negative real axis. The sign of the result is the same as the
sign of ``x.imag``, even when ``x.imag`` is zero::

>>> phase(complex(-1.0, 0.0))
>>> phase(-1+0j)
3.141592653589793
>>> phase(complex(-1.0, -0.0))
>>> phase(-1-0j)
-3.141592653589793


Expand Down
16 changes: 11 additions & 5 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ numeric literal yields an imaginary number (a complex number with a zero real
part) which you can add to an integer or float to get a complex number with real
and imaginary parts.

The constructors :func:`int`, :func:`float`, and
:func:`complex` can be used to produce numbers of a specific type.

.. index::
single: arithmetic
pair: built-in function; int
Expand All @@ -262,12 +265,15 @@ and imaginary parts.

Python fully supports mixed arithmetic: when a binary arithmetic operator has
operands of different numeric types, the operand with the "narrower" type is
widened to that of the other, where integer is narrower than floating point,
which is narrower than complex. A comparison between numbers of different types
behaves as though the exact values of those numbers were being compared. [2]_
widened to that of the other, where integer is narrower than floating point.
Arithmetic with complex and real operands is defined by the usual mathematical
formula, for example::

The constructors :func:`int`, :func:`float`, and
:func:`complex` can be used to produce numbers of a specific type.
x + complex(u, v) = complex(x + u, v)
x * complex(u, v) = complex(x * u, x * v)

A comparison between numbers of different types behaves as though the exact
values of those numbers were being compared. [2]_

All numeric types (except complex) support the following operations (for priorities of
the operations, see :ref:`operator-summary`):
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_floatobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ extern PyObject* _Py_string_to_number_with_underscores(

extern double _Py_parse_inf_or_nan(const char *p, char **endptr);

extern int _Py_convert_to_double(PyObject **v, double *dbl);


#ifdef __cplusplus
}
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ def test_truediv(self):
self.assertTrue(isnan(z.real))
self.assertTrue(isnan(z.imag))

self.assertComplexesAreIdentical(complex(INF, NAN) / 2,
complex(INF, NAN))

self.assertComplexesAreIdentical(complex(INF, 1)/(0.0+1j),
complex(NAN, -INF))

Expand Down Expand Up @@ -224,20 +227,32 @@ def check(n, deltas, is_equal, imag = 0.0):
def test_add(self):
self.assertEqual(1j + int(+1), complex(+1, 1))
self.assertEqual(1j + int(-1), complex(-1, 1))
self.assertComplexesAreIdentical(complex(-0.0, -0.0) + (-0.0),
complex(-0.0, -0.0))
self.assertComplexesAreIdentical((-0.0) + complex(-0.0, -0.0),
complex(-0.0, -0.0))
self.assertRaises(OverflowError, operator.add, 1j, 10**1000)
self.assertRaises(TypeError, operator.add, 1j, None)
self.assertRaises(TypeError, operator.add, None, 1j)

def test_sub(self):
self.assertEqual(1j - int(+1), complex(-1, 1))
self.assertEqual(1j - int(-1), complex(1, 1))
self.assertComplexesAreIdentical(complex(-0.0, -0.0) - 0.0,
complex(-0.0, -0.0))
self.assertComplexesAreIdentical(-0.0 - complex(0.0, 0.0),
complex(-0.0, -0.0))
self.assertRaises(OverflowError, operator.sub, 1j, 10**1000)
self.assertRaises(TypeError, operator.sub, 1j, None)
self.assertRaises(TypeError, operator.sub, None, 1j)

def test_mul(self):
self.assertEqual(1j * int(20), complex(0, 20))
self.assertEqual(1j * int(-1), complex(0, -1))
self.assertComplexesAreIdentical(complex(INF, NAN) * 2,
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
complex(INF, NAN))
self.assertComplexesAreIdentical(2 * complex(INF, NAN),
complex(INF, NAN))
self.assertRaises(OverflowError, operator.mul, 1j, 10**1000)
self.assertRaises(TypeError, operator.mul, 1j, None)
self.assertRaises(TypeError, operator.mul, None, 1j)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Implement mixed-mode arithmetic rules combining real and complex variables
as specified by C standards since C99. Patch by Sergey B Kirpichev.
167 changes: 128 additions & 39 deletions Objects/complexobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_to_double()
#include "pycore_long.h" // _PyLong_GetZero()
#include "pycore_object.h" // _PyObject_Init()
#include "pycore_pymath.h" // _Py_ADJUST_ERANGE2()
Expand Down Expand Up @@ -481,75 +482,163 @@ complex_hash(PyComplexObject *v)
return (obj)

static int
to_complex(PyObject **pobj, Py_complex *pc)
to_float(PyObject **pobj, double *dbl)
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
{
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);
}
else if (_Py_convert_to_double(pobj, dbl) < 0) {
return -1;
}
*pobj = Py_NewRef(Py_NotImplemented);
return -1;
return 0;
}

static int
to_complex(PyObject **pobj, Py_complex *pc)
{
pc->imag = 0.0;
return to_float(pobj, &(pc->real));
}

/* Complex arithmetic rules implement special mixed-mode case: combining
pure-real (float's or int's) value and complex value performed directly, not
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
by first coercing the real value to complex.

Lets consider the addition as an example, assuming that int's are implicitly
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
converted to float's. We have following rules (up to variants with changed
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
order of operands):

complex(x, y) + complex(u, v) = complex(x + u, y + v)
float(x) + complex(u, v) = complex(x + u, v)
skirpichev marked this conversation as resolved.
Show resolved Hide resolved

Similar rules are implemented for subtraction and multiplication. See C11's
Annex G, sections G.5.1 and G.5.2. The true division is special:

complex(x, y) / float(u) = complex(x/u, y/u)
float(x) / complex(u, v) = complex(x, 0) / complex(u, v)
*/

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);
if (PyComplex_Check(w)) {
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
PyObject *tmp = v;
v = w;
w = tmp;
}

Py_complex a = ((PyComplexObject *)(v))->cval;
double b;
skirpichev marked this conversation as resolved.
Show resolved Hide resolved

if (PyComplex_Check(w)) {
Py_complex b = ((PyComplexObject *)(w))->cval;
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
a = _Py_c_sum(a, b);
}
else if (to_float(&w, &b) < 0) {
return w;
}
else {
a.real += b;
}

return PyComplex_FromCComplex(a);
}

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);
Py_complex a;

if (PyComplex_Check(w)) {
Py_complex b = ((PyComplexObject *)(w))->cval;

if (PyComplex_Check(v)) {
a = ((PyComplexObject *)(v))->cval;
errno = 0;
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
a = _Py_c_diff(a, b);
}
else if (to_float(&v, &a.real) < 0) {
return v;
}
else {
a = (Py_complex) {a.real, -b.imag};
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
a.real -= b.real;
}
}
else {
a = ((PyComplexObject *)(v))->cval;
double b;

if (to_float(&w, &b) < 0) {
return w;
}
a.real -= b;
}

return PyComplex_FromCComplex(a);
}

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);
if (PyComplex_Check(w)) {
PyObject *tmp = v;
v = w;
w = tmp;
}

Py_complex a = ((PyComplexObject *)(v))->cval;
double b;

if (PyComplex_Check(w)) {
Py_complex b = ((PyComplexObject *)(w))->cval;
a = _Py_c_prod(a, b);
}
else if (to_float(&w, &b) < 0) {
return w;
}
else {
a.real *= b;
a.imag *= b;
}

return PyComplex_FromCComplex(a);
}

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);
Py_complex a;

if (PyComplex_Check(w)) {
Py_complex b = ((PyComplexObject *)(w))->cval;
TO_COMPLEX(v, a);
errno = 0;
a = _Py_c_quot(a, b);
}
else {
double b;

if (to_float(&w, &b) < 0) {
return w;
}
if (b) {
a = ((PyComplexObject *)(v))->cval;
a.real /= b;
a.imag /= b;
}
else {
errno = EDOM;
}
}

if (errno == EDOM) {
PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
return NULL;
}
return PyComplex_FromCComplex(quot);
return PyComplex_FromCComplex(a);
}

static PyObject *
Expand Down
6 changes: 3 additions & 3 deletions Objects/floatobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,13 @@ PyFloat_AsDouble(PyObject *op)
#define CONVERT_TO_DOUBLE(obj, dbl) \
if (PyFloat_Check(obj)) \
dbl = PyFloat_AS_DOUBLE(obj); \
else if (convert_to_double(&(obj), &(dbl)) < 0) \
else if (_Py_convert_to_double(&(obj), &(dbl)) < 0) \
return obj;

/* Methods */

static int
convert_to_double(PyObject **v, double *dbl)
int
_Py_convert_to_double(PyObject **v, double *dbl)
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
{
PyObject *obj = *v;

Expand Down
2 changes: 0 additions & 2 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2742,7 +2742,6 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
double value = PyLong_AsDouble(item);
if (value != -1.0 || !PyErr_Occurred()) {
re_sum = cs_add(re_sum, value);
im_sum.hi += 0.0;
Py_DECREF(item);
continue;
}
Expand All @@ -2755,7 +2754,6 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
if (PyFloat_Check(item)) {
double value = PyFloat_AS_DOUBLE(item);
re_sum = cs_add(re_sum, value);
im_sum.hi += 0.0;
_Py_DECREF_SPECIALIZED(item, _PyFloat_ExactDealloc);
continue;
}
Expand Down
Loading