From 5b69baad687f2c057ff7e6c96a194eecb377046f Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 17:15:16 +0530 Subject: [PATCH 01/14] added __reduce__ + exp_digits = 4 + tests --- src/csrc/casts.cpp | 2 +- src/csrc/scalar.c | 54 +++++++++++++++++++- tests/test_quaddtype.py | 109 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/csrc/casts.cpp b/src/csrc/casts.cpp index 1465a6f..8f15e46 100644 --- a/src/csrc/casts.cpp +++ b/src/csrc/casts.cpp @@ -416,7 +416,7 @@ quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars) // Use scientific notation with full precision const char *scientific_str = Dragon4_Scientific_QuadDType_CStr(sleef_val, DigitMode_Unique, SLEEF_QUAD_DECIMAL_DIG, 0, 1, - TrimMode_LeaveOneZero, 1, 2); + TrimMode_LeaveOneZero, 1, 4); if (scientific_str == NULL) { PyErr_SetString(PyExc_RuntimeError, "Float formatting failed"); return NULL; diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 140e768..58b489f 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -352,7 +352,7 @@ QuadPrecision_repr_dragon4(QuadPrecisionObject *self) .sign = 1, .trim_mode = TrimMode_LeaveOneZero, .digits_left = 1, - .exp_digits = 3}; + .exp_digits = 4}; PyObject *str; if (self->backend == BACKEND_SLEEF) { @@ -601,11 +601,63 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig return PyTuple_Pack(2, numerator, denominator); } +static PyObject * +QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) +{ + Dragon4_Options opt = {.scientific = 1, + .digit_mode = DigitMode_Unique, + .cutoff_mode = CutoffMode_TotalLength, + .precision = SLEEF_QUAD_DECIMAL_DIG, + .sign = 1, + .trim_mode = TrimMode_LeaveOneZero, + .digits_left = 1, + .exp_digits = 4}; + + PyObject *str_value; + if (self->backend == BACKEND_SLEEF) { + str_value = Dragon4_Scientific_QuadDType(&self->value.sleef_value, opt.digit_mode, + opt.precision, opt.min_digits, opt.sign, + opt.trim_mode, opt.digits_left, opt.exp_digits); + } + else { + Sleef_quad sleef_val = Sleef_cast_from_doubleq1(self->value.longdouble_value); + str_value = Dragon4_Scientific_QuadDType(&sleef_val, opt.digit_mode, opt.precision, + opt.min_digits, opt.sign, opt.trim_mode, + opt.digits_left, opt.exp_digits); + } + if (str_value == NULL) { + return NULL; + } + + PyObject *backend_obj = PyUnicode_FromString( + self->backend == BACKEND_SLEEF ? "sleef" : "longdouble"); + if (backend_obj == NULL) { + Py_DECREF(str_value); + return NULL; + } + + PyObject *args = PyTuple_Pack(2, str_value, backend_obj); + Py_DECREF(str_value); + Py_DECREF(backend_obj); + if (args == NULL) { + return NULL; + } + + PyObject *type_obj = (PyObject *)Py_TYPE(self); + Py_INCREF(type_obj); + PyObject *result = PyTuple_Pack(2, type_obj, args); + Py_DECREF(type_obj); + Py_DECREF(args); + return result; +} + static PyMethodDef QuadPrecision_methods[] = { {"is_integer", (PyCFunction)QuadPrecision_is_integer, METH_NOARGS, "Return True if the value is an integer."}, {"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS, "Return a pair of integers whose ratio is exactly equal to the original value."}, + {"__reduce__", (PyCFunction)QuadPrecision_reduce, METH_NOARGS, + "Support pickling: return (QuadPrecision, (str_value, backend))."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 83ec1f8..4f0085c 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5338,6 +5338,115 @@ def test_pickle_fortran_order(self, backend): assert unpickled.dtype == original.dtype assert unpickled.flags.f_contiguous == original.flags.f_contiguous + +class TestScalarPickle: + """Regression tests for issue #99: bare QuadPrecision scalars (not wrapped + in an array) must round-trip through pickle.dumps / pickle.loads without + raising and must preserve value, type, and backend.""" + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_issue_repro(self, backend): + """The exact repro from #99: was RuntimeError on loads().""" + import pickle + original = QuadPrecision("123.456", backend=backend) + loaded = pickle.loads(pickle.dumps(original)) + assert isinstance(loaded, QuadPrecision) + assert loaded == original + assert str(loaded) == str(original) + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + @pytest.mark.parametrize("value", [ + "0.0", "-0.0", "1.0", "-1.0", "42.0", "-42.0", + "3.141592653589793238462643383279502884197", # ~quad-precision pi + "2.718281828459045235360287471352662497757", + "1e100", "1e-100", "-1e100", "-1e-100", + "1.23456789012345678901234567890e30", + ]) + def test_pickle_scalar_finite_roundtrip(self, backend, value): + """Finite values must round-trip exactly (Dragon4-Unique is lossless).""" + import pickle + original = QuadPrecision(value, backend=backend) + loaded = pickle.loads(pickle.dumps(original)) + assert isinstance(loaded, QuadPrecision) + assert loaded.dtype == QuadPrecDType(backend=backend) + # Exact equality: pickle/unpickle should not lose any bits. + assert loaded == original + # And the canonical repr should match. + assert str(loaded) == str(original) + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_inf(self, backend): + import pickle + for s in ["inf", "-inf"]: + original = QuadPrecision(s, backend=backend) + loaded = pickle.loads(pickle.dumps(original)) + assert isinstance(loaded, QuadPrecision) + assert loaded == original # inf == inf, -inf == -inf + assert float(loaded) == float(original) + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_nan(self, backend): + import pickle + original = QuadPrecision("nan", backend=backend) + loaded = pickle.loads(pickle.dumps(original)) + assert isinstance(loaded, QuadPrecision) + # NaN != NaN, so we check isnan instead. + import math + assert math.isnan(float(loaded)) + assert loaded.dtype == QuadPrecDType(backend=backend) + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + @pytest.mark.parametrize("protocol", [0, 1, 2, 3, 4, 5]) + def test_pickle_scalar_all_protocols(self, backend, protocol): + """Round-trip must work across every pickle protocol version.""" + import pickle + original = QuadPrecision("3.14159265358979323846", backend=backend) + data = pickle.dumps(original, protocol=protocol) + loaded = pickle.loads(data) + assert isinstance(loaded, QuadPrecision) + assert loaded == original + assert loaded.dtype == QuadPrecDType(backend=backend) + + def test_pickle_scalar_preserves_type(self): + import pickle + loaded = pickle.loads(pickle.dumps(QuadPrecision("1.0"))) + assert type(loaded) is QuadPrecision + + def test_pickle_scalar_preserves_backend_across_mix(self): + """Each backend pickle must come back as the same backend, not silently + defaulting to sleef.""" + import pickle + ld = QuadPrecision("1.5", backend="longdouble") + sl = QuadPrecision("1.5", backend="sleef") + ld_loaded = pickle.loads(pickle.dumps(ld)) + sl_loaded = pickle.loads(pickle.dumps(sl)) + assert ld_loaded.dtype == QuadPrecDType(backend="longdouble") + assert sl_loaded.dtype == QuadPrecDType(backend="sleef") + + def test_pickle_scalar_in_list(self): + """Composite container of scalars also pickles cleanly.""" + import pickle + original = [QuadPrecision("1.5"), QuadPrecision("2.5"), + QuadPrecision("nan"), QuadPrecision("inf")] + loaded = pickle.loads(pickle.dumps(original)) + import math + assert len(loaded) == 4 + assert loaded[0] == original[0] + assert loaded[1] == original[1] + assert math.isnan(float(loaded[2])) + assert loaded[3] == original[3] + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_loads_does_not_raise(self, backend): + """Direct regression on the exact failure mode in the bug report + (RuntimeError from numpy's legacy SETITEM path).""" + import pickle + try: + pickle.loads(pickle.dumps(QuadPrecision("123.456", backend=backend))) + except RuntimeError as exc: + pytest.fail(f"pickle.loads raised RuntimeError: {exc}") + + @pytest.mark.parametrize("dtype", [ "bool", "byte", "int8", "ubyte", "uint8", From 9cd56742b05a63aa164e5e0da3a287b9fb241c37 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 17:46:36 +0530 Subject: [PATCH 02/14] use snsprintf for longdouble --- src/csrc/scalar.c | 14 ++++++++++---- tests/test_quaddtype.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 58b489f..a9a3252 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -2,6 +2,7 @@ #include #include #include +#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION @@ -620,10 +621,15 @@ QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) opt.trim_mode, opt.digits_left, opt.exp_digits); } else { - Sleef_quad sleef_val = Sleef_cast_from_doubleq1(self->value.longdouble_value); - str_value = Dragon4_Scientific_QuadDType(&sleef_val, opt.digit_mode, opt.precision, - opt.min_digits, opt.sign, opt.trim_mode, - opt.digits_left, opt.exp_digits); + char buffer[128]; + int written = snprintf(buffer, sizeof(buffer), "%.*Le", + LDBL_DECIMAL_DIG - 1, self->value.longdouble_value); + if (written < 0 || written >= (int)sizeof(buffer)) { + PyErr_SetString(PyExc_RuntimeError, + "Failed to format long double for pickle"); + return NULL; + } + str_value = PyUnicode_FromString(buffer); } if (str_value == NULL) { return NULL; diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 4f0085c..56888dc 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5412,6 +5412,24 @@ def test_pickle_scalar_preserves_type(self): loaded = pickle.loads(pickle.dumps(QuadPrecision("1.0"))) assert type(loaded) is QuadPrecision + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_preserves_full_precision(self, backend): + """Diagnostic precision-loss test. Two values with the same printed + repr can still differ at the full bit width, so check via subtraction + (which preserves precision) — `loaded - original` must be exactly zero. + Catches the regression where the longdouble path went through (double).""" + import pickle + # A value with more than 16 significant digits — exercises precision + # beyond what double can represent. + original = QuadPrecision("3.14159265358979323846264338327950288", + backend=backend) + loaded = pickle.loads(pickle.dumps(original)) + diff = loaded - original + assert diff == QuadPrecision("0.0", backend=backend), ( + f"pickle round-trip lost precision on {backend}: " + f"loaded - original = {diff!r}" + ) + def test_pickle_scalar_preserves_backend_across_mix(self): """Each backend pickle must come back as the same backend, not silently defaulting to sleef.""" From 4c32b91a103ddc148834506d1cc6b2750311330b Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 20:02:42 +0530 Subject: [PATCH 03/14] defining LDBL_DECIMAL_DIG for MSVC --- src/csrc/scalar.c | 5 +++-- src/include/constants.hpp | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index a9a3252..0ff1f71 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -2,7 +2,6 @@ #include #include #include -#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION @@ -332,13 +331,15 @@ QuadPrecision_str_dragon4(QuadPrecisionObject *self) static PyObject * QuadPrecision_str(QuadPrecisionObject *self) +// This is just define here for debugging, we actually use QuadPrecision_str_dragon4 for __str__ method. { char buffer[128]; if (self->backend == BACKEND_SLEEF) { Sleef_snprintf(buffer, sizeof(buffer), "%.*Qe", SLEEF_QUAD_DIG, self->value.sleef_value); } else { - snprintf(buffer, sizeof(buffer), "%.35Le", self->value.longdouble_value); + snprintf(buffer, sizeof(buffer), "%.*Le", LDBL_DECIMAL_DIG - 1, + self->value.longdouble_value); } return PyUnicode_FromString(buffer); } diff --git a/src/include/constants.hpp b/src/include/constants.hpp index e9733f0..8886731 100644 --- a/src/include/constants.hpp +++ b/src/include/constants.hpp @@ -9,6 +9,15 @@ extern "C" { #include #include #include +#include + +/* LDBL_DECIMAL_DIG: minimum decimal digits needed for a lossless + * long-double → string → long-double round-trip. Standard C11 constant in + * , but MSVC omits it. On MSVC long double is the same width as + * double, so DBL_DECIMAL_DIG (17) is the exact correct fallback. */ +#ifndef LDBL_DECIMAL_DIG +# define LDBL_DECIMAL_DIG DBL_DECIMAL_DIG +#endif // Quad precision constants using sleef_q macro #define QUAD_PRECISION_ZERO sleef_q(+0x0000000000000LL, 0x0000000000000000ULL, -16383) From 34c9c6d553108bb38be792fb910cf21b4cfbddc5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 17:09:11 +0000 Subject: [PATCH 04/14] fix asymmetric check for rounding intervals + tests --- src/csrc/dragon4.c | 2 +- tests/test_quaddtype.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/csrc/dragon4.c b/src/csrc/dragon4.c index f86f269..bf37f8c 100644 --- a/src/csrc/dragon4.c +++ b/src/csrc/dragon4.c @@ -1929,7 +1929,7 @@ Dragon4_PrintFloat_Sleef_quad(Sleef_quad *value, Dragon4_Options *opt) /* mantissa_lo is unchanged */ exponent = floatExponent - 16383 - 112; mantissaBit = 112; - hasUnequalMargins = (floatExponent != 1) && (mantissa_hi == 0 && mantissa_lo == 0); + hasUnequalMargins = (floatExponent != 1) && (mantissa_hi == (1ull << 48) && mantissa_lo == 0); } else { /* subnormal */ diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 56888dc..ca08f63 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -308,6 +308,39 @@ def test_string_roundtrip(): ) +def test_string_roundtrip_all_powers_of_two(): + """Every exact power of two from the smallest subnormal up to overflow must + round-trip through str() and repr(). Powers of two are the only values whose + rounding interval is asymmetric, so they are the sole trigger for Dragon4 + margin bugs and are otherwise unreachable by random or decimal fuzzing.""" + two = QuadPrecision("2.0", backend="sleef") + maxv = numpy_quaddtype.max_value + p = numpy_quaddtype.smallest_subnormal + + str_fails = [] + repr_fails = [] + tested = 0 + while True: + tested += 1 + if QuadPrecision(str(p), backend="sleef") != p: + str_fails.append(str(p)) + repr_inner = repr(p).split("'")[1] + if QuadPrecision(repr_inner, backend="sleef") != p: + repr_fails.append(repr(p)) + nxt = p * two + if not (abs(nxt) <= maxv): + break + p = nxt + + assert tested > 30000, f"expected the full power-of-two sweep, only tested {tested}" + assert not str_fails, ( + f"{len(str_fails)} powers of two failed str() round-trip, e.g. {str_fails[:5]}" + ) + assert not repr_fails, ( + f"{len(repr_fails)} powers of two failed repr() round-trip, e.g. {repr_fails[:5]}" + ) + + class TestBytesSupport: """Test suite for QuadPrecision bytes input support.""" From 9d57423e20416aa44cf17147d8e53af488bf5fce Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 17:13:04 +0000 Subject: [PATCH 05/14] applying other reviews --- src/csrc/scalar.c | 15 --------------- tests/test_quaddtype.py | 24 +++--------------------- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 0ff1f71..e14854c 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -329,21 +329,6 @@ QuadPrecision_str_dragon4(QuadPrecisionObject *self) } } -static PyObject * -QuadPrecision_str(QuadPrecisionObject *self) -// This is just define here for debugging, we actually use QuadPrecision_str_dragon4 for __str__ method. -{ - char buffer[128]; - if (self->backend == BACKEND_SLEEF) { - Sleef_snprintf(buffer, sizeof(buffer), "%.*Qe", SLEEF_QUAD_DIG, self->value.sleef_value); - } - else { - snprintf(buffer, sizeof(buffer), "%.*Le", LDBL_DECIMAL_DIG - 1, - self->value.longdouble_value); - } - return PyUnicode_FromString(buffer); -} - static PyObject * QuadPrecision_repr_dragon4(QuadPrecisionObject *self) { diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index ca08f63..9fb8c20 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5379,7 +5379,6 @@ class TestScalarPickle: @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) def test_pickle_scalar_issue_repro(self, backend): - """The exact repro from #99: was RuntimeError on loads().""" import pickle original = QuadPrecision("123.456", backend=backend) loaded = pickle.loads(pickle.dumps(original)) @@ -5402,9 +5401,7 @@ def test_pickle_scalar_finite_roundtrip(self, backend, value): loaded = pickle.loads(pickle.dumps(original)) assert isinstance(loaded, QuadPrecision) assert loaded.dtype == QuadPrecDType(backend=backend) - # Exact equality: pickle/unpickle should not lose any bits. assert loaded == original - # And the canonical repr should match. assert str(loaded) == str(original) @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) @@ -5414,7 +5411,7 @@ def test_pickle_scalar_inf(self, backend): original = QuadPrecision(s, backend=backend) loaded = pickle.loads(pickle.dumps(original)) assert isinstance(loaded, QuadPrecision) - assert loaded == original # inf == inf, -inf == -inf + assert loaded == original assert float(loaded) == float(original) @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) @@ -5423,7 +5420,6 @@ def test_pickle_scalar_nan(self, backend): original = QuadPrecision("nan", backend=backend) loaded = pickle.loads(pickle.dumps(original)) assert isinstance(loaded, QuadPrecision) - # NaN != NaN, so we check isnan instead. import math assert math.isnan(float(loaded)) assert loaded.dtype == QuadPrecDType(backend=backend) @@ -5447,13 +5443,9 @@ def test_pickle_scalar_preserves_type(self): @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) def test_pickle_scalar_preserves_full_precision(self, backend): - """Diagnostic precision-loss test. Two values with the same printed - repr can still differ at the full bit width, so check via subtraction - (which preserves precision) — `loaded - original` must be exactly zero. - Catches the regression where the longdouble path went through (double).""" + """Compare via subtraction, not repr: two values with the same printed + repr can still differ at the full bit width.""" import pickle - # A value with more than 16 significant digits — exercises precision - # beyond what double can represent. original = QuadPrecision("3.14159265358979323846264338327950288", backend=backend) loaded = pickle.loads(pickle.dumps(original)) @@ -5487,16 +5479,6 @@ def test_pickle_scalar_in_list(self): assert math.isnan(float(loaded[2])) assert loaded[3] == original[3] - @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) - def test_pickle_scalar_loads_does_not_raise(self, backend): - """Direct regression on the exact failure mode in the bug report - (RuntimeError from numpy's legacy SETITEM path).""" - import pickle - try: - pickle.loads(pickle.dumps(QuadPrecision("123.456", backend=backend))) - except RuntimeError as exc: - pytest.fail(f"pickle.loads raised RuntimeError: {exc}") - @pytest.mark.parametrize("dtype", [ "bool", From 1e69bec4f4001be44bf32789827fd1aa546240b9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Thu, 16 Jul 2026 15:20:04 +0530 Subject: [PATCH 06/14] pickle from raw bytes and reconstruct --- src/csrc/quaddtype_main.c | 3 + src/csrc/scalar.c | 128 ++++++++++++++++++++++++++++---------- src/include/constants.hpp | 9 --- 3 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/csrc/quaddtype_main.c b/src/csrc/quaddtype_main.c index 4aaa6a5..3fed41a 100644 --- a/src/csrc/quaddtype_main.c +++ b/src/csrc/quaddtype_main.c @@ -68,6 +68,9 @@ get_sleef_constant(PyObject *self, PyObject *args) static PyMethodDef module_methods[] = { {"is_longdouble_128", py_is_longdouble_128, METH_NOARGS, "Check if long double is 128-bit"}, {"get_sleef_constant", get_sleef_constant, METH_VARARGS, "Get Sleef constant by name"}, + {"from_raw_bytes", QuadPrecision_from_raw_bytes, METH_VARARGS, + "Reconstruct a QuadPrecision scalar from its raw little-endian bytes " + "(used by pickle)."}, {"set_num_threads", py_quadblas_set_num_threads, METH_VARARGS, "Set number of threads for QuadBLAS"}, {"get_num_threads", py_quadblas_get_num_threads, METH_NOARGS, diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index ceceede..f39686a 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -620,68 +620,130 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig return PyTuple_Pack(2, numerator, denominator); } -static PyObject * -QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) +static int +quad_host_is_big_endian(void) { - Dragon4_Options opt = {.scientific = 1, - .digit_mode = DigitMode_Unique, - .cutoff_mode = CutoffMode_TotalLength, - .precision = SLEEF_QUAD_DECIMAL_DIG, - .sign = 1, - .trim_mode = TrimMode_LeaveOneZero, - .digits_left = 1, - .exp_digits = 4}; + uint16_t probe = 1; + return ((const unsigned char *)&probe)[0] == 0; +} - PyObject *str_value; - if (self->backend == BACKEND_SLEEF) { - str_value = Dragon4_Scientific_QuadDType(&self->value.sleef_value, opt.digit_mode, - opt.precision, opt.min_digits, opt.sign, - opt.trim_mode, opt.digits_left, opt.exp_digits); + +static void +quad_copy_canonical(unsigned char *dst, const unsigned char *src, size_t n) +{ + if (quad_host_is_big_endian()) { + for (size_t i = 0; i < n; i++) { + dst[i] = src[n - 1 - i]; + } } else { - char buffer[128]; - int written = snprintf(buffer, sizeof(buffer), "%.*Le", - LDBL_DECIMAL_DIG - 1, self->value.longdouble_value); - if (written < 0 || written >= (int)sizeof(buffer)) { - PyErr_SetString(PyExc_RuntimeError, - "Failed to format long double for pickle"); - return NULL; - } - str_value = PyUnicode_FromString(buffer); + memcpy(dst, src, n); } - if (str_value == NULL) { +} + + +static PyObject * +QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) +{ + size_t nbytes = (self->backend == BACKEND_SLEEF) + ? sizeof(self->value.sleef_value) + : sizeof(self->value.longdouble_value); + const unsigned char *src = (self->backend == BACKEND_SLEEF) + ? (const unsigned char *)&self->value.sleef_value + : (const unsigned char *)&self->value.longdouble_value; + + unsigned char raw[sizeof(quad_value)]; + quad_copy_canonical(raw, src, nbytes); + + PyObject *data = PyBytes_FromStringAndSize((const char *)raw, (Py_ssize_t)nbytes); + if (data == NULL) { return NULL; } PyObject *backend_obj = PyUnicode_FromString( self->backend == BACKEND_SLEEF ? "sleef" : "longdouble"); if (backend_obj == NULL) { - Py_DECREF(str_value); + Py_DECREF(data); return NULL; } - PyObject *args = PyTuple_Pack(2, str_value, backend_obj); - Py_DECREF(str_value); + PyObject *module = PyImport_ImportModule("numpy_quaddtype._quaddtype_main"); + if (module == NULL) { + Py_DECREF(data); + Py_DECREF(backend_obj); + return NULL; + } + PyObject *reconstruct = PyObject_GetAttrString(module, "from_raw_bytes"); + Py_DECREF(module); + if (reconstruct == NULL) { + Py_DECREF(data); + Py_DECREF(backend_obj); + return NULL; + } + + PyObject *args = PyTuple_Pack(2, data, backend_obj); + Py_DECREF(data); Py_DECREF(backend_obj); if (args == NULL) { + Py_DECREF(reconstruct); return NULL; } - PyObject *type_obj = (PyObject *)Py_TYPE(self); - Py_INCREF(type_obj); - PyObject *result = PyTuple_Pack(2, type_obj, args); - Py_DECREF(type_obj); + PyObject *result = PyTuple_Pack(2, reconstruct, args); + Py_DECREF(reconstruct); Py_DECREF(args); return result; } +PyObject * +QuadPrecision_from_raw_bytes(PyObject *Py_UNUSED(module), PyObject *args) +{ + Py_buffer view; + const char *backend_str = "sleef"; + if (!PyArg_ParseTuple(args, "y*|s", &view, &backend_str)) { + return NULL; + } + + QuadBackendType backend = BACKEND_SLEEF; + if (strcmp(backend_str, "longdouble") == 0) { + backend = BACKEND_LONGDOUBLE; + } + else if (strcmp(backend_str, "sleef") != 0) { + PyBuffer_Release(&view); + PyErr_SetString(PyExc_ValueError, "Invalid backend. Use 'sleef' or 'longdouble'."); + return NULL; + } + + size_t expected = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double); + if (view.len != (Py_ssize_t)expected) { + PyErr_Format(PyExc_ValueError, + "QuadPrecision.from_raw_bytes expected %zu bytes for the '%s' " + "backend, got %zd", + expected, backend_str, view.len); + PyBuffer_Release(&view); + return NULL; + } + + QuadPrecisionObject *self = QuadPrecision_raw_new(backend); + if (self == NULL) { + PyBuffer_Release(&view); + return NULL; + } + unsigned char *dst = (backend == BACKEND_SLEEF) + ? (unsigned char *)&self->value.sleef_value + : (unsigned char *)&self->value.longdouble_value; + quad_copy_canonical(dst, (const unsigned char *)view.buf, expected); + PyBuffer_Release(&view); + return (PyObject *)self; +} + static PyMethodDef QuadPrecision_methods[] = { {"is_integer", (PyCFunction)QuadPrecision_is_integer, METH_NOARGS, "Return True if the value is an integer."}, {"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS, "Return a pair of integers whose ratio is exactly equal to the original value."}, {"__reduce__", (PyCFunction)QuadPrecision_reduce, METH_NOARGS, - "Support pickling: return (QuadPrecision, (str_value, backend))."}, + "Support pickling: return (from_raw_bytes, (raw_bytes, backend))."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/src/include/constants.hpp b/src/include/constants.hpp index 8886731..e9733f0 100644 --- a/src/include/constants.hpp +++ b/src/include/constants.hpp @@ -9,15 +9,6 @@ extern "C" { #include #include #include -#include - -/* LDBL_DECIMAL_DIG: minimum decimal digits needed for a lossless - * long-double → string → long-double round-trip. Standard C11 constant in - * , but MSVC omits it. On MSVC long double is the same width as - * double, so DBL_DECIMAL_DIG (17) is the exact correct fallback. */ -#ifndef LDBL_DECIMAL_DIG -# define LDBL_DECIMAL_DIG DBL_DECIMAL_DIG -#endif // Quad precision constants using sleef_q macro #define QUAD_PRECISION_ZERO sleef_q(+0x0000000000000LL, 0x0000000000000000ULL, -16383) From 85074c16171d0bd022bc9ccc266cc925de582392 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Thu, 16 Jul 2026 15:20:15 +0530 Subject: [PATCH 07/14] adding tests --- tests/test_quaddtype.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 76350f6..1dd0b6f 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5689,6 +5689,73 @@ def test_pickle_scalar_in_list(self): assert math.isnan(float(loaded[2])) assert loaded[3] == original[3] + @staticmethod + def _raw_bytes(q): + """The exact on-the-wire payload used by __reduce__ (bit pattern).""" + return q.__reduce__()[1][0] + + def test_pickle_scalar_extreme_values_roundtrip(self): + """Subnormals and values near the maximum are exactly where a decimal + string round-trip would lose bits; the raw-bytes path must preserve + them exactly.""" + import pickle + extremes = [ + numpy_quaddtype.smallest_subnormal, + numpy_quaddtype.smallest_normal, + numpy_quaddtype.smallest_subnormal * QuadPrecision("13.0"), + numpy_quaddtype.max_value, + -numpy_quaddtype.max_value, + numpy_quaddtype.epsilon, + ] + for original in extremes: + loaded = pickle.loads(pickle.dumps(original)) + assert isinstance(loaded, QuadPrecision) + assert self._raw_bytes(loaded) == self._raw_bytes(original) + assert loaded == original + + def test_pickle_scalar_raw_bit_fuzz(self): + """Fuzz the entire 128-bit space (subnormals, inf, NaN payloads, values + near overflow) and assert every value survives a pickle round-trip + bit-for-bit. Uses the new from_raw_bytes reconstructor to synthesize + arbitrary bit patterns that decimal fuzzing cannot reach.""" + import pickle + import random + from numpy_quaddtype._quaddtype_main import from_raw_bytes + + nbytes = len(self._raw_bytes(QuadPrecision("1.0", backend="sleef"))) + rng = random.Random(0xC0FFEE) + for _ in range(4000): + raw = bytes(rng.randrange(256) for _ in range(nbytes)) + original = from_raw_bytes(raw, "sleef") + loaded = pickle.loads(pickle.dumps(original)) + # Compare bit patterns directly so NaNs (which are != themselves) + # are also checked. + assert self._raw_bytes(loaded) == self._raw_bytes(original) + + def test_from_raw_bytes_roundtrip_matches_reduce(self): + """from_raw_bytes is the inverse of the __reduce__ payload for both + backends, including the little-endian canonicalization.""" + from numpy_quaddtype._quaddtype_main import from_raw_bytes + for backend in ("sleef", "longdouble"): + original = QuadPrecision("3.14159265358979323846264338327950288", + backend=backend) + _, (data, be) = original.__reduce__() + assert be == backend + rebuilt = from_raw_bytes(data, backend) + assert self._raw_bytes(rebuilt) == self._raw_bytes(original) + assert rebuilt == original + + def test_from_raw_bytes_rejects_wrong_length(self): + from numpy_quaddtype._quaddtype_main import from_raw_bytes + with pytest.raises(ValueError): + from_raw_bytes(b"\x00" * 3, "sleef") + + def test_from_raw_bytes_rejects_bad_backend(self): + from numpy_quaddtype._quaddtype_main import from_raw_bytes + good = QuadPrecision("1.0", backend="sleef").__reduce__()[1][0] + with pytest.raises(ValueError): + from_raw_bytes(good, "float128") + @pytest.mark.parametrize("dtype", [ "bool", From f0b4405a659565ca7a5d4ab80869212e9d87efd8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Thu, 16 Jul 2026 15:20:55 +0530 Subject: [PATCH 08/14] just a dev note for the sleef_strtoq finding --- src/csrc/utilities.c | 35 +++++++++++++++++++++++++++++++++++ src/include/scalar.h | 3 +++ 2 files changed, 38 insertions(+) diff --git a/src/csrc/utilities.c b/src/csrc/utilities.c index 33f0798..2852946 100644 --- a/src/csrc/utilities.c +++ b/src/csrc/utilities.c @@ -139,6 +139,41 @@ cstring_to_quad_internal(const char *str, const char *start, QuadBackendType bac temp[len] = '\0'; // Call Sleef_strtoq with the bounded string + // + // DEVELOPER NOTE - Sleef_strtoq decimal precision limit. + // The decimal branch of Sleef_strtoq is NOT correctly-rounded. It + // accumulates digits (n = n*10 + d) into SLEEF's triple-double "tdx" + // format (a 64-bit exponent + vdouble3 mantissa, ~159 mantissa bits), + // scales by exp10i() and rounds via cast_vq_tdx(). Only the hex (0x...) + // branch is exact bit-manipulation. + // + // Three different digit counts are involved - do NOT conflate them: + // * ~34 = a quad's actual precision (113 * log10(2)) + // * 36 = digits that GUARANTEE any quad round-trips; this is + // SLEEF_QUAD_DECIMAL_DIG and the most Dragon4 (str/repr) emits + // * ~48 = the tdx mantissa's raw capacity (159 bits / log2(10)) + // * ~45 = the EFFECTIVE window this parser resolves correctly - a few + // digits below 48 because the tdx add/mul/div/exp10i are not + // perfectly rounded. Property of the parser, not of quads. + // + // ~45 is NOT a hard/coded limit: it is a soft, build-dependent threshold, + // and it is measured (empirical), not read from a constant. Sweeping + // genuine quads across the exponent range, the smallest k at which + // nudging a half-ULP midpoint by 10^-k gets silently dropped (rounds the + // wrong way) was 45 (some values tolerate 46-48). Inputs longer than the + // window are still read - the extra digits just stop reliably affecting + // the result. So if the digit that decides rounding sits at position + // >= ~46 - e.g. a value just above a midpoint - the result can be 1 ULP + // off. Concrete repro + // (SLEEF 3.9.0, x86): the ~46-digit-deep value near + // "1.99999...9997111...e0" parses to ...ffffe, not the correct ...fffff. + // + // Practical impact on quad<->string round-trips: none here. Every quad + // is captured by <= 36 digits, ~9 below the ~45 window, so Dragon4 + // output re-parses exactly. BUT that ~45 is FMA-/codegen-dependent (the + // tdx arithmetic can lose a guard bit on some toolchains, e.g. macOS) + // and can shrink toward 36 - so do not rely on string parsing for + // exactness. Binary paths (raw bytes) are always exact. char *sleef_endptr; out_value->sleef_value = Sleef_strtoq(temp, &sleef_endptr); free(temp); diff --git a/src/include/scalar.h b/src/include/scalar.h index 74a6896..d5310cc 100644 --- a/src/include/scalar.h +++ b/src/include/scalar.h @@ -24,6 +24,9 @@ QuadPrecision_raw_new(QuadBackendType backend); QuadPrecisionObject * QuadPrecision_from_object(PyObject *value, QuadBackendType backend); +PyObject * +QuadPrecision_from_raw_bytes(PyObject *module, PyObject *args); + int init_quadprecision_scalar(void); From 7574adac110940f08966d3578cd8e76ce0c33a1b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 17 Jul 2026 19:09:52 +0530 Subject: [PATCH 09/14] pad the garbage bytes with 0 --- src/csrc/scalar.c | 3 +++ tests/test_quaddtype.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index f39686a..d78b54e 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -3,6 +3,7 @@ #include #include #include +#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION @@ -40,6 +41,8 @@ QuadPrecision_raw_new(QuadBackendType backend) new->value.sleef_value = Sleef_cast_from_doubleq1(0.0); } else { + // An 80-bit long double occupies 16 bytes but writes only 10; + memset(&new->value, 0, sizeof(new->value)); new->value.longdouble_value = 0.0L; } return new; diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 1dd0b6f..93881b0 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5756,6 +5756,22 @@ def test_from_raw_bytes_rejects_bad_backend(self): with pytest.raises(ValueError): from_raw_bytes(good, "float128") + def test_pickle_scalar_longdouble_padding_zeroed(self): + """An 80-bit long double holds 10 significant bytes in a 16-byte slot; + the 6 padding bytes must be zero-initialized so the pickled payload is + deterministic and never leaks allocator garbage.""" + from numpy_quaddtype._quaddtype_main import from_raw_bytes + raw = self._raw_bytes(QuadPrecision("1.0", backend="longdouble")) + if len(raw) != 16 or numpy_quaddtype.is_longdouble_128(): + pytest.skip("no padding: long double is not 80-bit-in-16-bytes here") + # Poison the allocator free list with objects whose padding is 0xFF, so a + # reused object slot would expose garbage if raw_new didn't clear it. + junk = [from_raw_bytes(b"\xff" * 16, "longdouble") for _ in range(64)] + del junk + raw2 = self._raw_bytes(QuadPrecision("1.0", backend="longdouble")) + assert raw2[10:] == b"\x00" * 6, raw2.hex() + assert raw2 == raw + @pytest.mark.parametrize("dtype", [ "bool", From ee40e6d780e46635a23295562bc6ae2487ff43f5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 17 Jul 2026 19:24:36 +0530 Subject: [PATCH 10/14] tag ldbl and error in incompat platform --- src/csrc/quaddtype_main.c | 6 ++++-- src/csrc/scalar.c | 34 +++++++++++++++++++++++++++++++--- tests/test_quaddtype.py | 26 +++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/csrc/quaddtype_main.c b/src/csrc/quaddtype_main.c index 3fed41a..c15b864 100644 --- a/src/csrc/quaddtype_main.c +++ b/src/csrc/quaddtype_main.c @@ -69,8 +69,10 @@ static PyMethodDef module_methods[] = { {"is_longdouble_128", py_is_longdouble_128, METH_NOARGS, "Check if long double is 128-bit"}, {"get_sleef_constant", get_sleef_constant, METH_VARARGS, "Get Sleef constant by name"}, {"from_raw_bytes", QuadPrecision_from_raw_bytes, METH_VARARGS, - "Reconstruct a QuadPrecision scalar from its raw little-endian bytes " - "(used by pickle)."}, + "from_raw_bytes(data, backend='sleef', ld_format=-1): reconstruct a " + "QuadPrecision scalar from its raw little-endian bytes (used by pickle). " + "For the 'longdouble' backend ld_format is the source LDBL_MANT_DIG and, " + "if given, must match this platform's."}, {"set_num_threads", py_quadblas_set_num_threads, METH_VARARGS, "Set number of threads for QuadBLAS"}, {"get_num_threads", py_quadblas_get_num_threads, METH_NOARGS, diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index d78b54e..86d4363 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -4,6 +4,7 @@ #include #include #include +#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION @@ -684,7 +685,22 @@ QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored)) return NULL; } - PyObject *args = PyTuple_Pack(2, data, backend_obj); + PyObject *args; + if (self->backend == BACKEND_LONGDOUBLE) { + // Tag longdouble payloads with the platform's LDBL_MANT_DIG + PyObject *fmt = PyLong_FromLong((long)LDBL_MANT_DIG); + if (fmt == NULL) { + Py_DECREF(data); + Py_DECREF(backend_obj); + Py_DECREF(reconstruct); + return NULL; + } + args = PyTuple_Pack(3, data, backend_obj, fmt); + Py_DECREF(fmt); + } + else { + args = PyTuple_Pack(2, data, backend_obj); + } Py_DECREF(data); Py_DECREF(backend_obj); if (args == NULL) { @@ -703,7 +719,8 @@ QuadPrecision_from_raw_bytes(PyObject *Py_UNUSED(module), PyObject *args) { Py_buffer view; const char *backend_str = "sleef"; - if (!PyArg_ParseTuple(args, "y*|s", &view, &backend_str)) { + int ld_format = -1; // source LDBL_MANT_DIG for longdouble; -1 = not provided + if (!PyArg_ParseTuple(args, "y*|si", &view, &backend_str, &ld_format)) { return NULL; } @@ -727,6 +744,17 @@ QuadPrecision_from_raw_bytes(PyObject *Py_UNUSED(module), PyObject *args) return NULL; } + if (backend == BACKEND_LONGDOUBLE && ld_format != -1 && + ld_format != LDBL_MANT_DIG) { + PyErr_Format(PyExc_ValueError, + "Cannot unpickle a 'longdouble' QuadPrecision created on a " + "platform with a different long double format " + "(LDBL_MANT_DIG=%d) than this one (LDBL_MANT_DIG=%d).", + ld_format, (int)LDBL_MANT_DIG); + PyBuffer_Release(&view); + return NULL; + } + QuadPrecisionObject *self = QuadPrecision_raw_new(backend); if (self == NULL) { PyBuffer_Release(&view); @@ -746,7 +774,7 @@ static PyMethodDef QuadPrecision_methods[] = { {"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS, "Return a pair of integers whose ratio is exactly equal to the original value."}, {"__reduce__", (PyCFunction)QuadPrecision_reduce, METH_NOARGS, - "Support pickling: return (from_raw_bytes, (raw_bytes, backend))."}, + "Support pickling: return (from_raw_bytes, (raw_bytes, backend[, ld_format]))."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 93881b0..d298a44 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -5739,9 +5739,9 @@ def test_from_raw_bytes_roundtrip_matches_reduce(self): for backend in ("sleef", "longdouble"): original = QuadPrecision("3.14159265358979323846264338327950288", backend=backend) - _, (data, be) = original.__reduce__() - assert be == backend - rebuilt = from_raw_bytes(data, backend) + _, reduce_args = original.__reduce__() + assert reduce_args[1] == backend + rebuilt = from_raw_bytes(*reduce_args) assert self._raw_bytes(rebuilt) == self._raw_bytes(original) assert rebuilt == original @@ -5772,6 +5772,26 @@ def test_pickle_scalar_longdouble_padding_zeroed(self): assert raw2[10:] == b"\x00" * 6, raw2.hex() assert raw2 == raw + def test_reduce_longdouble_carries_format_tag(self): + """longdouble __reduce__ carries a format tag (LDBL_MANT_DIG) so a + cross-format pickle can be rejected; sleef (always binary128) omits it.""" + _, sl_args = QuadPrecision("1.5", backend="sleef").__reduce__() + _, ld_args = QuadPrecision("1.5", backend="longdouble").__reduce__() + assert len(sl_args) == 2 and sl_args[1] == "sleef" + assert len(ld_args) == 3 and ld_args[1] == "longdouble" + assert isinstance(ld_args[2], int) + + def test_from_raw_bytes_rejects_wrong_longdouble_format(self): + """A longdouble payload tagged with a different platform's format must be + rejected, not silently reinterpreted.""" + from numpy_quaddtype._quaddtype_main import from_raw_bytes + _, (data, be, fmt) = QuadPrecision("1.5", backend="longdouble").__reduce__() + with pytest.raises(ValueError): + from_raw_bytes(data, "longdouble", fmt + 1) + # The matching tag (and omitting it) still reconstruct fine. + assert from_raw_bytes(data, "longdouble", fmt) == QuadPrecision("1.5", backend="longdouble") + assert from_raw_bytes(data, "longdouble") == QuadPrecision("1.5", backend="longdouble") + @pytest.mark.parametrize("dtype", [ "bool", From a574c02bdae9b30fe4e1e77f40de644a3d0f3e11 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 17 Jul 2026 20:54:41 +0530 Subject: [PATCH 11/14] trim devnote --- src/csrc/utilities.c | 44 +++++++++----------------------------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/src/csrc/utilities.c b/src/csrc/utilities.c index 2852946..dea2e50 100644 --- a/src/csrc/utilities.c +++ b/src/csrc/utilities.c @@ -138,42 +138,16 @@ cstring_to_quad_internal(const char *str, const char *start, QuadBackendType bac memcpy(temp, str, len); temp[len] = '\0'; - // Call Sleef_strtoq with the bounded string + // Call Sleef_strtoq with the bounded string. // - // DEVELOPER NOTE - Sleef_strtoq decimal precision limit. - // The decimal branch of Sleef_strtoq is NOT correctly-rounded. It - // accumulates digits (n = n*10 + d) into SLEEF's triple-double "tdx" - // format (a 64-bit exponent + vdouble3 mantissa, ~159 mantissa bits), - // scales by exp10i() and rounds via cast_vq_tdx(). Only the hex (0x...) - // branch is exact bit-manipulation. - // - // Three different digit counts are involved - do NOT conflate them: - // * ~34 = a quad's actual precision (113 * log10(2)) - // * 36 = digits that GUARANTEE any quad round-trips; this is - // SLEEF_QUAD_DECIMAL_DIG and the most Dragon4 (str/repr) emits - // * ~48 = the tdx mantissa's raw capacity (159 bits / log2(10)) - // * ~45 = the EFFECTIVE window this parser resolves correctly - a few - // digits below 48 because the tdx add/mul/div/exp10i are not - // perfectly rounded. Property of the parser, not of quads. - // - // ~45 is NOT a hard/coded limit: it is a soft, build-dependent threshold, - // and it is measured (empirical), not read from a constant. Sweeping - // genuine quads across the exponent range, the smallest k at which - // nudging a half-ULP midpoint by 10^-k gets silently dropped (rounds the - // wrong way) was 45 (some values tolerate 46-48). Inputs longer than the - // window are still read - the extra digits just stop reliably affecting - // the result. So if the digit that decides rounding sits at position - // >= ~46 - e.g. a value just above a midpoint - the result can be 1 ULP - // off. Concrete repro - // (SLEEF 3.9.0, x86): the ~46-digit-deep value near - // "1.99999...9997111...e0" parses to ...ffffe, not the correct ...fffff. - // - // Practical impact on quad<->string round-trips: none here. Every quad - // is captured by <= 36 digits, ~9 below the ~45 window, so Dragon4 - // output re-parses exactly. BUT that ~45 is FMA-/codegen-dependent (the - // tdx arithmetic can lose a guard bit on some toolchains, e.g. macOS) - // and can shrink toward 36 - so do not rely on string parsing for - // exactness. Binary paths (raw bytes) are always exact. + // NOTE: SLEEF's decimal strtoq is only non-correctly-rounded for inputs + // whose *significant* digits exceed what a quad can hold (>~45); there it + // may be <= 1 ULP off. This is unreachable for quad<->string round-trips: + // every quad is exact within SLEEF_QUAD_DECIMAL_DIG (36) significant + // digits and Dragon4 (str/repr) emits <= 36, so re-parsing is exact. Only + // significant digits count - magnitude/exponent (e.g. 1e4932) is scaled + // separately and is fine. Pickling uses raw bytes (from_raw_bytes), which + // never goes through this path. char *sleef_endptr; out_value->sleef_value = Sleef_strtoq(temp, &sleef_endptr); free(temp); From a2b59fc79432e282acf7e7a00ed4c41c6bbfbadb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 17 Jul 2026 21:00:10 +0530 Subject: [PATCH 12/14] remove 3.13t wheels --- .github/workflows/build_wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index ac2fb57..fdd3f10 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -26,7 +26,7 @@ jobs: - name: Build wheels env: - CIBW_BUILD: "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp313t-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64" + CIBW_BUILD: "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64" CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_BUILD_VERBOSITY: "3" @@ -88,7 +88,7 @@ jobs: - name: Build wheels env: - CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp313t-* cp314t-*" + CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*" CIBW_ENABLE: cpython-prerelease cpython-freethreading # CIBW_ARCHS_MACOS: ${{ matrix.os == 'macos-13' && 'x86_64' || 'arm64' }} CIBW_BUILD_VERBOSITY: "3" @@ -149,7 +149,7 @@ jobs: - name: Build wheels env: - CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp313t-* cp314t-*" + CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*" CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_ARCHS_WINDOWS: ${{ matrix.architecture == 'x86' && 'x86' || 'AMD64' }} CIBW_BUILD_VERBOSITY: "3" From f582c0ffdc8731f5417953704598d506bdcae76d Mon Sep 17 00:00:00 2001 From: Swayam Date: Thu, 23 Jul 2026 19:43:35 +0530 Subject: [PATCH 13/14] Update src/csrc/scalar.c Co-authored-by: Nathan Goldbaum --- src/csrc/scalar.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 86d4363..d66fb03 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -42,7 +42,7 @@ QuadPrecision_raw_new(QuadBackendType backend) new->value.sleef_value = Sleef_cast_from_doubleq1(0.0); } else { - // An 80-bit long double occupies 16 bytes but writes only 10; + // An 80-bit long double occupies 16 bytes but writes only 10 memset(&new->value, 0, sizeof(new->value)); new->value.longdouble_value = 0.0L; } From a8218971bbc5fcef258320be83e50b4119e075e2 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Thu, 23 Jul 2026 19:56:10 +0530 Subject: [PATCH 14/14] fix msg --- src/csrc/scalar.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index d66fb03..82a3057 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -737,7 +737,7 @@ QuadPrecision_from_raw_bytes(PyObject *Py_UNUSED(module), PyObject *args) size_t expected = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double); if (view.len != (Py_ssize_t)expected) { PyErr_Format(PyExc_ValueError, - "QuadPrecision.from_raw_bytes expected %zu bytes for the '%s' " + "from_raw_bytes expected %zu bytes for the '%s' " "backend, got %zd", expected, backend_str, view.len); PyBuffer_Release(&view);