Skip to content
Merged
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
80 changes: 58 additions & 22 deletions quaddtype/numpy_quaddtype/src/scalar.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,52 @@ QuadPrecision_raw_new(QuadBackendType backend)
return new;
}

static QuadPrecisionObject *
quad_from_py_int(PyObject *py_int, QuadBackendType backend, QuadPrecisionObject *self_to_cleanup)
{
int overflow = 0;
long long lval = PyLong_AsLongLongAndOverflow(py_int, &overflow);

if (overflow != 0) {
// Integer is too large, convert to string and recursively call QuadPrecision_from_object
PyObject *str_obj = PyObject_Str(py_int);
if (str_obj == NULL) {
if (self_to_cleanup) {
Py_DECREF(self_to_cleanup);
}
return NULL;
}

QuadPrecisionObject *result = QuadPrecision_from_object(str_obj, backend);
Py_DECREF(str_obj);
if (self_to_cleanup) {
Py_DECREF(self_to_cleanup); // discard the default one
}
return result;
}
else if (lval == -1 && PyErr_Occurred()) {
if (self_to_cleanup) {
Py_DECREF(self_to_cleanup);
}
return NULL;
}

// No overflow, use the integer value directly
QuadPrecisionObject *self = self_to_cleanup ? self_to_cleanup : QuadPrecision_raw_new(backend);
if (!self) {
return NULL;
}

if (backend == BACKEND_SLEEF) {
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
}
else {
self->value.longdouble_value = (long double)lval;
}
return self;

}

QuadPrecisionObject *
QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
{
Expand Down Expand Up @@ -76,16 +122,10 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
Py_DECREF(self);
return NULL;
}
long long lval = PyLong_AsLongLong(py_int);
Py_DECREF(py_int);

if (backend == BACKEND_SLEEF) {
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
}
else {
self->value.longdouble_value = (long double)lval;
}
return self;
QuadPrecisionObject *result = quad_from_py_int(py_int, backend, self);
Py_DECREF(py_int);
return result;
}
// Try as boolean
else if (PyArray_IsScalar(value, Bool)) {
Expand All @@ -94,9 +134,16 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
Py_DECREF(self);
return NULL;
}

// Booleans are always 0 or 1, so no overflow check needed
long long lval = PyLong_AsLongLong(py_int);
Py_DECREF(py_int);

if (lval == -1 && PyErr_Occurred()) {
Py_DECREF(self);
return NULL;
}

