Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/csrc/casts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exp_digits is the minimum width of the exponent field (zero-padded).
Dragon4 always emits every significant exponent digit regardless, so this has no effect on round-trip correctness.
e+4932 prints in full at exp_digits=2 just as at 4.
The change is purely for consistency width across all formatting paths __repr__ and __reduce__ already use exp_digits=4, and binary128 exponents span up to 4 digits (±4932), so a 4-digit field formats every value consistently. casts.cpp was the only site still at 2, which produced variable-width exponents (...e+05 vs ...e+0005) for the same dtype depending on code path so setting it to 4 makes the scientific output uniform everywhere.

if (scientific_str == NULL) {
PyErr_SetString(PyExc_RuntimeError, "Float formatting failed");
return NULL;
Expand Down
2 changes: 1 addition & 1 deletion src/csrc/dragon4.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
72 changes: 58 additions & 14 deletions src/csrc/scalar.c
Original file line number Diff line number Diff line change
Expand Up @@ -335,19 +335,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)
{
Expand All @@ -358,7 +345,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) {
Expand Down Expand Up @@ -633,11 +620,68 @@ 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 {
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;
}

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 */
};

Expand Down
9 changes: 9 additions & 0 deletions src/include/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ extern "C" {
#include <sleefquad.h>
#include <stdint.h>
#include <string.h>
#include <float.h>

/* LDBL_DECIMAL_DIG: minimum decimal digits needed for a lossless
* long-double → string → long-double round-trip. Standard C11 constant in
* <float.h>, 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)
Expand Down
142 changes: 142 additions & 0 deletions tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -5548,6 +5581,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):
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]


@pytest.mark.parametrize("dtype", [
"bool",
"byte", "int8", "ubyte", "uint8",
Expand Down
Loading