From aebbb10a526972cc12eeb43d98dcb3c5f951cc51 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 16:38:40 +0530 Subject: [PATCH 1/2] adding tests --- src/csrc/scalar_ops.cpp | 23 +++++++++- src/include/scalar.h | 4 ++ tests/test_quaddtype.py | 98 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index 1e62c40..b8029d9 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -224,12 +224,31 @@ QuadPrecision_float(QuadPrecisionObject *self) static PyObject * QuadPrecision_int(QuadPrecisionObject *self) { + Sleef_quad value; if (self->backend == BACKEND_SLEEF) { - return PyLong_FromLongLong(Sleef_cast_to_int64q1(self->value.sleef_value)); + value = self->value.sleef_value; } else { - return PyLong_FromLongLong((long long)self->value.longdouble_value); + // Route the longdouble backend through quad as as_integer_ratio does; + // the prior `(long long)longdouble_value` cast also saturated/UBed on + // NaN/Inf/out-of-range. + value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); } + + if (Sleef_iunordq1(value, value)) { + PyErr_SetString(PyExc_ValueError, "cannot convert float NaN to integer"); + return NULL; + } + if (Sleef_icmpgeq1(Sleef_fabsq1(value), QUAD_PRECISION_INF)) { + PyErr_SetString(PyExc_OverflowError, + "cannot convert float infinity to integer"); + return NULL; + } + + // Python's int(float) truncates toward zero; Sleef_snprintf("%.0Qf") used + // by quad_to_pylong would otherwise apply round-to-nearest-even. + Sleef_quad truncated = Sleef_truncq1(value); + return quad_to_pylong(truncated); } template diff --git a/src/include/scalar.h b/src/include/scalar.h index 4afd725..bd54931 100644 --- a/src/include/scalar.h +++ b/src/include/scalar.h @@ -7,6 +7,7 @@ extern "C" { #include #include +#include #include "quad_common.h" typedef struct { @@ -26,6 +27,9 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend); int init_quadprecision_scalar(void); +PyObject * +quad_to_pylong(Sleef_quad value); + #ifdef __cplusplus } #endif diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 83ec1f8..0938a2b 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -4894,6 +4894,104 @@ def test_as_integer_ratio_compatibility_with_float(self, value): float_ratio = float_num / float_denom assert abs(quad_ratio - float_ratio) < 1e-15 +class TestIntConversion: + """Regression tests for issue #97: int(QuadPrecision(...)) must + raise on NaN/Inf, truncate toward zero, and produce arbitrary-precision + Python ints rather than saturating at INT64_MAX.""" + + # ---- NaN / Inf must raise the right exceptions ---- + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + def test_int_of_nan_raises_value_error(self, backend): + # Python: int(float('nan')) -> ValueError + with pytest.raises(ValueError, match="NaN"): + int(QuadPrecision("nan", backend=backend)) + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + @pytest.mark.parametrize("inf_str", ["inf", "-inf"]) + def test_int_of_inf_raises_overflow_error(self, backend, inf_str): + # Python: int(float('inf')) -> OverflowError + with pytest.raises(OverflowError, match="infinity"): + int(QuadPrecision(inf_str, backend=backend)) + + # ---- Truncate toward zero (NOT floor, NOT banker's rounding) ---- + + @pytest.mark.parametrize("value_str,expected", [ + ("0.0", 0), + ("-0.0", 0), + ("0.5", 0), # Python int(0.5) == 0, not 1 (banker's would give 0 here, coincidence) + ("-0.5", 0), # Python int(-0.5) == 0, not -1 + ("1.5", 1), # Python int(1.5) == 1, not 2 (banker's would give 2 — divergence) + ("-1.5", -1), # Python int(-1.5) == -1, not -2 (floor would give -2) + ("2.5", 2), # Python int(2.5) == 2, banker's would give 2 (matches) + ("3.7", 3), + ("-3.7", -3), + ("0.9999999999999", 0), + ("-0.9999999999999", 0), + ("42.0", 42), + ("-42.0", -42), + ]) + def test_int_truncates_toward_zero(self, value_str, expected): + assert int(QuadPrecision(value_str)) == expected + # Cross-check against Python's float for values float can represent exactly. + assert int(QuadPrecision(value_str)) == int(float(value_str)) + + # ---- Beyond int64: the original bug ---- + + def test_int_beyond_int64_positive(self): + # 2^63 = 9223372036854775808 — one past INT64_MAX. The old code returned + # INT64_MAX (9223372036854775807). Must now be exact. + n = 2**63 + assert int(QuadPrecision(str(n))) == n + + def test_int_beyond_int64_negative(self): + n = -(2**63) - 1 # one past INT64_MIN + assert int(QuadPrecision(str(n))) == n + + @pytest.mark.parametrize("exponent", [40, 60, 80, 100]) + def test_int_powers_of_two_far_above_int64(self, exponent): + # 2^exponent fits exactly in quad's 113-bit mantissa for exponent < 113. + n = 2 ** exponent + assert int(QuadPrecision(str(n))) == n + + def test_int_int64_max_exact(self): + m = 2**63 - 1 + assert int(QuadPrecision(str(m))) == m + + def test_int_int64_min_exact(self): + m = -(2**63) + assert int(QuadPrecision(str(m))) == m + + # ---- 1e30 from the issue ---- + + def test_int_1e30_not_saturated(self): + # The issue calls this out explicitly: int(QuadPrecision('1e30')) used to + # return INT64_MAX. It should now match what int(Decimal('1e30')) gives. + result = int(QuadPrecision("1e30")) + assert result == 10**30, f"got {result!r}, expected 10**30" + + # ---- Return type ---- + + def test_int_returns_python_int(self): + v = int(QuadPrecision("123")) + assert type(v) is int + + def test_int_of_huge_value_returns_python_int(self): + v = int(QuadPrecision("1e30")) + assert type(v) is int + assert v.bit_length() > 64 # arbitrary-precision, not a C int + + # ---- Round-trip: int -> QuadPrecision -> int ---- + + @pytest.mark.parametrize("n", [ + 0, 1, -1, 42, -42, + 2**31, 2**32, 2**62, 2**63, 2**63 + 1, 2**70, 2**100, + -(2**63), -(2**63) - 1, -(2**70), + ]) + def test_int_quad_int_roundtrip(self, n): + assert int(QuadPrecision(str(n))) == n + + def test_quadprecision_scalar_dtype_expose(): quad_ld = QuadPrecision("1e100", backend="longdouble") quad_sleef = QuadPrecision("1e100", backend="sleef") From 8b7512a0c9997479242af24817f4d2a52caba9b7 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 16:42:48 +0530 Subject: [PATCH 2/2] expose quad_to_pylong in header --- src/csrc/scalar_ops.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index b8029d9..30e2045 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -245,8 +245,6 @@ QuadPrecision_int(QuadPrecisionObject *self) return NULL; } - // Python's int(float) truncates toward zero; Sleef_snprintf("%.0Qf") used - // by quad_to_pylong would otherwise apply round-to-nearest-even. Sleef_quad truncated = Sleef_truncq1(value); return quad_to_pylong(truncated); }