if (backend == BACKEND_SLEEF) {
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
}
Expand Down Expand Up @@ -145,7 +192,7 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
self->value.longdouble_value = (long double)dval;
}
}
else if (PyUnicode_CheckExact(value)) {
else if (PyUnicode_Check(value)) {
const char *s = PyUnicode_AsUTF8(value);
char *endptr = NULL;
if (backend == BACKEND_SLEEF) {
Expand All @@ -161,18 +208,7 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
}
}
else if (PyLong_Check(value)) {
long long val = PyLong_AsLongLong(value);
if (val == -1 && PyErr_Occurred()) {
PyErr_SetString(PyExc_OverflowError, "Overflow Error, value out of range");
Py_DECREF(self);
return NULL;
}
if (backend == BACKEND_SLEEF) {
self->value.sleef_value = Sleef_cast_from_int64q1(val);
}
else {
self->value.longdouble_value = (long double)val;
}
return quad_from_py_int(value, backend, self);
}
else if (Py_TYPE(value) == &QuadPrecision_Type) {
Py_DECREF(self); // discard the default one
Expand Down
122 changes: 122 additions & 0 deletions quaddtype/tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,66 @@ def test_create_scalar_simple():
assert isinstance(QuadPrecision(1), QuadPrecision)


@pytest.mark.parametrize("int_val", [
# Very large integers that exceed long double range
2 ** 1024,
2 ** 2048,
10 ** 308,
10 ** 4000,
# Edge cases
0,
1,
-1,
# Negative large integers
-(2 ** 1024),
])
def test_create_scalar_from_large_int(int_val):
"""Test that QuadPrecision can handle very large integers beyond long double range.

This test ensures that integers like 2**1024, which overflow standard long double,
are properly converted via string representation to QuadPrecision without raising
overflow errors. The conversion should match the string-based conversion.
"""
# Convert large int to QuadPrecision
result = QuadPrecision(int_val)
assert isinstance(result, QuadPrecision)

# String conversion should give the same result
str_val = str(int_val)
result_from_str = QuadPrecision(str_val)

# Both conversions should produce the same value
# (can be inf==inf on some platforms for very large values)
assert result == result_from_str

# For zero and small values, verify exact conversion
if int_val == 0:
assert float(result) == 0.0
elif abs(int_val) == 1:
assert float(result) == float(int_val)


def test_create_scalar_from_int_with_broken_str():
"""Test that QuadPrecision handles errors when __str__ fails on large integers.

This test checks the error handling path in scalar.c where PyObject_Str(py_int)
returns NULL. We simulate this by subclassing int with a __str__ method
that raises an exception.
"""
class BrokenInt(int):
def __str__(self):
raise RuntimeError("Intentionally broken __str__ method")

# Create an instance with a value that will overflow long long (> 2**63 - 1)
# This triggers the string conversion path in quad_from_py_int
broken_int = BrokenInt(2 ** 1024)

# When PyLong_AsLongLongAndOverflow returns overflow,
# it tries to convert to string, which should fail and propagate the error
with pytest.raises(RuntimeError, match="Intentionally broken __str__ method"):
QuadPrecision(broken_int)


class TestQuadPrecisionArrayCreation:
"""Test suite for QuadPrecision array creation from sequences and arrays."""

Expand Down Expand Up @@ -248,6 +308,68 @@ def test_string_roundtrip():
)


def test_string_subclass_parsing():
"""Test that QuadPrecision handles string subclasses correctly.

This tests the PyUnicode_Check path in scalar.c lines 195-209,
verifying that string subclasses work and that parsing errors
are properly handled.
"""
class MyString(str):
"""A custom string subclass"""
pass

# Test valid string subclass - should parse correctly
valid_str = MyString("3.14159265358979323846")
result = QuadPrecision(valid_str)
assert isinstance(result, QuadPrecision)
expected = QuadPrecision("3.14159265358979323846")
assert result == expected

# Test with scientific notation
sci_str = MyString("1.23e-100")
result = QuadPrecision(sci_str)
assert isinstance(result, QuadPrecision)

# Test with negative value
neg_str = MyString("-42.5")
result = QuadPrecision(neg_str)
assert float(result) == -42.5

# Test invalid string - should raise ValueError
invalid_str = MyString("not a number")
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
QuadPrecision(invalid_str)

# Test partially valid string (has trailing garbage)
partial_str = MyString("3.14abc")
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
QuadPrecision(partial_str)

# Test empty string
empty_str = MyString("")
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
QuadPrecision(empty_str)

# Test string with leading garbage
leading_garbage = MyString("abc3.14")
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
QuadPrecision(leading_garbage)

# Test special values
inf_str = MyString("inf")
result = QuadPrecision(inf_str)
assert np.isinf(float(result))

neg_inf_str = MyString("-inf")
result = QuadPrecision(neg_inf_str)
assert np.isinf(float(result)) and float(result) < 0

nan_str = MyString("nan")
result = QuadPrecision(nan_str)
assert np.isnan(float(result))


@pytest.mark.parametrize("name,expected", [("pi", np.pi), ("e", np.e), ("log2e", np.log2(np.e)), ("log10e", np.log10(np.e)), ("ln2", np.log(2.0)), ("ln10", np.log(10.0))])
def test_math_constant(name, expected):
assert isinstance(getattr(numpy_quaddtype, name), QuadPrecision)
Expand Down