Skip to content

Commit 5b69baa

Browse files
committed
added __reduce__ + exp_digits = 4 + tests
1 parent a6885f7 commit 5b69baa

3 files changed

Lines changed: 163 additions & 2 deletions

File tree

src/csrc/casts.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars)
416416
// Use scientific notation with full precision
417417
const char *scientific_str = Dragon4_Scientific_QuadDType_CStr(sleef_val, DigitMode_Unique,
418418
SLEEF_QUAD_DECIMAL_DIG, 0, 1,
419-
TrimMode_LeaveOneZero, 1, 2);
419+
TrimMode_LeaveOneZero, 1, 4);
420420
if (scientific_str == NULL) {
421421
PyErr_SetString(PyExc_RuntimeError, "Float formatting failed");
422422
return NULL;

src/csrc/scalar.c

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ QuadPrecision_repr_dragon4(QuadPrecisionObject *self)
352352
.sign = 1,
353353
.trim_mode = TrimMode_LeaveOneZero,
354354
.digits_left = 1,
355-
.exp_digits = 3};
355+
.exp_digits = 4};
356356

357357
PyObject *str;
358358
if (self->backend == BACKEND_SLEEF) {
@@ -601,11 +601,63 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig
601601
return PyTuple_Pack(2, numerator, denominator);
602602
}
603603

604+
static PyObject *
605+
QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored))
606+
{
607+
Dragon4_Options opt = {.scientific = 1,
608+
.digit_mode = DigitMode_Unique,
609+
.cutoff_mode = CutoffMode_TotalLength,
610+
.precision = SLEEF_QUAD_DECIMAL_DIG,
611+
.sign = 1,
612+
.trim_mode = TrimMode_LeaveOneZero,
613+
.digits_left = 1,
614+
.exp_digits = 4};
615+
616+
PyObject *str_value;
617+
if (self->backend == BACKEND_SLEEF) {
618+
str_value = Dragon4_Scientific_QuadDType(&self->value.sleef_value, opt.digit_mode,
619+
opt.precision, opt.min_digits, opt.sign,
620+
opt.trim_mode, opt.digits_left, opt.exp_digits);
621+
}
622+
else {
623+
Sleef_quad sleef_val = Sleef_cast_from_doubleq1(self->value.longdouble_value);
624+
str_value = Dragon4_Scientific_QuadDType(&sleef_val, opt.digit_mode, opt.precision,
625+
opt.min_digits, opt.sign, opt.trim_mode,
626+
opt.digits_left, opt.exp_digits);
627+
}
628+
if (str_value == NULL) {
629+
return NULL;
630+
}
631+
632+
PyObject *backend_obj = PyUnicode_FromString(
633+
self->backend == BACKEND_SLEEF ? "sleef" : "longdouble");
634+
if (backend_obj == NULL) {
635+
Py_DECREF(str_value);
636+
return NULL;
637+
}
638+
639+
PyObject *args = PyTuple_Pack(2, str_value, backend_obj);
640+
Py_DECREF(str_value);
641+
Py_DECREF(backend_obj);
642+
if (args == NULL) {
643+
return NULL;
644+
}
645+
646+
PyObject *type_obj = (PyObject *)Py_TYPE(self);
647+
Py_INCREF(type_obj);
648+
PyObject *result = PyTuple_Pack(2, type_obj, args);
649+
Py_DECREF(type_obj);
650+
Py_DECREF(args);
651+
return result;
652+
}
653+
604654
static PyMethodDef QuadPrecision_methods[] = {
605655
{"is_integer", (PyCFunction)QuadPrecision_is_integer, METH_NOARGS,
606656
"Return True if the value is an integer."},
607657
{"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS,
608658
"Return a pair of integers whose ratio is exactly equal to the original value."},
659+
{"__reduce__", (PyCFunction)QuadPrecision_reduce, METH_NOARGS,
660+
"Support pickling: return (QuadPrecision, (str_value, backend))."},
609661
{NULL, NULL, 0, NULL} /* Sentinel */
610662
};
611663

tests/test_quaddtype.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5338,6 +5338,115 @@ def test_pickle_fortran_order(self, backend):
53385338
assert unpickled.dtype == original.dtype
53395339
assert unpickled.flags.f_contiguous == original.flags.f_contiguous
53405340

