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" 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/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/src/csrc/quaddtype_main.c b/src/csrc/quaddtype_main.c index 4aaa6a5..c15b864 100644 --- a/src/csrc/quaddtype_main.c +++ b/src/csrc/quaddtype_main.c @@ -68,6 +68,11 @@ 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, + "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 7606d3f..86d4363 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION @@ -40,6 +42,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; @@ -335,19 +339,6 @@ QuadPrecision_str_dragon4(QuadPrecisionObject *self) } } -static PyObject * -QuadPrecision_str(QuadPrecisionObject *self) -{ - 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); - } - return PyUnicode_FromString(buffer); -} - static PyObject * QuadPrecision_repr_dragon4(QuadPrecisionObject *self) { @@ -358,7 +349,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) { @@ -633,11 +624,157 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig return PyTuple_Pack(2, numerator, denominator); } +static int +quad_host_is_big_endian(void) +{ + uint16_t probe = 1; + return ((const unsigned char *)&probe)[0] == 0; +} + + +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 { + memcpy(dst, src, n); + } +} + + +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(data); + return NULL; + } + + 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; + 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) { + Py_DECREF(reconstruct); + return NULL; + } + + 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"; + 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; + } + + 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; + } + + 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); + 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 (from_raw_bytes, (raw_bytes, backend[, ld_format]))."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/src/csrc/utilities.c b/src/csrc/utilities.c index 33f0798..dea2e50 100644 --- a/src/csrc/utilities.c +++ b/src/csrc/utilities.c @@ -138,7 +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. + // + // 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); 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); diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 19296c0..d298a44 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.""" @@ -5548,6 +5581,218 @@ 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): + 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) + assert loaded == original + 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 + 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) + 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 + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_pickle_scalar_preserves_full_precision(self, backend): + """Compare via subtraction, not repr: two values with the same printed + repr can still differ at the full bit width.""" + import pickle + 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.""" + 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] + + @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) + _, 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 + + 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") + + 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 + + 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", "byte", "int8", "ubyte", "uint8",