summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--benchmarks/benchmarks/bench_scalar.py12
-rw-r--r--numpy/core/src/multiarray/abstractdtypes.c3
-rw-r--r--numpy/core/src/multiarray/array_coercion.c10
-rw-r--r--numpy/core/src/multiarray/array_coercion.h3
-rw-r--r--numpy/core/src/umath/scalarmath.c.src808
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