From c9c6841fc944693593d848cb9327493c5b07475b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 01/22] ENH: add shared helpers for rejecting mixed QuadPrecision backends --- src/include/dtype.h | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/include/dtype.h b/src/include/dtype.h index 5396e1c..0f3e934 100644 --- a/src/include/dtype.h +++ b/src/include/dtype.h @@ -30,6 +30,50 @@ quad_resolve_descrs_fail(PyArray_Descr *loop_descrs[], int n) return (NPY_CASTING)-1; } +/* + * Values never move between backends until inter-backend casts are exact and + * a promotion policy is defined (gh-112, gh-113). + */ +static inline void +quad_set_backend_mismatch_error(void) +{ + PyErr_SetString(PyExc_TypeError, + "Cannot mix QuadPrecision backends 'sleef' and 'longdouble' " + "(Python int and float operands use 'sleef')"); +} + +/* + * Backend for a loop over Quad operands. NumPy hands non-Quad operands to the + * resolver as default (SLEEF) descriptors, so a longdouble operand decides; + * genuinely mixed Quad operands are then rejected by the Quad-to-Quad cast. + */ +static inline QuadBackendType +quad_loop_backend(PyArray_Descr *const descrs[], int n) +{ + for (int i = 0; i < n; i++) { + if (((QuadPrecDTypeObject *)descrs[i])->backend == BACKEND_LONGDOUBLE) { + return BACKEND_LONGDOUBLE; + } + } + return BACKEND_SLEEF; +} + +/* + * Loop descriptor for a Quad output given input descriptor `in`. An `out` of + * another backend still gets `in`'s backend, so NumPy's cast into it raises. + */ +static inline PyArray_Descr * +quad_out_loop_descr(PyArray_Descr *in, PyArray_Descr *out) +{ + PyArray_Descr *res = in; + if (out != NULL && + ((QuadPrecDTypeObject *)out)->backend == ((QuadPrecDTypeObject *)in)->backend) { + res = out; + } + Py_INCREF(res); + return res; +} + int init_quadprec_dtype(void); From d03e55f6f993254cae11ea275b58eef9585cd9ce Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 02/22] BUG: raise on casts between QuadPrecision backends instead of converting through double --- src/csrc/casts.cpp | 131 ++++----------------------------------------- 1 file changed, 10 insertions(+), 121 deletions(-) diff --git a/src/csrc/casts.cpp b/src/csrc/casts.cpp index 1dd5caf..788c2e0 100644 --- a/src/csrc/casts.cpp +++ b/src/csrc/casts.cpp @@ -69,81 +69,19 @@ quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), return NPY_NO_CASTING; } - Py_INCREF(given_descrs[1]); - loop_descrs[1] = given_descrs[1]; - if (given_descrs[0]->backend != given_descrs[1]->backend) { - // Different backends require actual conversion, no view possible - *view_offset = NPY_MIN_INTP; - if (given_descrs[0]->backend == BACKEND_SLEEF) { - // SLEEF -> long double may lose precision - return static_cast(NPY_SAME_KIND_CASTING | NPY_SAME_VALUE_CASTING_FLAG); - } - // long double -> SLEEF preserves value exactly - return static_cast(NPY_SAFE_CASTING | NPY_SAME_VALUE_CASTING_FLAG); + quad_set_backend_mismatch_error(); + Py_CLEAR(loop_descrs[0]); + return (NPY_CASTING)-1; } + Py_INCREF(given_descrs[1]); + loop_descrs[1] = given_descrs[1]; + *view_offset = 0; return static_cast(NPY_NO_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } -// Helper function for quad-to-quad same_value check (inter-backend) -// NOTE: the inter-backend uses `double` as intermediate, -// so only values that can be exactly represented in double can pass same_value check -static inline int -quad_to_quad_same_value_check(const quad_value *in_val, QuadBackendType backend_in, - const quad_value *out_val, QuadBackendType backend_out) -{ - // Convert output back to input backend for comparison - quad_value roundtrip; - - if (backend_in == BACKEND_SLEEF) { - // Input was SLEEF, output is longdouble - // Convert longdouble back to SLEEF for comparison - long double ld = out_val->longdouble_value; - if (std::isnan(ld)) { - // Preserve sign of NaN - roundtrip.sleef_value = (!ld_signbit(&ld)) ? QUAD_PRECISION_NAN : QUAD_PRECISION_NEG_NAN; - } - else if (std::isinf(ld)) { - roundtrip.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF; - } - else { - Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast(ld)); - memcpy(&roundtrip.sleef_value, &temp, sizeof(Sleef_quad)); - } - - // Compare in SLEEF domain && signbit preserved - bool is_sign_preserved = (quad_signbit(&in_val->sleef_value) == quad_signbit(&roundtrip.sleef_value)); - if(quad_isnan(&in_val->sleef_value) && quad_isnan(&roundtrip.sleef_value) && is_sign_preserved) - return 1; // Both NaN - if (Sleef_icmpeqq1(in_val->sleef_value, roundtrip.sleef_value) && is_sign_preserved) - return 1; // Equal - } - else { - // Input was longdouble, output is SLEEF - // Convert SLEEF back to longdouble for comparison - roundtrip.longdouble_value = static_cast(cast_sleef_to_double(out_val->sleef_value)); - - // Compare in longdouble domain && signbit preserved - bool is_sign_preserved = (ld_signbit(&in_val->longdouble_value) == ld_signbit(&roundtrip.longdouble_value)); - if ((std::isnan(in_val->longdouble_value) && std::isnan(roundtrip.longdouble_value)) && is_sign_preserved) - return 1; - if ((in_val->longdouble_value == roundtrip.longdouble_value) && is_sign_preserved) - return 1; - } - - // Values don't match - Sleef_quad sleef_val = quad_to_sleef_quad(in_val, backend_in); - const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); - if (val_str != NULL) { - PyErr_Format(PyExc_ValueError, - "QuadPrecision value '%s' cannot be represented exactly in target backend", - val_str); - } - return -1; -} - template static int quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], @@ -156,62 +94,13 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], npy_intp in_stride = strides[0]; npy_intp out_stride = strides[1]; - QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)context->descriptors[0]; - QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)context->descriptors[1]; - QuadBackendType backend_in = descr_in->backend; - QuadBackendType backend_out = descr_out->backend; - int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); - - // inter-backend casting - if (backend_in != backend_out) { - while (N--) { - quad_value in_val; - load_quad(in_ptr, backend_in, &in_val); - quad_value out_val; - if (backend_in == BACKEND_SLEEF) - { - out_val.longdouble_value = static_cast(cast_sleef_to_double(in_val.sleef_value)); - } - else - { - long double ld = in_val.longdouble_value; - if (std::isnan(ld)) { - out_val.sleef_value = (!ld_signbit(&ld)) ? QUAD_PRECISION_NAN : QUAD_PRECISION_NEG_NAN; - } - else if (std::isinf(ld)) { - out_val.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF; - } - else - { - // to prevent compiler optimizations, ABI handling issues with __float128 on x86-64 machines - // won't be expensive as for fixed size compiler can optimize memcpy with movq - Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast(ld)); - std::memcpy(&out_val.sleef_value, &temp, sizeof(Sleef_quad)); - } - } - - // check same_value for inter-backend casts - if(same_value_casting) - { - int ret = quad_to_quad_same_value_check(&in_val, backend_in, &out_val, backend_out); - if (ret < 0) { - return -1; - } - } - - store_quad(out_ptr, &out_val, backend_out); - in_ptr += in_stride; - out_ptr += out_stride; - } - return 0; - } + // The resolver only accepts casts within one backend. + QuadBackendType backend = ((QuadPrecDTypeObject *)context->descriptors[0])->backend; - // same backend: direct copy - // same_value casting not needed here as values are identical while(N--) { quad_value val; - load_quad(in_ptr, backend_in, &val); - store_quad(out_ptr, &val, backend_out); + load_quad(in_ptr, backend, &val); + store_quad(out_ptr, &val, backend); in_ptr += in_stride; out_ptr += out_stride; } From 65a4f5ec651d800eade661b752b202c6abb94a49 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 03/22] BUG: reject mixed backends in common_instance and scalar setitem --- src/csrc/dtype.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/csrc/dtype.c b/src/csrc/dtype.c index cfe654f..6f00742 100644 --- a/src/csrc/dtype.c +++ b/src/csrc/dtype.c @@ -55,15 +55,9 @@ ensure_canonical(QuadPrecDTypeObject *self) static QuadPrecDTypeObject * common_instance(QuadPrecDTypeObject *dtype1, QuadPrecDTypeObject *dtype2) { - // if backend mismatch then return SLEEF one (safe to cast ld to quad) if (dtype1->backend != dtype2->backend) { - if (dtype1->backend == BACKEND_SLEEF) { - Py_INCREF(dtype1); - return dtype1; - } - - Py_INCREF(dtype2); - return dtype2; + quad_set_backend_mismatch_error(); + return NULL; } Py_INCREF(dtype1); return dtype1; @@ -120,6 +114,10 @@ quadprec_setitem(QuadPrecDTypeObject *descr, PyObject *obj, char *dataptr) { QuadPrecisionObject *value; if (PyObject_TypeCheck(obj, &QuadPrecision_Type)) { + if (((QuadPrecisionObject *)obj)->backend != descr->backend) { + quad_set_backend_mismatch_error(); + return -1; + } Py_INCREF(obj); value = (QuadPrecisionObject *)obj; } From 00dfab62964c3adf72ac151cf5069b8235fb9c2f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 04/22] BUG: copy QuadPrecision values exactly and reject other backends in the constructor --- src/csrc/scalar.c | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 13f5ff8..8f8e176 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -96,7 +96,23 @@ quad_from_py_int(PyObject *py_int, QuadBackendType backend, QuadPrecisionObject QuadPrecisionObject * QuadPrecision_from_object(PyObject *value, QuadBackendType backend) -{ +{ + // QuadPrecision is a NumPy floating scalar, so it must be handled before the + // NumPy scalar path, which converts through double. + if (PyObject_TypeCheck(value, &QuadPrecision_Type)) { + QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)value; + if (quad_obj->backend != backend) { + quad_set_backend_mismatch_error(); + return NULL; + } + QuadPrecisionObject *self = QuadPrecision_raw_new(backend); + if (self == NULL) { + return NULL; + } + self->value = quad_obj->value; + return self; + } + // Handle numpy scalars (np.int32, np.float32, etc.) before arrays // We need to check this before PySequence_Check because some numpy scalars are sequences if (PyArray_CheckScalar(value)) { @@ -247,23 +263,6 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend) else if (PyLong_Check(value)) { return quad_from_py_int(value, backend, self); } - else if (Py_TYPE(value) == &QuadPrecision_Type) { - Py_DECREF(self); // discard the default one - QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)value; - // create a new one with the same backend - QuadPrecisionObject *self = QuadPrecision_raw_new(quad_obj->backend); - if (self == NULL) { - return NULL; - } - if (quad_obj->backend == BACKEND_SLEEF) { - self->value.sleef_value = quad_obj->value.sleef_value; - } - else { - self->value.longdouble_value = quad_obj->value.longdouble_value; - } - - return self; - } else { PyObject *type_str = PyObject_Str((PyObject *)Py_TYPE(value)); if (type_str != NULL) { From d6a9958da1990a561a5a1f01d4911151415e3b97 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 05/22] MAINT: use the shared mixed-backend error for scalar ops --- src/csrc/scalar_ops.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index 33cf624..d5437ce 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -19,6 +19,7 @@ extern "C" { #include "ops.hpp" #include "scalar_ops.h" #include "quad_common.h" +#include "dtype.h" #include "constants.hpp" template @@ -77,7 +78,7 @@ quad_binary_func(PyObject *op1, PyObject *op2) Py_INCREF(other); other_quad = (QuadPrecisionObject *)other; if (other_quad->backend != backend) { - PyErr_SetString(PyExc_TypeError, "Cannot mix QuadPrecision backends"); + quad_set_backend_mismatch_error(); Py_DECREF(other_quad); Py_DECREF(other); return NULL; @@ -138,11 +139,7 @@ quad_richcompare(QuadPrecisionObject *self, PyObject *other, int cmp_op) Py_INCREF(other); other_quad = (QuadPrecisionObject *)other; if (other_quad->backend != backend) { - // we could allow, but this will be bad - // Two values that are different in quad precision, - // might appear equal when converted to double. - PyErr_SetString(PyExc_TypeError, - "Cannot compare QuadPrecision objects with different backends"); + quad_set_backend_mismatch_error(); Py_DECREF(other_quad); return NULL; } From b4441dbfe7caa657ae96ed605ca65cbb1989aea6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 06/22] MAINT: pick the loop backend with the shared helper in binary resolvers --- src/csrc/umath/binary_ops.cpp | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/csrc/umath/binary_ops.cpp b/src/csrc/umath/binary_ops.cpp index f00da52..3ab8e9b 100644 --- a/src/csrc/umath/binary_ops.cpp +++ b/src/csrc/umath/binary_ops.cpp @@ -32,17 +32,9 @@ quad_binary_op_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtyp QuadPrecDTypeObject *descr_in1 = (QuadPrecDTypeObject *)given_descrs[0]; QuadPrecDTypeObject *descr_in2 = (QuadPrecDTypeObject *)given_descrs[1]; - QuadBackendType target_backend; - - // Determine target backend and if casting is needed - NPY_CASTING casting = NPY_NO_CASTING; - if (descr_in1->backend != descr_in2->backend) { - target_backend = BACKEND_LONGDOUBLE; - casting = NPY_SAFE_CASTING; - } - else { - target_backend = descr_in1->backend; - } + QuadBackendType target_backend = quad_loop_backend(given_descrs, 2); + NPY_CASTING casting = (descr_in1->backend == descr_in2->backend) ? NPY_NO_CASTING + : NPY_SAFE_CASTING; // Set up input descriptors, casting if necessary for (int i = 0; i < 2; i++) { @@ -161,17 +153,9 @@ quad_binary_op_2out_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const QuadPrecDTypeObject *descr_in1 = (QuadPrecDTypeObject *)given_descrs[0]; QuadPrecDTypeObject *descr_in2 = (QuadPrecDTypeObject *)given_descrs[1]; - QuadBackendType target_backend; - - // Determine target backend and if casting is needed - NPY_CASTING casting = NPY_NO_CASTING; - if (descr_in1->backend != descr_in2->backend) { - target_backend = BACKEND_LONGDOUBLE; - casting = NPY_SAFE_CASTING; - } - else { - target_backend = descr_in1->backend; - } + QuadBackendType target_backend = quad_loop_backend(given_descrs, 2); + NPY_CASTING casting = (descr_in1->backend == descr_in2->backend) ? NPY_NO_CASTING + : NPY_SAFE_CASTING; // Set up input descriptors, casting if necessary for (int i = 0; i < 2; i++) { From b7f3896b73ad2360eeb70f05daeb604f91022bf9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 07/22] MAINT: pick the loop backend with the shared helper in the comparison resolver --- src/csrc/umath/comparison_ops.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/csrc/umath/comparison_ops.cpp b/src/csrc/umath/comparison_ops.cpp index 2c0ad8a..d8cb559 100644 --- a/src/csrc/umath/comparison_ops.cpp +++ b/src/csrc/umath/comparison_ops.cpp @@ -36,17 +36,9 @@ quad_comparison_op_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const QuadPrecDTypeObject *descr_in1 = (QuadPrecDTypeObject *)given_descrs[0]; QuadPrecDTypeObject *descr_in2 = (QuadPrecDTypeObject *)given_descrs[1]; - QuadBackendType target_backend; - - // As dealing with different backends then cast to boolean - NPY_CASTING casting = NPY_NO_CASTING; - if (descr_in1->backend != descr_in2->backend) { - target_backend = BACKEND_LONGDOUBLE; - casting = NPY_SAFE_CASTING; - } - else { - target_backend = descr_in1->backend; - } + QuadBackendType target_backend = quad_loop_backend(given_descrs, 2); + NPY_CASTING casting = (descr_in1->backend == descr_in2->backend) ? NPY_NO_CASTING + : NPY_SAFE_CASTING; // Set up input descriptors, casting if necessary for (int i = 0; i < 2; i++) { From 90406c46848b157608b6a646ebf4c4660902e507 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 08/22] BUG: give unary outputs the input backend so mismatched out raises --- src/csrc/umath/unary_ops.cpp | 53 ++---------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/src/csrc/umath/unary_ops.cpp b/src/csrc/umath/unary_ops.cpp index a36e5dc..3872b31 100644 --- a/src/csrc/umath/unary_ops.cpp +++ b/src/csrc/umath/unary_ops.cpp @@ -29,22 +29,7 @@ quad_unary_op_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtype Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; - if (given_descrs[1] == NULL) { - Py_INCREF(given_descrs[0]); - loop_descrs[1] = given_descrs[0]; - } - else { - Py_INCREF(given_descrs[1]); - loop_descrs[1] = given_descrs[1]; - } - - QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)given_descrs[0]; - QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)loop_descrs[1]; - - if (descr_in->backend != descr_out->backend) { - return NPY_UNSAFE_CASTING; - } - + loop_descrs[1] = quad_out_loop_descr(given_descrs[0], given_descrs[1]); return NPY_NO_CASTING; } @@ -288,26 +273,9 @@ quad_unary_op_2out_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; - // Output descriptors (2 outputs) for (int i = 1; i < 3; i++) { - if (given_descrs[i] == NULL) { - Py_INCREF(given_descrs[0]); - loop_descrs[i] = given_descrs[0]; - } - else { - Py_INCREF(given_descrs[i]); - loop_descrs[i] = given_descrs[i]; - } + loop_descrs[i] = quad_out_loop_descr(given_descrs[0], given_descrs[i]); } - - QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)given_descrs[0]; - QuadPrecDTypeObject *descr_out1 = (QuadPrecDTypeObject *)loop_descrs[1]; - QuadPrecDTypeObject *descr_out2 = (QuadPrecDTypeObject *)loop_descrs[2]; - - if (descr_in->backend != descr_out1->backend || descr_in->backend != descr_out2->backend) { - return NPY_UNSAFE_CASTING; - } - return NPY_NO_CASTING; } @@ -442,14 +410,7 @@ quad_frexp_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtypes[] loop_descrs[0] = given_descrs[0]; // Output 1: mantissa (same dtype as input) - if (given_descrs[1] == NULL) { - Py_INCREF(given_descrs[0]); - loop_descrs[1] = given_descrs[0]; - } - else { - Py_INCREF(given_descrs[1]); - loop_descrs[1] = given_descrs[1]; - } + loop_descrs[1] = quad_out_loop_descr(given_descrs[0], given_descrs[1]); // Output 2: exponent (int32) if (given_descrs[2] == NULL) { @@ -462,14 +423,6 @@ quad_frexp_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtypes[] Py_INCREF(given_descrs[2]); loop_descrs[2] = given_descrs[2]; } - - QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)given_descrs[0]; - QuadPrecDTypeObject *descr_out1 = (QuadPrecDTypeObject *)loop_descrs[1]; - - if (descr_in->backend != descr_out1->backend) { - return NPY_UNSAFE_CASTING; - } - return NPY_NO_CASTING; } From 6f833a61a44e46c4be240aa543d31e61fb2ad04a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 09/22] TST: use a same-backend source in the cast padding test --- tests/test_longdouble_padding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_longdouble_padding.py b/tests/test_longdouble_padding.py index 176e707..665262f 100644 --- a/tests/test_longdouble_padding.py +++ b/tests/test_longdouble_padding.py @@ -132,7 +132,7 @@ def test_frexp_zeroes_longdouble_padding(aligned): @pytest.mark.parametrize("aligned", [True, False]) -@pytest.mark.parametrize("source_backend", [None, "sleef"]) +@pytest.mark.parametrize("source_backend", [None, "longdouble"]) def test_cast_zeroes_longdouble_padding(aligned, source_backend): dtype = x87_longdouble_dtype() if source_backend is None: From 97991c84002e09e055f9ab5c62a78c7775ed2f0e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 10/22] TST: drop cross-backend cast expectations from quad-to-quad tests --- tests/test_quaddtype.py | 73 +++++++---------------------------------- 1 file changed, 11 insertions(+), 62 deletions(-) diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 254f77b..ad3d433 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -6079,35 +6079,24 @@ def test_hash_backends(self, backend): quad_val = QuadPrecision(1.5, backend=backend) assert hash(quad_val) == hash(1.5) -@pytest.mark.parametrize("src_backend,dst_backend", [ - ("sleef", "longdouble"), - ("longdouble", "sleef"), - ("sleef", "sleef"), - ("longdouble", "longdouble"), -]) +@pytest.mark.parametrize("backend", ["sleef", "longdouble"]) @pytest.mark.parametrize("value", [ "0.0", "-0.0", "1.0", "-1.0", "3.14159265358979323846", "inf", "-inf", "nan", "1e100", "1e-100", "-nan" ]) -def test_quad_to_quad_backend_casting(src_backend, dst_backend, value): - """Test casting between QuadPrecDType with different backends.""" +def test_quad_to_quad_backend_casting(backend, value): + """Casting within a backend is exact; across backends see test_backend_mixing.py.""" - src_arr = np.array([value], dtype=QuadPrecDType(backend=src_backend)) - dst_arr = src_arr.astype(QuadPrecDType(backend=dst_backend)) - res_arr = np.array([value], dtype=QuadPrecDType(backend=dst_backend)) - - expected_backend = 0 if dst_backend == 'sleef' else 1 - assert dst_arr.dtype.backend == expected_backend - + src_arr = np.array([value], dtype=QuadPrecDType(backend=backend)) + dst_arr = src_arr.astype(QuadPrecDType(backend=backend)) + res_arr = np.array([value], dtype=QuadPrecDType(backend=backend)) + + assert dst_arr.dtype == QuadPrecDType(backend=backend) assert np.signbit(src_arr[0]) == np.signbit(dst_arr[0]) if np.isnan(src_arr[0]): - assert np.isnan(dst_arr[0]) - elif np.isinf(src_arr[0]): - assert np.isinf(dst_arr[0]) - elif src_backend != dst_backend: - np.testing.assert_allclose(dst_arr, res_arr, rtol=1e-15) + assert np.isnan(dst_arr[0]) else: - np.testing.assert_array_equal(dst_arr, res_arr) + np.testing.assert_array_equal(dst_arr, res_arr) class TestSameValueCasting: """Test 'same_value' casting behavior for QuadPrecision.""" @@ -6292,15 +6281,11 @@ def test_same_value_cast_strings_narrow_width(self, dtype): q.astype(dtype, casting="same_value") @pytest.mark.parametrize("src_backend,dst_backend", [ - ("sleef", "longdouble"), - ("longdouble", "sleef"), ("sleef", "sleef"), ("longdouble", "longdouble") ]) def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend): - """Test values that should roundtrip exactly between backends.""" - # Values exactly representable in both backends (and in double, since - # inter-backend conversion goes through double) + """Test values that roundtrip exactly within a backend.""" passing_values = [ 0.0, -0.0, 1.0, -1.0, 0.5, 0.25, 0.125, @@ -6323,42 +6308,6 @@ def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend) assert float(result[0]) == float(src[0]), f"Value {val} failed for {src_backend} -> {dst_backend}" - @pytest.mark.parametrize("src_backend,dst_backend", [ - ("sleef", "longdouble"), - ("longdouble", "sleef"), - ]) - def test_quad_to_quad_interbackend_same_value_casting_failing(self, src_backend, dst_backend): - """Test values that cannot roundtrip exactly between backends. - - Inter-backend conversion goes through double, so values exceeding - double's precision (~53 bits mantissa) will fail same_value casting. - """ - ld_info = np.finfo(np.longdouble) - double_info = np.finfo(np.float64) - - # Skip if longdouble has same precision as quad (PowerPC binary128) - # In that case, sleef <-> longdouble might use a direct path - if ld_info.nmant >= 112: - pytest.skip("longdouble has same precision as quad on this platform") - - # For longdouble -> sleef: only fails if longdouble has more precision than double - if src_backend == "longdouble" and ld_info.nmant <= double_info.nmant: - pytest.skip("longdouble has same or less precision than double on this platform") - - # Values that exceed double precision (53-bit mantissa) - # These will lose precision when going through the double conversion - failing_values = [ - str(2**53 + 1), # First integer not exactly representable in double - "3.141592653589793238462643383279502884197", # Pi with more than double precision - "1.00000000000000011", # 1 + epsilon beyond double precision - ] - - for val in failing_values: - src = np.array([val], dtype=QuadPrecDType(backend=src_backend)) - with pytest.raises(ValueError): - src.astype(QuadPrecDType(backend=dst_backend), casting="same_value") - - @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) def test_quad_to_quad_same_backend_always_passes(self, backend): """Same backend conversion should always pass same_value.""" From 0deb01ba8e8069eeb4646d0abe4c951ad815e39e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 11/22] TST: cover rejection of mixed QuadPrecision backends --- tests/test_backend_mixing.py | 277 +++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 tests/test_backend_mixing.py diff --git a/tests/test_backend_mixing.py b/tests/test_backend_mixing.py new file mode 100644 index 0000000..bc85001 --- /dev/null +++ b/tests/test_backend_mixing.py @@ -0,0 +1,277 @@ +"""Values never move between the SLEEF and longdouble backends (gh-112, gh-113). + +NumPy's own dtypes, including ``np.longdouble``, are not backends and keep +working with both. +""" +import numpy as np +import pytest + +from numpy_quaddtype import QuadPrecDType, QuadPrecision + +MIXED = "Cannot mix QuadPrecision backends" +PAIRS = [("sleef", "longdouble"), ("longdouble", "sleef")] +BACKENDS = ["sleef", "longdouble"] + + +def _arr(values, backend): + return np.array(values, dtype=QuadPrecDType(backend=backend)) + + +def _raises(): + return pytest.raises(TypeError, match=MIXED) + + +def _cast_refused(): + # Under a casting rule NumPy checks can_cast first and raises its own error. + return pytest.raises(TypeError, match=f"{MIXED}|Cannot cast array data") + + +@pytest.mark.parametrize("src,dst", PAIRS) +class TestArraysOfDifferentBackends: + @pytest.mark.parametrize("op", [ + np.add, np.subtract, np.multiply, np.true_divide, np.power, np.maximum, + np.equal, np.less, np.logical_and, np.divmod, + ]) + def test_binary_ufunc(self, src, dst, op): + with _raises(): + op(_arr([1, 2], src), _arr([3, 4], dst)) + + @pytest.mark.parametrize("casting", ["same_kind", "unsafe"]) + def test_binary_out(self, src, dst, casting): + out = _arr([7, 7], dst) + with _raises(): + np.add(_arr([1, 2], src), _arr([3, 4], src), out=out, casting=casting) + np.testing.assert_array_equal(out, _arr([7, 7], dst)) + + def test_divmod_out(self, src, dst): + with _raises(): + np.divmod(_arr([7], src), _arr([2], src), out=(_arr([0], dst), _arr([0], src))) + + def test_ldexp_out(self, src, dst): + with _raises(): + np.ldexp(_arr([1.5], src), np.array([2]), out=_arr([0], dst)) + + @pytest.mark.parametrize("casting", ["same_kind", "unsafe"]) + @pytest.mark.parametrize("op", [np.sqrt, np.negative, np.sin]) + def test_unary_out(self, src, dst, op, casting): + out = _arr([7, 7], dst) + with _raises(): + op(_arr([0.5, 2.0], src), out=out, casting=casting) + np.testing.assert_array_equal(out, _arr([7, 7], dst)) + + @pytest.mark.parametrize("casting", ["same_kind", "unsafe"]) + def test_modf_out(self, src, dst, casting): + frac, whole = _arr([7], dst), _arr([7], src) + with _raises(): + np.modf(_arr([1.5], src), out=(frac, whole), casting=casting) + np.testing.assert_array_equal(frac, _arr([7], dst)) + + @pytest.mark.parametrize("casting", ["same_kind", "unsafe"]) + def test_frexp_out(self, src, dst, casting): + mantissa = _arr([7], dst) + with _raises(): + np.frexp(_arr([6.0], src), out=(mantissa, np.empty(1, np.int32)), + casting=casting) + np.testing.assert_array_equal(mantissa, _arr([7], dst)) + + def test_astype(self, src, dst): + with _raises(): + _arr([1.5], src).astype(QuadPrecDType(backend=dst)) + + @pytest.mark.parametrize("casting", ["same_kind", "safe", "same_value"]) + def test_astype_with_casting_rule(self, src, dst, casting): + with _cast_refused(): + _arr([1.5], src).astype(QuadPrecDType(backend=dst), casting=casting) + + @pytest.mark.parametrize("convert", [ + lambda a, dt: np.asarray(a, dtype=dt), + lambda a, dt: np.array(a, dtype=dt), + ], ids=["asarray", "array"]) + def test_array_construction(self, src, dst, convert): + with _raises(): + convert(_arr([1.5], src), QuadPrecDType(backend=dst)) + + def test_slice_assignment(self, src, dst): + target = _arr([7, 7], dst) + with _raises(): + target[...] = _arr([1, 2], src) + np.testing.assert_array_equal(target, _arr([7, 7], dst)) + + def test_copyto(self, src, dst): + target = _arr([7], dst) + with _raises(): + np.copyto(target, _arr([1], src), casting="unsafe") + np.testing.assert_array_equal(target, _arr([7], dst)) + + @pytest.mark.parametrize("join", [np.concatenate, np.stack]) + def test_join(self, src, dst, join): + with _raises(): + join([_arr([1], src), _arr([2], dst)]) + + def test_promotion(self, src, dst): + s, d = QuadPrecDType(backend=src), QuadPrecDType(backend=dst) + with _raises(): + np.result_type(s, d) + with _raises(): + np.promote_types(s, d) + + @pytest.mark.parametrize("casting", ["safe", "same_kind"]) + def test_can_cast(self, src, dst, casting): + assert not np.can_cast(QuadPrecDType(backend=src), QuadPrecDType(backend=dst), + casting=casting) + + +@pytest.mark.parametrize("src,dst", PAIRS) +class TestScalarsOfDifferentBackends: + def test_item_assignment(self, src, dst): + target = _arr([7, 7], dst) + with _raises(): + target[0] = QuadPrecision("1.5", backend=src) + np.testing.assert_array_equal(target, _arr([7, 7], dst)) + + def test_array_from_scalars(self, src, dst): + # Without a common instance NumPy keeps the scalars in an object array. + scalars = [QuadPrecision("1.5", backend=src), QuadPrecision("2.5", backend=dst)] + objects = np.array(scalars) + assert objects.dtype == np.dtype(object) + assert all(o is s for o, s in zip(objects, scalars)) + with _raises(): + np.array([QuadPrecision(1, backend=src)], dtype=QuadPrecDType(backend=dst)) + with _raises(): + np.full(2, QuadPrecision("1.5", backend=src), dtype=QuadPrecDType(backend=dst)) + + def test_object_array_cast(self, src, dst): + objects = np.array([QuadPrecision("1.5", backend=src)], dtype=object) + with _raises(): + objects.astype(QuadPrecDType(backend=dst)) + + def test_constructor(self, src, dst): + with _raises(): + QuadPrecision(QuadPrecision("1.5", backend=src), backend=dst) + + @pytest.mark.parametrize("op", [ + lambda x, y: x + y, lambda x, y: x * y, lambda x, y: x == y, lambda x, y: x < y, + ]) + def test_scalar_ops(self, src, dst, op): + with _raises(): + op(QuadPrecision(1, backend=src), QuadPrecision(2, backend=dst)) + + +def test_constructor_default_backend_rejects_longdouble(): + with _raises(): + QuadPrecision(QuadPrecision("1.5", backend="longdouble")) + + +@pytest.mark.parametrize("op", [ + lambda a: a + 1.0, lambda a: 2 * a, lambda a: a > 0, lambda a: np.maximum(a, 1), +]) +def test_python_literal_with_longdouble_array_raises(op): + # NumPy gives Python int and float operands the default (SLEEF) descriptor. + with pytest.raises(TypeError, match="Python int and float operands use 'sleef'"): + op(_arr([1.0, 2.0], "longdouble")) + + +@pytest.mark.parametrize("func", [ + lambda dt, a: np.ones(3, dtype=dt), + lambda dt, a: np.full(3, 1.5, dtype=dt), + lambda dt, a: np.ones_like(a), + lambda dt, a: a.mean(), + lambda dt, a: a.std(), + lambda dt, a: np.median(a), + lambda dt, a: np.isclose(a, a), + lambda dt, a: np.round(a, 1), + lambda dt, a: a.var(), + lambda dt, a: np.allclose(a, a), +], ids=["ones", "full", "ones_like", "mean", "std", "median", "isclose", "round", "var", + "allclose"]) +def test_numpy_functions_with_internal_python_scalars(func): + ld = QuadPrecDType(backend="longdouble") + with _raises(): + func(ld, _arr([1.0, 2.0, 3.0], "longdouble")) + sleef = QuadPrecDType(backend="sleef") + func(sleef, _arr([1.0, 2.0, 3.0], "sleef")) + + +def test_longdouble_workarounds(): + dt = QuadPrecDType(backend="longdouble") + a = _arr([1.0, 2.0, 3.0], "longdouble") + np.testing.assert_array_equal(np.full(3, np.longdouble(1), dtype=dt), _arr([1, 1, 1], "longdouble")) + np.testing.assert_array_equal(np.ones(3).astype(dt), _arr([1, 1, 1], "longdouble")) + np.testing.assert_array_equal(a / np.float64(2), _arr([0.5, 1.0, 1.5], "longdouble")) + assert a.sum() / np.float64(a.size) == QuadPrecision(2, backend="longdouble") + where = np.where(a > np.float64(1), a, 0) + np.testing.assert_array_equal(where, _arr([0.0, 2.0, 3.0], "longdouble")) + + +@pytest.mark.parametrize("backend", BACKENDS) +class TestStillSupported: + @pytest.mark.parametrize("other", [ + np.array([2.0, 4.0]), + np.array([2.0, 4.0], dtype=np.float32), + np.array([2, 4], dtype=np.int64), + np.array([2.0, 4.0], dtype=np.longdouble), + ], ids=["float64", "float32", "int64", "longdouble"]) + @pytest.mark.parametrize("quad_first", [True, False]) + def test_numpy_array_operand(self, backend, other, quad_first): + quad = _arr([1.0, 3.0], backend) + x, y = (quad, other) if quad_first else (other, quad) + result = np.add(x, y) + assert result.dtype == QuadPrecDType(backend=backend) + np.testing.assert_array_equal(result, _arr([3.0, 7.0], backend)) + np.testing.assert_array_equal(x < y, quad < other if quad_first else other < quad) + + @pytest.mark.parametrize("scalar", [ + np.float64(2.0), np.float32(2.0), np.int32(2), np.longdouble(2.0), + ], ids=["float64", "float32", "int32", "longdouble"]) + def test_numpy_scalar_operand(self, backend, scalar): + result = _arr([1.0, 3.0], backend) * scalar + assert result.dtype == QuadPrecDType(backend=backend) + np.testing.assert_array_equal(result, _arr([2.0, 6.0], backend)) + + def test_numpy_longdouble_casts(self, backend): + values = np.array([1, 2, 3], dtype=np.longdouble) / np.longdouble(4) + quad = values.astype(QuadPrecDType(backend=backend)) + assert quad.dtype == QuadPrecDType(backend=backend) + back = quad.astype(np.longdouble) + assert back.dtype == np.longdouble + np.testing.assert_array_equal(back, values) + + def test_scalar_with_python_literal(self, backend): + q = QuadPrecision("1.5", backend=backend) + assert q + 1.0 == QuadPrecision("2.5", backend=backend) + assert np.asarray(q * 2).dtype == QuadPrecDType(backend=backend) + assert q > 1 + + def test_same_backend_exchange(self, backend): + dt = QuadPrecDType(backend=backend) + a = _arr([1, 2], backend) + assert np.result_type(dt, dt) == dt + assert np.concatenate([a, a]).dtype == dt + target = _arr([0, 0], backend) + target[...] = a + target[0] = QuadPrecision(5, backend=backend) + np.copyto(target, a[::-1]) + np.testing.assert_array_equal(target, _arr([2, 1], backend)) + assert np.sqrt(a, out=_arr([0, 0], backend)).dtype == dt + + def test_constructor_copies_exactly(self, backend): + tiny = QuadPrecision(2.0, backend=backend) ** -60 + value = QuadPrecision(1, backend=backend) + tiny + assert value != QuadPrecision(1, backend=backend) + copy = QuadPrecision(value, backend=backend) + assert copy == value + assert copy.as_integer_ratio() == value.as_integer_ratio() + + +def test_sleef_array_with_python_literals(): + a = _arr([1.0, 2.0], "sleef") + np.testing.assert_array_equal(a + 1.0, _arr([2.0, 3.0], "sleef")) + np.testing.assert_array_equal(2 * a, _arr([2.0, 4.0], "sleef")) + np.testing.assert_array_equal(a > 1, [False, True]) + + +def test_numpy_dtypes_unaffected(): + ld = np.array([1, 2], dtype=np.longdouble) + assert (ld + 1.0).dtype == np.longdouble + assert np.result_type(np.longdouble, np.float64) == np.longdouble + assert np.concatenate([ld, np.array([3.0])]).dtype == np.longdouble From 47410fa52a618a51b79773b192ecaffd345cee19 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 12/22] DOC: document that QuadPrecision backends do not mix --- docs/api/core.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/api/core.md b/docs/api/core.md index 8fa6107..4067d45 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -136,6 +136,37 @@ The fundamental types provided by NumPy QuadDType. True ``` +### Mixing backends + +Values never move between the SLEEF and longdouble backends: any operation +that would combine or convert them raises `TypeError`. This covers ufuncs, +`out=` arrays, `astype`, assignment, `np.concatenate`, `np.result_type` and +`QuadPrecision(value, backend=...)`. NumPy's own dtypes, including +`np.longdouble`, work with both backends. + +Python `int` and `float` operands use the SLEEF backend, so with longdouble +arrays they raise too. This includes NumPy functions that pass Python scalars +internally, such as `np.ones`, `np.full`, `np.ones_like`, `mean`, `var`, +`std`, `np.median`, `np.isclose`, `np.allclose` and `np.round`. Pass NumPy scalars or arrays, or +`QuadPrecision(..., backend="longdouble")`, instead, for example +`np.full(3, np.longdouble(1), dtype=dt)` or `np.ones(3).astype(dt)`: + +```python +>>> import numpy as np +>>> from numpy_quaddtype import QuadPrecDType +>>> ld = np.array([1.5, 2.5], dtype=QuadPrecDType(backend="longdouble")) +>>> ld + np.float64(1) +array([2.5, 3.5], dtype=QuadPrecDType(backend='longdouble')) +>>> ld + 1 +Traceback (most recent call last): + ... +TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' (Python int and float operands use 'sleef') +>>> ld.astype(QuadPrecDType()) +Traceback (most recent call last): + ... +TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' (Python int and float operands use 'sleef') +``` + ## Convenience Functions ```{eval-rst} From d9310796d7bcaf3fab5230d8cf696d1dfa364923 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:21:43 +0530 Subject: [PATCH 13/22] DOC: add changelog with the mixed-backend behaviour change --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 6ff6c50..f6d8251 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ A cross-platform Quad (128-bit) float Data-Type for NumPy. - [Serving the documentation](#serving-the-documentation) - [Development Tips](#development-tips) - [Cleaning the Build Directory](#cleaning-the-build-directory) + - [Changelog](#changelog) ## Installation @@ -257,3 +258,44 @@ The subproject folders (`subprojects/sleef`, `subprojects/qblas`) are cloned as ```bash git clean -ffxd ``` + +## Changelog + +### Unreleased + +#### Backwards-incompatible changes + +- Values no longer move between the SLEEF and longdouble backends. Every + operation that would combine or convert them now raises + `TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble'`. + Previously these silently converted through `float64` (losing precision) + or picked a backend inconsistently. This covers: + - ufuncs with operands or `out=` arrays of different backends (before: + arithmetic and comparisons silently used longdouble); + - `astype`, `np.copyto`, slice and item assignment between backends + (before: silent conversion through `float64`); + - `np.result_type`, `np.promote_types` and `np.concatenate` across + backends (before: silently picked SLEEF); `np.array` of mixed + `QuadPrecision` scalars now gives an object array; + - `QuadPrecision(value, backend=...)` with a `QuadPrecision` of the other + backend, including `QuadPrecision(x)` for a longdouble `x`, since the + default backend is SLEEF. +- Python `int` and `float` operands use the SLEEF backend, so with longdouble + arrays they now raise as well. This includes NumPy functions that pass + Python scalars internally, such as `np.ones`, `np.full`, `np.ones_like`, + `mean`, `var`, `std`, `np.median`, `np.isclose`, `np.allclose` and + `np.round`. Use NumPy scalars or arrays + instead, for example `ld / np.float64(2)`, + `np.full(3, np.longdouble(1), dtype=ld.dtype)` or + `np.ones(3).astype(ld.dtype)`. SLEEF arrays and NumPy's own dtypes, + including `np.longdouble`, are unaffected. + +#### Bug fixes + +- Assigning a `QuadPrecision` scalar into an array of the other backend, or + casting an object array of them, stored the raw bytes under the wrong + backend (for example `0.0` instead of `1.5`). It now raises. +- Unary ufuncs such as `np.sqrt` with an `out=` array of the other backend + and `casting="unsafe"` wrote values in the wrong format. They now raise. +- `QuadPrecision(q)` for a `QuadPrecision` `q` of the same backend rounded + through `float64`; it now copies the value exactly. From d0fd9125a2c21d8a313a503b74639feb8f171ca8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:25:29 +0530 Subject: [PATCH 14/22] DOC: note that some mixed-backend errors carry NumPy's casting message --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f6d8251..706b2ff 100644 --- a/README.md +++ b/README.md @@ -266,10 +266,12 @@ git clean -ffxd #### Backwards-incompatible changes - Values no longer move between the SLEEF and longdouble backends. Every - operation that would combine or convert them now raises - `TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble'`. - Previously these silently converted through `float64` (losing precision) - or picked a backend inconsistently. This covers: + operation that would combine or convert them now raises `TypeError`, with + the message `Cannot mix QuadPrecision backends 'sleef' and 'longdouble'`, + or NumPy's own `Cannot cast ...` message where NumPy checks the casting + rule first (for example `astype(..., casting="same_kind")`). Previously + these silently converted through `float64` (losing precision) or picked a + backend inconsistently. This covers: - ufuncs with operands or `out=` arrays of different backends (before: arithmetic and comparisons silently used longdouble); - `astype`, `np.copyto`, slice and item assignment between backends From ff9acd9f6168fc956d69355760781d27312a4828 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 11:25:29 +0530 Subject: [PATCH 15/22] DOC: mention NumPy's casting message for mixed backends --- docs/api/core.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/core.md b/docs/api/core.md index 4067d45..af02654 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -139,7 +139,9 @@ The fundamental types provided by NumPy QuadDType. ### Mixing backends Values never move between the SLEEF and longdouble backends: any operation -that would combine or convert them raises `TypeError`. This covers ufuncs, +that would combine or convert them raises `TypeError`: either +`Cannot mix QuadPrecision backends ...` or, where NumPy checks the casting rule +first, NumPy's own `Cannot cast ...` message. This covers ufuncs, `out=` arrays, `astype`, assignment, `np.concatenate`, `np.result_type` and `QuadPrecision(value, backend=...)`. NumPy's own dtypes, including `np.longdouble`, work with both backends. From 920864945e7f5d9111444782d86b3bbf24e2e2de Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 13:57:05 +0530 Subject: [PATCH 16/22] MAINT: keep the cross-backend cast loop for gh-112 and refuse only in the resolver --- src/csrc/casts.cpp | 124 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 9 deletions(-) diff --git a/src/csrc/casts.cpp b/src/csrc/casts.cpp index 788c2e0..140d87c 100644 --- a/src/csrc/casts.cpp +++ b/src/csrc/casts.cpp @@ -69,19 +69,76 @@ quad_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), return NPY_NO_CASTING; } + Py_INCREF(given_descrs[1]); + loop_descrs[1] = given_descrs[1]; + if (given_descrs[0]->backend != given_descrs[1]->backend) { + // gh-112: the loop already handles cross-backend casts; allow them here once they are exact. quad_set_backend_mismatch_error(); - Py_CLEAR(loop_descrs[0]); - return (NPY_CASTING)-1; + return quad_resolve_descrs_fail((PyArray_Descr **)loop_descrs, 2); } - Py_INCREF(given_descrs[1]); - loop_descrs[1] = given_descrs[1]; - *view_offset = 0; return static_cast(NPY_NO_CASTING | NPY_SAME_VALUE_CASTING_FLAG); } +// Helper function for quad-to-quad same_value check (inter-backend) +// NOTE: the inter-backend uses `double` as intermediate, +// so only values that can be exactly represented in double can pass same_value check +static inline int +quad_to_quad_same_value_check(const quad_value *in_val, QuadBackendType backend_in, + const quad_value *out_val, QuadBackendType backend_out) +{ + // Convert output back to input backend for comparison + quad_value roundtrip; + + if (backend_in == BACKEND_SLEEF) { + // Input was SLEEF, output is longdouble + // Convert longdouble back to SLEEF for comparison + long double ld = out_val->longdouble_value; + if (std::isnan(ld)) { + // Preserve sign of NaN + roundtrip.sleef_value = (!ld_signbit(&ld)) ? QUAD_PRECISION_NAN : QUAD_PRECISION_NEG_NAN; + } + else if (std::isinf(ld)) { + roundtrip.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF; + } + else { + Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast(ld)); + memcpy(&roundtrip.sleef_value, &temp, sizeof(Sleef_quad)); + } + + // Compare in SLEEF domain && signbit preserved + bool is_sign_preserved = (quad_signbit(&in_val->sleef_value) == quad_signbit(&roundtrip.sleef_value)); + if(quad_isnan(&in_val->sleef_value) && quad_isnan(&roundtrip.sleef_value) && is_sign_preserved) + return 1; // Both NaN + if (Sleef_icmpeqq1(in_val->sleef_value, roundtrip.sleef_value) && is_sign_preserved) + return 1; // Equal + } + else { + // Input was longdouble, output is SLEEF + // Convert SLEEF back to longdouble for comparison + roundtrip.longdouble_value = static_cast(cast_sleef_to_double(out_val->sleef_value)); + + // Compare in longdouble domain && signbit preserved + bool is_sign_preserved = (ld_signbit(&in_val->longdouble_value) == ld_signbit(&roundtrip.longdouble_value)); + if ((std::isnan(in_val->longdouble_value) && std::isnan(roundtrip.longdouble_value)) && is_sign_preserved) + return 1; + if ((in_val->longdouble_value == roundtrip.longdouble_value) && is_sign_preserved) + return 1; + } + + // Values don't match + Sleef_quad sleef_val = quad_to_sleef_quad(in_val, backend_in); + const char *val_str = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); + if (val_str != NULL) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision value '%s' cannot be represented exactly in target backend", + val_str); + } + return -1; +} + template static int quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], @@ -94,13 +151,62 @@ quad_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], npy_intp in_stride = strides[0]; npy_intp out_stride = strides[1]; - // The resolver only accepts casts within one backend. - QuadBackendType backend = ((QuadPrecDTypeObject *)context->descriptors[0])->backend; + QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)context->descriptors[0]; + QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)context->descriptors[1]; + QuadBackendType backend_in = descr_in->backend; + QuadBackendType backend_out = descr_out->backend; + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CONTEXT_FLAG) == NPY_SAME_VALUE_CONTEXT_FLAG); + + // inter-backend casting + if (backend_in != backend_out) { + while (N--) { + quad_value in_val; + load_quad(in_ptr, backend_in, &in_val); + quad_value out_val; + if (backend_in == BACKEND_SLEEF) + { + out_val.longdouble_value = static_cast(cast_sleef_to_double(in_val.sleef_value)); + } + else + { + long double ld = in_val.longdouble_value; + if (std::isnan(ld)) { + out_val.sleef_value = (!ld_signbit(&ld)) ? QUAD_PRECISION_NAN : QUAD_PRECISION_NEG_NAN; + } + else if (std::isinf(ld)) { + out_val.sleef_value = (ld > 0) ? QUAD_PRECISION_INF : QUAD_PRECISION_NINF; + } + else + { + // to prevent compiler optimizations, ABI handling issues with __float128 on x86-64 machines + // won't be expensive as for fixed size compiler can optimize memcpy with movq + Sleef_quad temp = Sleef_cast_from_doubleq1(static_cast(ld)); + std::memcpy(&out_val.sleef_value, &temp, sizeof(Sleef_quad)); + } + } + + // check same_value for inter-backend casts + if(same_value_casting) + { + int ret = quad_to_quad_same_value_check(&in_val, backend_in, &out_val, backend_out); + if (ret < 0) { + return -1; + } + } + + store_quad(out_ptr, &out_val, backend_out); + in_ptr += in_stride; + out_ptr += out_stride; + } + return 0; + } + // same backend: direct copy + // same_value casting not needed here as values are identical while(N--) { quad_value val; - load_quad(in_ptr, backend, &val); - store_quad(out_ptr, &val, backend); + load_quad(in_ptr, backend_in, &val); + store_quad(out_ptr, &val, backend_out); in_ptr += in_stride; out_ptr += out_stride; } From 9d2d1475509e3f4706ecfeb60bfe17a1110c09b2 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 13:57:05 +0530 Subject: [PATCH 17/22] TST: restore cross-backend same_value cast tests as strict xfails for gh-112 --- tests/test_quaddtype.py | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index ad3d433..d9100c9 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -6098,6 +6098,11 @@ def test_quad_to_quad_backend_casting(backend, value): else: np.testing.assert_array_equal(dst_arr, res_arr) +CROSS_BACKEND_CAST_REFUSED = pytest.mark.xfail( + raises=TypeError, strict=True, reason="gh-112: casts between backends are refused" +) + + class TestSameValueCasting: """Test 'same_value' casting behavior for QuadPrecision.""" def test_same_value_cast(self): @@ -6281,11 +6286,15 @@ def test_same_value_cast_strings_narrow_width(self, dtype): q.astype(dtype, casting="same_value") @pytest.mark.parametrize("src_backend,dst_backend", [ + pytest.param("sleef", "longdouble", marks=CROSS_BACKEND_CAST_REFUSED), + pytest.param("longdouble", "sleef", marks=CROSS_BACKEND_CAST_REFUSED), ("sleef", "sleef"), ("longdouble", "longdouble") ]) def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend): - """Test values that roundtrip exactly within a backend.""" + """Test values that should roundtrip exactly between backends.""" + # Values exactly representable in both backends (and in double, since + # inter-backend conversion goes through double) passing_values = [ 0.0, -0.0, 1.0, -1.0, 0.5, 0.25, 0.125, @@ -6308,6 +6317,43 @@ def test_quad_to_quad_same_value_casting_passing(self, src_backend, dst_backend) assert float(result[0]) == float(src[0]), f"Value {val} failed for {src_backend} -> {dst_backend}" + @CROSS_BACKEND_CAST_REFUSED + @pytest.mark.parametrize("src_backend,dst_backend", [ + ("sleef", "longdouble"), + ("longdouble", "sleef"), + ]) + def test_quad_to_quad_interbackend_same_value_casting_failing(self, src_backend, dst_backend): + """Test values that cannot roundtrip exactly between backends. + + Inter-backend conversion goes through double, so values exceeding + double's precision (~53 bits mantissa) will fail same_value casting. + """ + ld_info = np.finfo(np.longdouble) + double_info = np.finfo(np.float64) + + # Skip if longdouble has same precision as quad (PowerPC binary128) + # In that case, sleef <-> longdouble might use a direct path + if ld_info.nmant >= 112: + pytest.skip("longdouble has same precision as quad on this platform") + + # For longdouble -> sleef: only fails if longdouble has more precision than double + if src_backend == "longdouble" and ld_info.nmant <= double_info.nmant: + pytest.skip("longdouble has same or less precision than double on this platform") + + # Values that exceed double precision (53-bit mantissa) + # These will lose precision when going through the double conversion + failing_values = [ + str(2**53 + 1), # First integer not exactly representable in double + "3.141592653589793238462643383279502884197", # Pi with more than double precision + "1.00000000000000011", # 1 + epsilon beyond double precision + ] + + for val in failing_values: + src = np.array([val], dtype=QuadPrecDType(backend=src_backend)) + with pytest.raises(ValueError): + src.astype(QuadPrecDType(backend=dst_backend), casting="same_value") + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) def test_quad_to_quad_same_backend_always_passes(self, backend): """Same backend conversion should always pass same_value.""" From 0c77c9184d9ecb79b4954a504046a630e90c9a75 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 13:57:05 +0530 Subject: [PATCH 18/22] TST: use each backend's epsilon in the exact constructor copy test --- tests/test_backend_mixing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_backend_mixing.py b/tests/test_backend_mixing.py index bc85001..d23d352 100644 --- a/tests/test_backend_mixing.py +++ b/tests/test_backend_mixing.py @@ -255,8 +255,9 @@ def test_same_backend_exchange(self, backend): assert np.sqrt(a, out=_arr([0, 0], backend)).dtype == dt def test_constructor_copies_exactly(self, backend): - tiny = QuadPrecision(2.0, backend=backend) ** -60 - value = QuadPrecision(1, backend=backend) + tiny + nmant = 112 if backend == "sleef" else np.finfo(np.longdouble).nmant + eps = QuadPrecision(2.0, backend=backend) ** -nmant + value = QuadPrecision(1, backend=backend) + eps assert value != QuadPrecision(1, backend=backend) copy = QuadPrecision(value, backend=backend) assert copy == value From 090528e790d44a53879fdd5d54f19b174dc29fac Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 13:57:05 +0530 Subject: [PATCH 19/22] CI: skip pandas 3.1.0 on s390x, which fails to build there --- .github/workflows/big_endian.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/big_endian.yml b/.github/workflows/big_endian.yml index 528ad17..219028d 100644 --- a/.github/workflows/big_endian.yml +++ b/.github/workflows/big_endian.yml @@ -137,7 +137,8 @@ jobs: python -m pip install --break-system-packages --no-deps . -v --no-build-isolation --force-reinstall && # Install test dependencies separately - python -m pip install --break-system-packages .[test] && + # pandas 3.1.0 fails to build on s390x: pandas-dev/pandas#71548 + python -m pip install --break-system-packages .[test] pandas!=3.1.0 && python -m pytest -vvv --color=yes --timeout=600 --tb=short tests/ '" From b89d147eb1b292b804582ab93ed89c2b3b484132 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 14:23:10 +0530 Subject: [PATCH 20/22] MAINT: drop the Python-literal hint from the backend mismatch error --- src/include/dtype.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/include/dtype.h b/src/include/dtype.h index 0f3e934..f262c1e 100644 --- a/src/include/dtype.h +++ b/src/include/dtype.h @@ -38,8 +38,7 @@ static inline void quad_set_backend_mismatch_error(void) { PyErr_SetString(PyExc_TypeError, - "Cannot mix QuadPrecision backends 'sleef' and 'longdouble' " - "(Python int and float operands use 'sleef')"); + "Cannot mix QuadPrecision backends 'sleef' and 'longdouble'"); } /* From 5cdda83f297ac2ce774806141ebbdb3009e3f68a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 14:23:10 +0530 Subject: [PATCH 21/22] TST: match the general mismatch message for Python literals --- tests/test_backend_mixing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_backend_mixing.py b/tests/test_backend_mixing.py index d23d352..fa1ac4c 100644 --- a/tests/test_backend_mixing.py +++ b/tests/test_backend_mixing.py @@ -167,7 +167,7 @@ def test_constructor_default_backend_rejects_longdouble(): ]) def test_python_literal_with_longdouble_array_raises(op): # NumPy gives Python int and float operands the default (SLEEF) descriptor. - with pytest.raises(TypeError, match="Python int and float operands use 'sleef'"): + with pytest.raises(TypeError, match=MIXED): op(_arr([1.0, 2.0], "longdouble")) From afe690b55461b2792283bc4ad93b6ab3f5569791 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Tue, 3 Nov 2026 14:23:10 +0530 Subject: [PATCH 22/22] DOC: show the shorter backend mismatch error --- docs/api/core.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/core.md b/docs/api/core.md index af02654..6166042 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -162,11 +162,11 @@ array([2.5, 3.5], dtype=QuadPrecDType(backend='longdouble')) >>> ld + 1 Traceback (most recent call last): ... -TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' (Python int and float operands use 'sleef') +TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' >>> ld.astype(QuadPrecDType()) Traceback (most recent call last): ... -TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' (Python int and float operands use 'sleef') +TypeError: Cannot mix QuadPrecision backends 'sleef' and 'longdouble' ``` ## Convenience Functions