diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 2be5078..31538c8 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -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) { @@ -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)) { @@ -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); } @@ -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) { @@ -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 diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 7d396e9..deeb5c9 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -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.""" @@ -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)