5341+
5342+
class TestScalarPickle:
5343+
"""Regression tests for issue #99: bare QuadPrecision scalars (not wrapped
5344+
in an array) must round-trip through pickle.dumps / pickle.loads without
5345+
raising and must preserve value, type, and backend."""
5346+
5347+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5348+
def test_pickle_scalar_issue_repro(self, backend):
5349+
"""The exact repro from #99: was RuntimeError on loads()."""
5350+
import pickle
5351+
original = QuadPrecision("123.456", backend=backend)
5352+
loaded = pickle.loads(pickle.dumps(original))
5353+
assert isinstance(loaded, QuadPrecision)
5354+
assert loaded == original
5355+
assert str(loaded) == str(original)
5356+
5357+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5358+
@pytest.mark.parametrize("value", [
5359+
"0.0", "-0.0", "1.0", "-1.0", "42.0", "-42.0",
5360+
"3.141592653589793238462643383279502884197", # ~quad-precision pi
5361+
"2.718281828459045235360287471352662497757",
5362+
"1e100", "1e-100", "-1e100", "-1e-100",
5363+
"1.23456789012345678901234567890e30",
5364+
])
5365+
def test_pickle_scalar_finite_roundtrip(self, backend, value):
5366+
"""Finite values must round-trip exactly (Dragon4-Unique is lossless)."""
5367+
import pickle
5368+
original = QuadPrecision(value, backend=backend)
5369+
loaded = pickle.loads(pickle.dumps(original))
5370+
assert isinstance(loaded, QuadPrecision)
5371+
assert loaded.dtype == QuadPrecDType(backend=backend)
5372+
# Exact equality: pickle/unpickle should not lose any bits.
5373+
assert loaded == original
5374+
# And the canonical repr should match.
5375+
assert str(loaded) == str(original)
5376+
5377+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5378+
def test_pickle_scalar_inf(self, backend):
5379+
import pickle
5380+
for s in ["inf", "-inf"]:
5381+
original = QuadPrecision(s, backend=backend)
5382+
loaded = pickle.loads(pickle.dumps(original))
5383+
assert isinstance(loaded, QuadPrecision)
5384+
assert loaded == original # inf == inf, -inf == -inf
5385+
assert float(loaded) == float(original)
5386+
5387+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5388+
def test_pickle_scalar_nan(self, backend):
5389+
import pickle
5390+
original = QuadPrecision("nan", backend=backend)
5391+
loaded = pickle.loads(pickle.dumps(original))
5392+
assert isinstance(loaded, QuadPrecision)
5393+
# NaN != NaN, so we check isnan instead.
5394+
import math
5395+
assert math.isnan(float(loaded))
5396+
assert loaded.dtype == QuadPrecDType(backend=backend)
5397+
5398+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5399+
@pytest.mark.parametrize("protocol", [0, 1, 2, 3, 4, 5])
5400+
def test_pickle_scalar_all_protocols(self, backend, protocol):
5401+
"""Round-trip must work across every pickle protocol version."""
5402+
import pickle
5403+
original = QuadPrecision("3.14159265358979323846", backend=backend)
5404+
data = pickle.dumps(original, protocol=protocol)
5405+
loaded = pickle.loads(data)
5406+
assert isinstance(loaded, QuadPrecision)
5407+
assert loaded == original
5408+
assert loaded.dtype == QuadPrecDType(backend=backend)
5409+
5410+
def test_pickle_scalar_preserves_type(self):
5411+
import pickle
5412+
loaded = pickle.loads(pickle.dumps(QuadPrecision("1.0")))
5413+
assert type(loaded) is QuadPrecision
5414+
5415+
def test_pickle_scalar_preserves_backend_across_mix(self):
5416+
"""Each backend pickle must come back as the same backend, not silently
5417+
defaulting to sleef."""
5418+
import pickle
5419+
ld = QuadPrecision("1.5", backend="longdouble")
5420+
sl = QuadPrecision("1.5", backend="sleef")
5421+
ld_loaded = pickle.loads(pickle.dumps(ld))
5422+
sl_loaded = pickle.loads(pickle.dumps(sl))
5423+
assert ld_loaded.dtype == QuadPrecDType(backend="longdouble")
5424+
assert sl_loaded.dtype == QuadPrecDType(backend="sleef")
5425+
5426+
def test_pickle_scalar_in_list(self):
5427+
"""Composite container of scalars also pickles cleanly."""
5428+
import pickle
5429+
original = [QuadPrecision("1.5"), QuadPrecision("2.5"),
5430+
QuadPrecision("nan"), QuadPrecision("inf")]
5431+
loaded = pickle.loads(pickle.dumps(original))
5432+
import math
5433+
assert len(loaded) == 4
5434+
assert loaded[0] == original[0]
5435+
assert loaded[1] == original[1]
5436+
assert math.isnan(float(loaded[2]))
5437+
assert loaded[3] == original[3]
5438+
5439+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
5440+
def test_pickle_scalar_loads_does_not_raise(self, backend):
5441+
"""Direct regression on the exact failure mode in the bug report
5442+
(RuntimeError from numpy's legacy SETITEM path)."""
5443+
import pickle
5444+
try:
5445+
pickle.loads(pickle.dumps(QuadPrecision("123.456", backend=backend)))
5446+
except RuntimeError as exc:
5447+
pytest.fail(f"pickle.loads raised RuntimeError: {exc}")
5448+
5449+
53415450
@pytest.mark.parametrize("dtype", [
53425451
"bool",
53435452
"byte", "int8", "ubyte", "uint8",

0 commit comments

Comments
 (0)