diff options
| -rw-r--r-- | benchmarks/benchmarks/bench_scalar.py | 12 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/abstractdtypes.c | 3 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/array_coercion.c | 10 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/array_coercion.h | 3 | ||||
| -rw-r--r-- | numpy/core/src/umath/scalarmath.c.src | 808 |
5 files changed, 545 insertions, 291 deletions
diff --git a/benchmarks/benchmarks/bench_scalar.py b/benchmarks/benchmarks/bench_scalar.py index 219e48bed..2ad3ae670 100644 --- a/benchmarks/benchmarks/bench_scalar.py +++ b/benchmarks/benchmarks/bench_scalar.py @@ -10,6 +10,7 @@ class ScalarMath(Benchmark): param_names = ["type"] def setup(self, typename): self.num = np.dtype(typename).type(2) + self.int32 = np.int32(2) def time_addition(self, typename): n = self.num @@ -31,3 +32,14 @@ class ScalarMath(Benchmark): n = self.num res = abs(abs(abs(abs(abs(abs(abs(abs(abs(abs(n)))))))))) + def time_add_int32_other(self, typename): + # Some mixed cases are fast, some are slow, this documents these + # differences. (When writing, it was fast if the type of the result + # is one of the inputs.) + int32 = self.int32 + other = self.num + int32 + other + int32 + other + int32 + other + int32 + other + int32 + other diff --git a/numpy/core/src/multiarray/abstractdtypes.c b/numpy/core/src/multiarray/abstractdtypes.c index cc1d7fad8..b0345c46b 100644 --- a/numpy/core/src/multiarray/abstractdtypes.c +++ b/numpy/core/src/multiarray/abstractdtypes.c @@ -259,6 +259,7 @@ NPY_NO_EXPORT PyArray_DTypeMeta PyArray_PyIntAbstractDType = {{{ .tp_name = "numpy._IntegerAbstractDType", },}, .flags = NPY_DT_ABSTRACT, + .type_num = -1, .dt_slots = &pyintabstractdtype_slots, }; @@ -276,6 +277,7 @@ NPY_NO_EXPORT PyArray_DTypeMeta PyArray_PyFloatAbstractDType = {{{ .tp_name = "numpy._FloatAbstractDType", },}, .flags = NPY_DT_ABSTRACT, + .type_num = -1, .dt_slots = &pyfloatabstractdtype_slots, }; @@ -293,5 +295,6 @@ NPY_NO_EXPORT PyArray_DTypeMeta PyArray_PyComplexAbstractDType = {{{ .tp_name = "numpy._ComplexAbstractDType", },}, .flags = NPY_DT_ABSTRACT, + .type_num = -1, .dt_slots = &pycomplexabstractdtype_slots, }; diff --git a/numpy/core/src/multiarray/array_coercion.c b/numpy/core/src/multiarray/array_coercion.c index ff77d6883..562e4f008 100644 --- a/numpy/core/src/multiarray/array_coercion.c +++ b/numpy/core/src/multiarray/array_coercion.c @@ -230,6 +230,16 @@ npy_discover_dtype_from_pytype(PyTypeObject *pytype) return (PyArray_DTypeMeta *)DType; } +/* + * Note: This function never fails, but will return `NULL` for unknown scalars + * and `None` for known array-likes (e.g. tuple, list, ndarray). + */ +NPY_NO_EXPORT PyObject * +PyArray_DiscoverDTypeFromScalarType(PyTypeObject *pytype) +{ + return (PyObject *)npy_discover_dtype_from_pytype(pytype); +} + /** * Find the correct DType class for the given python type. If flags is NULL diff --git a/numpy/core/src/multiarray/array_coercion.h b/numpy/core/src/multiarray/array_coercion.h index f2482cecc..63d543cf7 100644 --- a/numpy/core/src/multiarray/array_coercion.h +++ b/numpy/core/src/multiarray/array_coercion.h @@ -19,6 +19,9 @@ NPY_NO_EXPORT int _PyArray_MapPyTypeToDType( PyArray_DTypeMeta *DType, PyTypeObject *pytype, npy_bool userdef); +NPY_NO_EXPORT PyObject * +PyArray_DiscoverDTypeFromScalarType(PyTypeObject *pytype); + NPY_NO_EXPORT int PyArray_Pack(PyArray_Descr *descr, char *item, PyObject *value); diff --git a/numpy/core/src/umath/scalarmath.c.src b/numpy/core/src/umath/scalarmath.c.src index 402e6b561..c7e845914 100644 --- a/numpy/core/src/umath/scalarmath.c.src +++ b/numpy/core/src/umath/scalarmath.c.src @@ -26,6 +26,10 @@ #include "binop_override.h" #include "npy_longdouble.h" +#include "array_coercion.h" +#include "common.h" +#include "can_cast_table.h" + /* Basic operations: * * BINARY: @@ -565,189 +569,372 @@ static void /*** END OF BASIC CODE **/ -/* The general strategy for commutative binary operators is to +/* + * How binary operators work + * ------------------------- + * + * All binary (numeric) operators use the larger of the two types, with the + * exception of unsigned int and signed int mixed cases which must promote + * to a larger type. + * + * The strategy employed for all binary operation is that we coerce the other + * scalar if it is safe to do. E.g. `float64 + float32` the `float64` can + * convert `float32` and do the operation as `float64 + float64`. + * OTOH, for `float32 + float64` it is safe, and we should defer to `float64`. + * + * So we have multiple possible paths: + * - The other scalar is a subclass. In principle *both* inputs could be + * different subclasses. In this case we should defer, but only if the + * other subclass does *not* inherit the slot as well. + * That check is included via `BINOP_IS_FORWARD` (or manually for comparison). + * - The other scalar can be converted: All is good, we do the operation + * - The other scalar cannot be converted, there are two possibilities: + * - The reverse should work, so we return NotImplemented to defer. + * (If self is a subclass, this will end up in the "unknown" path.) + * - Neither works (e.g. `uint8 + int8`): We currently use the array path. + * - The other object is a unknown. It could be either a scalar, an array, + * or an array-like (including a list!). Because NumPy scalars pretend to be + * arrays we fall into the array fallback path here _normally_ (through + * the generic scalar path). + * First we check if we should defer, though. + * + * The last possibility is awkward and leads to very confusing situations. + * The problem is that usually we should defer (return NotImplemented) + * in that path. + * If the other object is a NumPy array (or array-like) it will know what to + * do. If NumPy knows that it is a scalar (not generic `object`), then it + * would make sense to try and use the "array path" (i.e. deal with it + * using the ufunc machinery). + * + * But this overlooks two things that currently work: + * + * 1. `np.float64(3) * [1, 2, 3]` happily returns an array result. + * 2. `np.int32(3) * decimal.Decimal(3)` works! (see below) + * + * The first must work, because scalars pretend to be arrays. Which means + * they inherit the greedy "convert the other object to an array" logic. + * This may be a questionable choice, but is fine. + * (As of now, it is not negotiable, since NumPy often converts 0-D arrays + * to scalars.) + * + * The second one is more confusing. This works also by using the ufunc + * machinery (array path), but it works because: + * + * np.add(np.int32(3), decimal.Decimal(3)) + * + * Will convert the `int32` to an int32 array, and the decimal to an object + * array. It then *casts* the `int32` array to an object array. + * The casting step CONVERTS the integer to a Python integer. The ufunc object + * loop will then call back into Python scalar logic. + * + * The above would be recursive, if it was not for the conversion of the int32 + * to a Python integer! + * This leads us to the EXCEEDINGLY IMPORTANT special case: + * + * WARNING: longdouble and clongdouble do NOT convert to a Python scalar + * when cast to object. Thus they MUST NEVER take the array-path. + * However, they STILL should defer at least for + * `np.longdouble(3) + array`. + * + * + * As a general note, in the above we defer exactly when we know that deferring + * will work. `longdouble` uses the "simple" logic of generally deferring + * though, because it would otherwise easily run into an infinite recursion. * - * 1) Convert the types to the common type if both are scalars (0 return) - * 2) If both are not scalars use ufunc machinery (-2 return) - * 3) If both are scalars but cannot be cast to the right type - * return NotImplemented (-1 return) * - * 4) Perform the function on the C-type. - * 5) If an error condition occurred, check to see - * what the current error-handling is and handle the error. + * The future?! + * ------------ * - * 6) Construct and return the output scalar. + * This is very tricky and it would be nice to formalize away that "recursive" + * path we currently use. I (seberg) have currently no great idea on this, + * this is more brainstorming! + * + * If both are scalars (known to NumPy), they have a DType and we may be able + * to do the ufunc promotion to make sure there is no risk of recursion. + * + * In principle always deferring would probably be clean. But we likely cannot + * do that? There is also an issue that it is nice that we allow adding a + * DType for an existing Python scalar (which will not know about NumPy + * scalars). + * The DType/ufunc machinery teaches NumPy how arrays will work with that + * Python scalar, but the DType may need to help us decide whether we should + * defer (return NotImplemented) or try using the ufunc machinery (or a + * simplified ufunc-like machinery limited to scalars). */ + +/* + * Enum used to describe the space of possibilities when converting the second + * argument to a binary operation. + */ +typedef enum { + /* An error occurred (should not really happen/be possible) */ + CONVERSION_ERROR = -1, + /* A known NumPy scalar, but of higher precision: we defer */ + DEFER_TO_OTHER_KNOWN_SCALAR, + /* Conversion was successful (known scalar of less precision) */ + CONVERSION_SUCCESS, + /* + * Other object is an unkown scalar or array-like, we (typically) use + * the generic path, which normally ends up in the ufunc machinery. + */ + OTHER_IS_UNKNOWN_OBJECT, + /* + * Promotion necessary + */ + PROMOTION_REQUIRED, + /* + * The other object may be a subclass, conversion is successful, but we + * may still need to defer. + */ + OTHER_IS_SUBCLASS, +} conversion_result; + /**begin repeat * #name = byte, ubyte, short, ushort, int, uint, * long, ulong, longlong, ulonglong, - * half, float, longdouble, + * half, float, double, longdouble, * cfloat, cdouble, clongdouble# - * #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint, - * npy_long, npy_ulong, npy_longlong, npy_ulonglong, - * npy_half, npy_float, npy_longdouble, - * npy_cfloat, npy_cdouble, npy_clongdouble# * #Name = Byte, UByte, Short, UShort, Int, UInt, * Long, ULong, LongLong, ULongLong, - * Half, Float, LongDouble, + * Half, Float, Double, LongDouble, * CFloat, CDouble, CLongDouble# - * #TYPE = NPY_BYTE, NPY_UBYTE, NPY_SHORT, NPY_USHORT, NPY_INT, NPY_UINT, - * NPY_LONG, NPY_ULONG, NPY_LONGLONG, NPY_ULONGLONG, - * NPY_HALF, NPY_FLOAT, NPY_LONGDOUBLE, - * NPY_CFLOAT, NPY_CDOUBLE, NPY_CLONGDOUBLE# + * #TYPE = BYTE, UBYTE, SHORT, USHORT, INT, UINT, + * LONG, ULONG, LONGLONG, ULONGLONG, + * HALF, FLOAT, DOUBLE, LONGDOUBLE, + * CFLOAT, CDOUBLE, CLONGDOUBLE# + * #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint, + * npy_long, npy_ulong, npy_longlong, npy_ulonglong, + * npy_half, npy_float, npy_double, npy_longdouble, + * npy_cfloat, npy_cdouble, npy_clongdouble# */ -static int -_@name@_convert_to_ctype(PyObject *a, @type@ *arg1) -{ - PyObject *temp; +#define IS_@TYPE@ 1 - if (PyArray_IsScalar(a, @Name@)) { - *arg1 = PyArrayScalar_VAL(a, @Name@); - return 0; - } - else if (PyArray_IsScalar(a, Generic)) { - PyArray_Descr *descr1; - - if (!PyArray_IsScalar(a, Number)) { - return -1; - } - descr1 = PyArray_DescrFromTypeObject((PyObject *)Py_TYPE(a)); - if (PyArray_CanCastSafely(descr1->type_num, @TYPE@)) { - PyArray_CastScalarDirect(a, descr1, arg1, @TYPE@); - Py_DECREF(descr1); - return 0; - } - else { - Py_DECREF(descr1); - return -1; - } - } - else if (PyArray_GetPriority(a, NPY_PRIORITY) > NPY_PRIORITY) { - return -2; - } - else if ((temp = PyArray_ScalarFromObject(a)) != NULL) { - int retval = _@name@_convert_to_ctype(temp, arg1); +#define IS_SAFE(FROM, TO) _npy_can_cast_safely_table[FROM][TO] - Py_DECREF(temp); - return retval; - } - return -2; -} +/* + * TODO: This whole thing is awkward, and we should create a helper header to + * define inline functions that convert single elements for all numeric + * types. That could then also be used to define all cast loops. + * (Even if that may get more complex for SIMD at some point.) + * For now, half casts could be optimized because of that. + */ -/**end repeat**/ +#if defined(IS_HALF) + #define CONVERT_TO_RESULT(value) \ + *result = npy_float_to_half((float)(value)) +#elif defined(IS_CFLOAT) || defined(IS_CDOUBLE) || defined(IS_CLONGDOUBLE) + #define CONVERT_TO_RESULT(value) \ + result->real = value; \ + result->imag = 0 +#else + #define CONVERT_TO_RESULT(value) *result = value +#endif -/* Same as above but added exact checks against known python types for speed */ +#define GET_VALUE_OR_DEFER(OTHER, Other, value) \ + case NPY_##OTHER: \ + if (IS_SAFE(NPY_##OTHER, NPY_@TYPE@)) { \ + assert(Py_TYPE(value) == &Py##Other##ArrType_Type); \ + CONVERT_TO_RESULT(PyArrayScalar_VAL(value, Other)); \ + ret = CONVERSION_SUCCESS; \ + } \ + else if (IS_SAFE(NPY_@TYPE@, NPY_##OTHER)) { \ + /* + * If self can cast safely to other, this is clear: + * we should definitely defer. + */ \ + ret = DEFER_TO_OTHER_KNOWN_SCALAR; \ + } \ + else { \ + /* Otherwise, we must promote */ \ + ret = PROMOTION_REQUIRED; \ + } \ + break; -/**begin repeat - * #name = double# - * #type = npy_double# - * #Name = Double# - * #TYPE = NPY_DOUBLE# - * #PYCHECKEXACT = PyFloat_CheckExact# - * #PYEXTRACTCTYPE = PyFloat_AS_DOUBLE# +/* + * Complex to complex (and rejecting complex to real) is a bit different: */ -static int -_@name@_convert_to_ctype(PyObject *a, @type@ *arg1) -{ - PyObject *temp; - - if (@PYCHECKEXACT@(a)){ - *arg1 = @PYEXTRACTCTYPE@(a); - return 0; - } - - if (PyArray_IsScalar(a, @Name@)) { - *arg1 = PyArrayScalar_VAL(a, @Name@); - return 0; - } - else if (PyArray_IsScalar(a, Generic)) { - PyArray_Descr *descr1; +#if defined(IS_CFLOAT) || defined(IS_CDOUBLE) || defined(IS_CLONGDOUBLE) + +#define GET_CVALUE_OR_DEFER(OTHER, Other, value) \ + case NPY_##OTHER: \ + if (IS_SAFE(NPY_##OTHER, NPY_@TYPE@)) { \ + assert(Py_TYPE(value) == &Py##Other##ArrType_Type); \ + result->real = PyArrayScalar_VAL(value, Other).real; \ + result->imag = PyArrayScalar_VAL(value, Other).imag; \ + ret = 1; \ + } \ + else if (IS_SAFE(NPY_@TYPE@, NPY_##OTHER)) { \ + ret = DEFER_TO_OTHER_KNOWN_SCALAR; \ + } \ + else { \ + ret = PROMOTION_REQUIRED; \ + } \ + break; - if (!PyArray_IsScalar(a, Number)) { - return -1; - } - descr1 = PyArray_DescrFromTypeObject((PyObject *)Py_TYPE(a)); - if (PyArray_CanCastSafely(descr1->type_num, @TYPE@)) { - PyArray_CastScalarDirect(a, descr1, arg1, @TYPE@); - Py_DECREF(descr1); - return 0; - } - else { - Py_DECREF(descr1); - return -1; - } - } - else if (PyArray_GetPriority(a, NPY_PRIORITY) > NPY_PRIORITY) { - return -2; - } - else if ((temp = PyArray_ScalarFromObject(a)) != NULL) { - int retval = _@name@_convert_to_ctype(temp, arg1); +#else - Py_DECREF(temp); - return retval; - } - return -2; -} +/* Getting a complex value to real is never safe: */ +#define GET_CVALUE_OR_DEFER(OTHER, Other, value) \ + case NPY_##OTHER: \ + if (IS_SAFE(NPY_@TYPE@, NPY_##OTHER)) { \ + ret = DEFER_TO_OTHER_KNOWN_SCALAR; \ + } \ + else { \ + ret = PROMOTION_REQUIRED; \ + } \ + break; -/**end repeat**/ +#endif -/**begin repeat - * #name = byte, ubyte, short, ushort, int, uint, - * long, ulong, longlong, ulonglong, - * half, float, double, cfloat, cdouble# - * #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint, - * npy_long, npy_ulong, npy_longlong, npy_ulonglong, - * npy_half, npy_float, npy_double, npy_cfloat, npy_cdouble# +/** + * Convert the value to the own type and and store the result. + * + * @param value The value to convert (if compatible) + * @param result The result value (output) + * @result The result value indicating what we did with `value` or what type + * of object it is (see `conversion_result`). */ -static int -_@name@_convert2_to_ctypes(PyObject *a, @type@ *arg1, - PyObject *b, @type@ *arg2) +static NPY_INLINE conversion_result +convert_to_@name@(PyObject *value, @type@ *result) { - int ret; - ret = _@name@_convert_to_ctype(a, arg1); - if (ret < 0) { - return ret; + /* Optimize the identical scalar specifically. */ + if (PyArray_IsScalar(value, @Name@)) { + *result = PyArrayScalar_VAL(value, @Name@); + return OTHER_IS_SUBCLASS; } - ret = _@name@_convert_to_ctype(b, arg2); - if (ret < 0) { - return ret; + + /* + * Then we check for the basic Python types float, int, and complex. + * (this is a bit tedious to do right for complex). + */ + if (PyBool_Check(value)) { + CONVERT_TO_RESULT(value == Py_True); + return CONVERSION_SUCCESS; } - return 0; -} -/**end repeat**/ -/**begin repeat - * #name = longdouble, clongdouble# - * #type = npy_longdouble, npy_clongdouble# - */ + if (IS_SAFE(NPY_DOUBLE, NPY_@TYPE@) && PyFloat_CheckExact(value)) { + CONVERT_TO_RESULT(PyFloat_AS_DOUBLE(value)); + return CONVERSION_SUCCESS; + } -static int -_@name@_convert2_to_ctypes(PyObject *a, @type@ *arg1, - PyObject *b, @type@ *arg2) -{ - int ret; - ret = _@name@_convert_to_ctype(a, arg1); - if (ret == -2) { - ret = -3; + if (IS_SAFE(NPY_LONG, NPY_@TYPE@) && PyLong_CheckExact(value)) { + int overflow; + long val = PyLong_AsLongAndOverflow(value, &overflow); + if (overflow) { + return OTHER_IS_UNKNOWN_OBJECT; /* handle as if arbitrary object */ + } + if (error_converting(val)) { + return CONVERSION_ERROR; /* should not be possible */ + } + CONVERT_TO_RESULT(val); + return CONVERSION_SUCCESS; } - if (ret < 0) { - return ret; + +#if defined(IS_CFLOAT) || defined(IS_CDOUBLE) || defined(IS_CLONGDOUBLE) + if (IS_SAFE(NPY_CDOUBLE, NPY_@TYPE@) && PyComplex_CheckExact(value)) { + Py_complex val = PyComplex_AsCComplex(value); + if (error_converting(val.real)) { + return -1; /* should not be possible */ + } + result->real = val.real; + result->imag = val.imag; + return 1; } - ret = _@name@_convert_to_ctype(b, arg2); - if (ret == -2) { - ret = -3; +#endif /* defined(IS_CFLOAT) || ... */ + + PyObject *dtype = PyArray_DiscoverDTypeFromScalarType(Py_TYPE(value)); + if (dtype == Py_None) { + Py_DECREF(dtype); + /* Signal that this is an array or array-like: Defer to array logic */ + return OTHER_IS_UNKNOWN_OBJECT; } - if (ret < 0) { - return ret; + else if (dtype == NULL) { + /* + * The input is an unknown python object. This should probably defer + * but only does so for float128. + * For all other cases, we defer to the array logic. If the object + * is indeed not an array-like, this will end up converting the NumPy + * scalar to a Python scalar and then try again. + * The logic is that the ufunc casts the input to object, which does + * the conversion. + */ + return OTHER_IS_UNKNOWN_OBJECT; } - return 0; + /* + * Otherwise, we have a clear NumPy scalar, find if it is a compatible + * builtin scalar. + * Each `GET_VALUE_OR_DEFER` represents a case clause for its type number, + * extracting the value if it is safe and otherwise deferring. + * (Safety is known at compile time, so the switch statement should be + * simplified by the compiler accordingly.) + * If we have a scalar that is not listed or not safe, we defer to it. + * + * We should probably defer more aggressively, but that is too big a change, + * since it would disable `np.float64(1.) * [1, 2, 3, 4]`. + */ + int ret; /* set by the GET_VALUE_OR_DEFER macro */ + switch (((PyArray_DTypeMeta *)dtype)->type_num) { + GET_VALUE_OR_DEFER(BOOL, Bool, value); + /* UInts */ + GET_VALUE_OR_DEFER(UBYTE, UByte, value); + GET_VALUE_OR_DEFER(USHORT, UShort, value); + GET_VALUE_OR_DEFER(UINT, UInt, value); + GET_VALUE_OR_DEFER(ULONG, ULong, value); + GET_VALUE_OR_DEFER(ULONGLONG, ULongLong, value); + /* Ints */ + GET_VALUE_OR_DEFER(BYTE, Byte, value); + GET_VALUE_OR_DEFER(SHORT, Short, value); + GET_VALUE_OR_DEFER(INT, Int, value); + GET_VALUE_OR_DEFER(LONG, Long, value); + GET_VALUE_OR_DEFER(LONGLONG, LongLong, value); + /* Floats */ + case NPY_HALF: + if (IS_SAFE(NPY_HALF, NPY_@TYPE@)) { + assert(Py_TYPE(value) == &PyHalfArrType_Type); + CONVERT_TO_RESULT(npy_half_to_float(PyArrayScalar_VAL(value, Half))); + ret = 1; + } + else if (IS_SAFE(NPY_@TYPE@, NPY_HALF)) { + ret = DEFER_TO_OTHER_KNOWN_SCALAR; + } + else { + ret = PROMOTION_REQUIRED; + } + break; + GET_VALUE_OR_DEFER(FLOAT, Float, value); + GET_VALUE_OR_DEFER(DOUBLE, Double, value); + GET_VALUE_OR_DEFER(LONGDOUBLE, LongDouble, value); + /* Complex: We should still defer, but the code won't work... */ + GET_CVALUE_OR_DEFER(CFLOAT, CFloat, value); + GET_CVALUE_OR_DEFER(CDOUBLE, CDouble, value); + GET_CVALUE_OR_DEFER(CLONGDOUBLE, CLongDouble, value); + default: + /* + * If there is no match, this is an unknown scalar object. It + * would make sense to defer generously here, but it should also + * always be safe to use the array path. + * The issue is, that the other scalar may or may not be designed + * to deal with NumPy scalars. Without knowing that, we cannot + * defer (which would be much faster potentially). + * TODO: We could add a DType flag to allow opting in to deferring! + */ + ret = OTHER_IS_UNKNOWN_OBJECT; + } + Py_DECREF(dtype); + return ret; } +#undef IS_SAFE +#undef CONVERT_TO_RESULT +#undef GET_VALUE_OR_DEFER +#undef GET_CVALUE_OR_DEFER +#undef IS_@TYPE@ + /**end repeat**/ @@ -794,14 +981,79 @@ _@name@_convert2_to_ctypes(PyObject *a, @type@ *arg1, * CFloat, CDouble, CLongDouble)*5, * (Half, Float, Double, LongDouble)*2# */ +#define IS_@name@ static PyObject * @name@_@oper@(PyObject *a, PyObject *b) { PyObject *ret; - @type@ arg1, arg2; - @otype@ out; + @type@ arg1, arg2, other_val; + + /* + * Check if this operation may be considered forward. Note `is_forward` + * does not imply that we can defer to a subclass `b`, we need to check + * `BINOP_IS_FORWARD` for that (it takes into account that both may be + * identicalclass). + */ + int is_forward = (Py_TYPE(a)->tp_as_number != NULL + && (void *)(Py_TYPE(a)->tp_as_number->nb_@oper@) == (void*)(@name@_@oper@)); + /* + * Extract the other value (if it is compatible). Otherwise, decide + * how to deal with it. This is somewhat complicated. + * + * Note: This pattern is used multiple times below. + */ + PyObject *other = is_forward ? b : a; + + conversion_result res = convert_to_@name@(other, &other_val); + switch (res) { + case CONVERSION_ERROR: + return NULL; /* an error occurred (should never happen) */ + case DEFER_TO_OTHER_KNOWN_SCALAR: + /* defer to other; This cannot be an `__rop__`, assert that: */ + assert(is_forward); + Py_RETURN_NOTIMPLEMENTED; + case CONVERSION_SUCCESS: + break; /* successfully extracted value we can proceed */ + case OTHER_IS_UNKNOWN_OBJECT: +#if defined(IS_longdouble) || defined(IS_clongdouble) + Py_RETURN_NOTIMPLEMENTED; +#endif + BINOP_GIVE_UP_IF_NEEDED(a, b, nb_@oper@, @name@_@oper@); + case PROMOTION_REQUIRED: + /* + * Either an array-like, unknown scalar or we need to promote. + * + * TODO: We could special case the promotion case here for much + * better speed and to deal with integer overflow warnings + * correctly. (e.g. `uint8 * int8` cannot warn). + */ + return PyGenericArrType_Type.tp_as_number->nb_@oper@(a,b); + case OTHER_IS_SUBCLASS: + /* + * Success, but defer if not a forward operator. This implements + * the asymmetry of `__add__` and `__radd__` for subclassing. + * (A subclass replacing `__add__`/`__radd__` has precedence.) + */ + if (is_forward && BINOP_IS_FORWARD(a, b, nb_@oper@, @name@_@oper@)) { + Py_RETURN_NOTIMPLEMENTED; + } + } + + if (is_forward) { + arg1 = PyArrayScalar_VAL(a, @Name@); + arg2 = other_val; + } + else { + arg1 = other_val; + arg2 = PyArrayScalar_VAL(b, @Name@); + } + + /* + * Prepare the actual calculation: + */ + @otype@ out; #if @twoout@ @otype@ out2; PyObject *obj; @@ -812,29 +1064,6 @@ static PyObject * int first; #endif - BINOP_GIVE_UP_IF_NEEDED(a, b, nb_@oper@, @name@_@oper@); - - switch(_@name@_convert2_to_ctypes(a, &arg1, b, &arg2)) { - case 0: - break; - case -1: - /* one of them can't be cast safely must be mixed-types*/ - return PyArray_Type.tp_as_number->nb_@oper@(a,b); - case -2: - /* use default handling */ - if (PyErr_Occurred()) { - return NULL; - } - return PyGenericArrType_Type.tp_as_number->nb_@oper@(a,b); - case -3: - /* - * special case for longdouble and clongdouble - * because they have a recursive getitem in their dtype - */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - #if @fperr@ npy_clear_floatstatus_barrier((char*)&out); #endif @@ -899,6 +1128,9 @@ static PyObject * return ret; } + +#undef IS_@name@ + /**end repeat**/ #define _IS_ZERO(x) (x == 0) @@ -920,17 +1152,6 @@ static PyObject * * Half, Float, Double, LongDouble, * CFloat, CDouble, CLongDouble# * - * #oname = float*4, double*6, half, float, double, longdouble, - * cfloat, cdouble, clongdouble# - * - * #otype = npy_float*4, npy_double*6, npy_half, npy_float, - * npy_double, npy_longdouble, - * npy_cfloat, npy_cdouble, npy_clongdouble# - * - * #OName = Float*4, Double*6, Half, Float, - * Double, LongDouble, - * CFloat, CDouble, CLongDouble# - * * #isint = 1*10,0*7# * #isuint = (0,1)*5,0*7# * #cmplx = 0*14,1*3# @@ -938,43 +1159,70 @@ static PyObject * * #zero = 0*10, NPY_HALF_ZERO, 0*6# * #one = 1*10, NPY_HALF_ONE, 1*6# */ +#define IS_@name@ static PyObject * @name@_power(PyObject *a, PyObject *b, PyObject *modulo) { + if (modulo != Py_None) { + /* modular exponentiation is not implemented (gh-8804) */ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } + PyObject *ret; - @type@ arg1, arg2, out; + @type@ arg1, arg2, other_val; - BINOP_GIVE_UP_IF_NEEDED(a, b, nb_power, @name@_power); + int is_forward = (Py_TYPE(a)->tp_as_number != NULL + && (void *)(Py_TYPE(a)->tp_as_number->nb_power) == (void*)(@name@_power)); - switch(_@name@_convert2_to_ctypes(a, &arg1, b, &arg2)) { - case 0: - break; - case -1: - /* can't cast both safely mixed-types? */ - return PyArray_Type.tp_as_number->nb_power(a,b,modulo); - case -2: - /* use default handling */ - if (PyErr_Occurred()) { - return NULL; - } - return PyGenericArrType_Type.tp_as_number->nb_power(a,b,modulo); - case -3: - default: + /* + * Extract the other value (if it is compatible). See the generic + * (non power) version above for detailed notes. + */ + PyObject *other = is_forward ? b : a; + + int res = convert_to_@name@(other, &other_val); + switch (res) { + case CONVERSION_ERROR: + return NULL; /* an error occurred (should never happen) */ + case DEFER_TO_OTHER_KNOWN_SCALAR: + assert(is_forward); + Py_RETURN_NOTIMPLEMENTED; + case CONVERSION_SUCCESS: + break; /* successfully extracted value we can proceed */ + case OTHER_IS_UNKNOWN_OBJECT: +#if defined(IS_longdouble) || defined(IS_clongdouble) + Py_RETURN_NOTIMPLEMENTED; +#endif + BINOP_GIVE_UP_IF_NEEDED(a, b, nb_power, @name@_power); + case PROMOTION_REQUIRED: + return PyGenericArrType_Type.tp_as_number->nb_power(a, b, modulo); + case OTHER_IS_SUBCLASS: /* - * special case for longdouble and clongdouble - * because they have a recursive getitem in their dtype + * Success, but defer if not a forward operator. This implements + * the asymmetry of `__add__` and `__radd__` for subclassing. + * (A subclass replacing `__add__`/`__radd__` has precedence.) */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; + if (is_forward && BINOP_IS_FORWARD(a, b, nb_power, @name@_power)) { + Py_RETURN_NOTIMPLEMENTED; + } } - if (modulo != Py_None) { - /* modular exponentiation is not implemented (gh-8804) */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; + if (is_forward) { + arg1 = PyArrayScalar_VAL(a, @Name@); + arg2 = other_val; + } + else { + arg1 = other_val; + arg2 = PyArrayScalar_VAL(b, @Name@); } + /* + * Prepare the actual calculation: + */ + @type@ out; + #if !@isint@ npy_clear_floatstatus_barrier((char*)&out); #endif @@ -1021,6 +1269,7 @@ static PyObject * } +#undef IS_@name@ /**end repeat**/ #undef _IS_ZERO @@ -1058,41 +1307,10 @@ instead of a TypeError. This should ensure that once everything gets converted to complex long doubles you correctly get a reasonably informative TypeError. This fixes the last part of bug gh-18548. */ - static PyObject * -clongdouble_@oper@(PyObject *a, PyObject *b) +clongdouble_@oper@(PyObject *NPY_UNUSED(a), PyObject *NPY_UNUSED(b)) { - npy_clongdouble arg1, arg2; - - BINOP_GIVE_UP_IF_NEEDED(a, b, nb_@oper@, clongdouble_@oper@); - - switch(_clongdouble_convert2_to_ctypes(a, &arg1, b, &arg2)) { - case 0: - break; - case -1: - /* one of them can't be cast safely must be mixed-types*/ - return PyArray_Type.tp_as_number->nb_@oper@(a,b); - case -2: - /* use default handling */ - if (PyErr_Occurred()) { - return NULL; - } - return PyGenericArrType_Type.tp_as_number->nb_@oper@(a,b); - case -3: - /* - * special case for longdouble and clongdouble - * because they have a recursive getitem in their dtype - */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - /* - * here we do the actual calculation with arg1 and arg2 - * as a function call. - */ - PyErr_SetString(PyExc_TypeError, "complex long doubles do not support remainder"); - return NULL; + Py_RETURN_NOTIMPLEMENTED; } /**end repeat**/ @@ -1124,6 +1342,14 @@ clongdouble_@oper@(PyObject *a, PyObject *b) * byte, ubyte, short, ushort, int, uint, * long, ulong, longlong, ulonglong# * + * #Name = (Byte, UByte, Short, UShort, Int, UInt, + * Long, ULong, LongLong, ULongLong, + * Half, Float, Double, LongDouble, + * CFloat, CDouble, CLongDouble)*3, + * + * Byte, UByte, Short, UShort, Int, UInt, + * Long, ULong, LongLong, ULongLong# + * * #type = (npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint, * npy_long, npy_ulong, npy_longlong, npy_ulonglong, * npy_half, npy_float, npy_double, npy_longdouble, @@ -1161,32 +1387,19 @@ clongdouble_@oper@(PyObject *a, PyObject *b) static PyObject * @name@_@oper@(PyObject *a) { - @type@ arg1; + @type@ val; @otype@ out; PyObject *ret; - switch(_@name@_convert_to_ctype(a, &arg1)) { - case 0: - break; - case -1: - /* can't cast both safely use different add function */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - case -2: - /* use default handling */ - if (PyErr_Occurred()) { - return NULL; - } - return PyGenericArrType_Type.tp_as_number->nb_@oper@(a); - } + + val = PyArrayScalar_VAL(a, @Name@); + + @name@_ctype_@oper@(val, &out); /* - * here we do the actual calculation with arg1 and arg2 - * make it a function call. + * TODO: Complex absolute should check floating point flags. */ - @name@_ctype_@oper@(arg1, &out); - ret = PyArrayScalar_New(@OName@); PyArrayScalar_ASSIGN(ret, @OName@, out); @@ -1210,6 +1423,10 @@ static PyObject * * uint, long, ulong, longlong, ulonglong, * half, float, double, longdouble, * cfloat, cdouble, clongdouble# + * #Name = Byte, UByte, Short, UShort, Int, UInt, + * Long, ULong, LongLong, ULongLong, + * Half, Float, Double, LongDouble, + * CFloat, CDouble, CLongDouble# * #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, * npy_uint, npy_long, npy_ulong, npy_longlong, npy_ulonglong, * npy_half, npy_float, npy_double, npy_longdouble, @@ -1221,24 +1438,14 @@ static int @name@_bool(PyObject *a) { int ret; - @type@ arg1; - - if (_@name@_convert_to_ctype(a, &arg1) < 0) { - if (PyErr_Occurred()) { - return -1; - } - return PyGenericArrType_Type.tp_as_number->nb_bool(a); - } + @type@ val; - /* - * here we do the actual calculation with arg1 and arg2 - * make it a function call. - */ + val = PyArrayScalar_VAL(a, @Name@); #if @simp@ - ret = @nonzero@(arg1); + ret = @nonzero@(val); #else - ret = (@nonzero@(arg1.real) || @nonzero@(arg1.imag)); + ret = (@nonzero@(val.real) || @nonzero@(val.imag)); #endif return ret; @@ -1353,36 +1560,53 @@ static NPY_INLINE PyObject * * long, ulong, longlong, ulonglong, * half, float, double, longdouble, * cfloat, cdouble, clongdouble# + * #Name = Byte, UByte, Short, UShort, Int, UInt, + * Long, ULong, LongLong, ULongLong, + * Half, Float, Double, LongDouble, + * CFloat, CDouble, CLongDouble# * #simp = def*10, def_half, def*3, cmplx*3# */ +#define IS_@name@ + static PyObject* @name@_richcompare(PyObject *self, PyObject *other, int cmp_op) { npy_@name@ arg1, arg2; - int out=0; - - RICHCMP_GIVE_UP_IF_NEEDED(self, other); + int out = 0; - switch(_@name@_convert2_to_ctypes(self, &arg1, other, &arg2)) { - case 0: - break; - case -1: - /* can't cast both safely use different add function */ - case -2: - /* use ufunc */ - if (PyErr_Occurred()) { - return NULL; - } - return PyGenericArrType_Type.tp_richcompare(self, other, cmp_op); - case -3: - /* - * special case for longdouble and clongdouble - * because they have a recursive getitem in their dtype - */ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; + /* + * Extract the other value (if it is compatible). + */ + int res = convert_to_@name@(other, &arg2); + + switch (res) { + case CONVERSION_ERROR: + return NULL; /* an error occurred (should never happen) */ + case DEFER_TO_OTHER_KNOWN_SCALAR: + assert(is_forward); + Py_RETURN_NOTIMPLEMENTED; + case CONVERSION_SUCCESS: + break; /* successfully extracted value we can proceed */ + case OTHER_IS_UNKNOWN_OBJECT: +#if defined(IS_longdouble) || defined(IS_clongdouble) + Py_RETURN_NOTIMPLEMENTED; +#endif + RICHCMP_GIVE_UP_IF_NEEDED(self, other); + case PROMOTION_REQUIRED: + return PyGenericArrType_Type.tp_richcompare(self, other, cmp_op); + case OTHER_IS_SUBCLASS: + /* + * Success, but defer if not a forward operator. This implements + * the asymmetry of `__add__` and `__radd__` for subclassing. + * (A subclass replacing `__add__`/`__radd__` has precedence.) + */ + if ((void *)Py_TYPE(other)->tp_richcompare != (void *)@name@_richcompare) { + Py_RETURN_NOTIMPLEMENTED; + } } + arg1 = PyArrayScalar_VAL(self, @Name@); + /* here we do the actual calculation with arg1 and arg2 */ switch (cmp_op) { case Py_EQ: @@ -1412,6 +1636,8 @@ static PyObject* PyArrayScalar_RETURN_FALSE; } } + +#undef IS_@name@ /**end repeat**/ /**begin repeat |
