From aebbb10a526972cc12eeb43d98dcb3c5f951cc51 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Tue, 19 May 2026 16:38:40 +0530 Subject: [PATCH 1/9] 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/9] 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); } From 137ca63b074dec8019864d9b4af7d0bbd1d5a479 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 04:45:58 +0000 Subject: [PATCH 3/9] ld->quad + ld->long fixes + tests --- src/csrc/scalar.c | 53 ++++++++++++---- src/csrc/scalar_ops.cpp | 36 ++++++----- src/include/scalar.h | 2 + tests/test_quaddtype.py | 133 +++++++++++++++++++++++++++++++--------- 4 files changed, 168 insertions(+), 56 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index 140e768..d0ef851 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -21,6 +21,11 @@ #include "pythoncapi_compat.h" +// forward declaration +static Sleef_quad +longdouble_to_quad(long double value); + + QuadPrecisionObject * QuadPrecision_raw_new(QuadBackendType backend) { @@ -443,8 +448,7 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) value = self->value.sleef_value; } else { - // lets also tackle ld from sleef functions as well - value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + value = longdouble_to_quad(self->value.longdouble_value); } if(Sleef_iunordq1(value, value)) { @@ -462,9 +466,7 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) // Check if value equals its truncated version Sleef_quad truncated = Sleef_truncq1(value); - int32_t is_equal = Sleef_icmpeqq1(value, truncated); - - if (is_equal) { + if (Sleef_icmpeqq1(value, truncated)) { Py_RETURN_TRUE; } else { @@ -474,7 +476,7 @@ QuadPrecision_is_integer(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored) PyObject* quad_to_pylong(Sleef_quad value) { - char buffer[128]; + char buffer[4936]; // 4933 + sign + null terminator, enough for 128-bit integer in decimal // Sleef_snprintf call is thread-unsafe LOCK_SLEEF; @@ -492,6 +494,37 @@ PyObject* quad_to_pylong(Sleef_quad value) return result; } +PyObject* longdouble_to_pylong(long double value) +{ + char buffer[4936]; // 4933 + sign + null terminator, enough for 128-bit integer in decimal + + // POSIX guarantees thread-safety of snprintf + int written = snprintf(buffer, sizeof(buffer), "%.0Lf", value); + if (written < 0 || written >= sizeof(buffer)) { + PyErr_SetString(PyExc_RuntimeError, "Failed to convert long double to string"); + return NULL; + } + + // Already raises ValueError and returns NULL on failure + return PyLong_FromString(buffer, NULL, 10); +} + +static Sleef_quad +longdouble_to_quad(long double value) +{ + if (isnanl(value) || isinfl(value) || value == 0.0L) + return Sleef_cast_from_doubleq1((double)value); + + int exp; + long double mantissa = frexpl(value, &exp); + long double scaled = ldexpl(mantissa, 64); + exp -= 64; + Sleef_quad q = (scaled < 0) + ? Sleef_negq1(Sleef_cast_from_uint64q1((uint64_t)(-scaled))) + : Sleef_cast_from_uint64q1((uint64_t)scaled); + return Sleef_ldexpq1(q, exp); +} + // inspired by the CPython implementation // https://github.com/python/cpython/blob/ac1ffd77858b62d169a08040c08aa5de26e145ac/Objects/floatobject.c#L1503C1-L1572C2 static PyObject * @@ -504,10 +537,8 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig if (self->backend == BACKEND_SLEEF) { value = self->value.sleef_value; - } - else { - // lets also tackle ld from sleef functions as well - value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + } else { + value = longdouble_to_quad(self->value.longdouble_value); } if(Sleef_iunordq1(value, value)) { @@ -653,7 +684,7 @@ QuadPrecision_hash(QuadPrecisionObject *self) value = self->value.sleef_value; } else { - value = Sleef_cast_from_doubleq1((double)self->value.longdouble_value); + value = longdouble_to_quad(self->value.longdouble_value); } // Check for NaN - use pointer hash (each NaN instance gets unique hash) diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index 30e2045..fdc656a 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -224,29 +224,35 @@ QuadPrecision_float(QuadPrecisionObject *self) static PyObject * QuadPrecision_int(QuadPrecisionObject *self) { - Sleef_quad value; - if (self->backend == BACKEND_SLEEF) { - value = self->value.sleef_value; - } - else { - // 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)) { + if (self->backend == BACKEND_SLEEF) + { + Sleef_quad value = self->value.sleef_value; + if (quad_isnan(&value)) { PyErr_SetString(PyExc_ValueError, "cannot convert float NaN to integer"); return NULL; } - if (Sleef_icmpgeq1(Sleef_fabsq1(value), QUAD_PRECISION_INF)) { + if (quad_isinf(&value)) + { PyErr_SetString(PyExc_OverflowError, "cannot convert float infinity to integer"); return NULL; } + return quad_to_pylong(Sleef_truncq1(value)); - Sleef_quad truncated = Sleef_truncq1(value); - return quad_to_pylong(truncated); + } + + long double value = self->value.longdouble_value; + if(isnanl(value)) + { + PyErr_SetString(PyExc_ValueError, "cannot convert float NaN to integer"); + return NULL; + } + if(isinfl(value)) + { + PyErr_SetString(PyExc_OverflowError, "cannot convert float infinity to integer"); + return NULL; + } + return longdouble_to_pylong(truncl(value)); } template diff --git a/src/include/scalar.h b/src/include/scalar.h index bd54931..74a6896 100644 --- a/src/include/scalar.h +++ b/src/include/scalar.h @@ -29,6 +29,8 @@ init_quadprecision_scalar(void); PyObject * quad_to_pylong(Sleef_quad value); +PyObject * +longdouble_to_pylong(long double value); #ifdef __cplusplus } diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 0938a2b..1164e5b 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -4894,20 +4894,31 @@ def test_as_integer_ratio_compatibility_with_float(self, value): float_ratio = float_num / float_denom assert abs(quad_ratio - float_ratio) < 1e-15 +BACKENDS = ["sleef", "longdouble"] + +EXACT_BOTH_BACKENDS = [ + 2**63 + 1, # 9223372036854775809, one past INT64_MAX, 64-bit span + -(2**63) - 1, # one past INT64_MIN + 12345678901234567, # 17-digit int, ~2^53.4, needs 54 bits + 18014398509481984001, # ~2^64, full 64-bit span + -18014398509481984001, +] + + 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.""" + Python ints rather than saturating at INT64_MAX. Exercised on both backends.""" # ---- NaN / Inf must raise the right exceptions ---- - @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + @pytest.mark.parametrize("backend", BACKENDS) 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("backend", BACKENDS) @pytest.mark.parametrize("inf_str", ["inf", "-inf"]) def test_int_of_inf_raises_overflow_error(self, backend, inf_str): # Python: int(float('inf')) -> OverflowError @@ -4916,6 +4927,7 @@ def test_int_of_inf_raises_overflow_error(self, backend, inf_str): # ---- Truncate toward zero (NOT floor, NOT banker's rounding) ---- + @pytest.mark.parametrize("backend", BACKENDS) @pytest.mark.parametrize("value_str,expected", [ ("0.0", 0), ("-0.0", 0), @@ -4931,65 +4943,126 @@ def test_int_of_inf_raises_overflow_error(self, backend, inf_str): ("42.0", 42), ("-42.0", -42), ]) - def test_int_truncates_toward_zero(self, value_str, expected): - assert int(QuadPrecision(value_str)) == expected + def test_int_truncates_toward_zero(self, backend, value_str, expected): + assert int(QuadPrecision(value_str, backend=backend)) == expected # Cross-check against Python's float for values float can represent exactly. - assert int(QuadPrecision(value_str)) == int(float(value_str)) + assert int(QuadPrecision(value_str, backend=backend)) == int(float(value_str)) + + @pytest.mark.parametrize("backend", BACKENDS) + @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + def test_int_exact_when_double_would_lose_precision(self, backend, n): + assert int(QuadPrecision(str(n), backend=backend)) == n # ---- Beyond int64: the original bug ---- - def test_int_beyond_int64_positive(self): + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_beyond_int64_positive(self, backend): # 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 + assert int(QuadPrecision(str(n), backend=backend)) == n - def test_int_beyond_int64_negative(self): + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_beyond_int64_negative(self, backend): n = -(2**63) - 1 # one past INT64_MIN - assert int(QuadPrecision(str(n))) == n + assert int(QuadPrecision(str(n), backend=backend)) == n + @pytest.mark.parametrize("backend", BACKENDS) @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. + def test_int_powers_of_two_far_above_int64(self, backend, exponent): + # Powers of two are exact in both the 64-bit and 113-bit mantissas. n = 2 ** exponent - assert int(QuadPrecision(str(n))) == n + assert int(QuadPrecision(str(n), backend=backend)) == n - def test_int_int64_max_exact(self): + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_int64_max_exact(self, backend): m = 2**63 - 1 - assert int(QuadPrecision(str(m))) == m + assert int(QuadPrecision(str(m), backend=backend)) == m - def test_int_int64_min_exact(self): + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_int64_min_exact(self, backend): m = -(2**63) - assert int(QuadPrecision(str(m))) == m + assert int(QuadPrecision(str(m), backend=backend)) == 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" + def test_int_1e30_sleef_exact(self): + # 10**30 needs 70 mantissa bits: exact in binary128, NOT in 64-bit long + # double. The issue calls this out: int(QuadPrecision('1e30')) used to + # saturate at INT64_MAX; on SLEEF it must be exactly 10**30. + assert int(QuadPrecision("1e30", backend="sleef")) == 10**30 + + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_1e30_not_saturated(self, backend): + # Both backends must return a ~1e30 magnitude integer, never the old + # INT64_MAX saturation. (longdouble rounds 1e30; we only check it is the + # correctly-rounded huge value, within long double's ~19-digit precision.) + result = int(QuadPrecision("1e30", backend=backend)) + assert result > 2**64 + assert abs(result - 10**30) < 10**(30 - 17) + + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_huge_value_not_truncated(self, backend): + # int(1e1000) needs ~1001 digits. The old fixed 128-byte buffer truncated + # to 127 garbage digits with no error. Must now return the full exact + # integer of the nearest representable value. + result = int(QuadPrecision("1e1000", backend=backend)) + assert len(str(result)) > 900 + assert abs(result - 10**1000) < 10**(1000 - 17) # ---- Return type ---- - def test_int_returns_python_int(self): - v = int(QuadPrecision("123")) + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_returns_python_int(self, backend): + v = int(QuadPrecision("123", backend=backend)) assert type(v) is int - def test_int_of_huge_value_returns_python_int(self): - v = int(QuadPrecision("1e30")) + @pytest.mark.parametrize("backend", BACKENDS) + def test_int_of_huge_value_returns_python_int(self, backend): + v = int(QuadPrecision("1e30", backend=backend)) assert type(v) is int assert v.bit_length() > 64 # arbitrary-precision, not a C int - # ---- Round-trip: int -> QuadPrecision -> int ---- - + # ---- Round-trip: int -> QuadPrecision -> int + @pytest.mark.parametrize("backend", BACKENDS) @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_int_quad_int_roundtrip(self, backend, n): + assert int(QuadPrecision(str(n), backend=backend)) == n + + +class TestLongdoubleBackendExactness: + """Issue #97 follow-up: is_integer(), as_integer_ratio() and hash() shared the + same longdouble-through-double precision bug as int(). Verify all three agree + with the exact value on BOTH backends for integers needing >53 mantissa bits.""" + + @pytest.mark.parametrize("backend", BACKENDS) + @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + def test_is_integer_true_for_large_exact_ints(self, backend, n): + assert QuadPrecision(str(n), backend=backend).is_integer() is True + + @pytest.mark.parametrize("backend", BACKENDS) + @pytest.mark.parametrize("value_str", ["42.5", "-0.5", "0.0001"]) + def test_is_integer_false_for_non_integers(self, backend, value_str): + assert QuadPrecision(value_str, backend=backend).is_integer() is False + + @pytest.mark.parametrize("backend", BACKENDS) + @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + def test_as_integer_ratio_reconstructs_large_exact_ints(self, backend, n): + # For an integer value the ratio is exactly n/1; n == num/den must hold + # exactly even without assuming the impl reduces the fraction. + num, den = QuadPrecision(str(n), backend=backend).as_integer_ratio() + assert num == n * den + + @pytest.mark.parametrize("backend", BACKENDS) + @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS + [0, 1, -1, 42]) + def test_hash_matches_python_int(self, backend, n): + # Key invariant: hash(QuadPrecision(n)) == hash(n) for integer-valued n, + # so a quad scalar and the equal Python int collide in dict/set lookups. + assert hash(QuadPrecision(str(n), backend=backend)) == hash(n) def test_quadprecision_scalar_dtype_expose(): From 3c0c7418f3b093e424a8d800918e9d2e2645587c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 04:57:37 +0000 Subject: [PATCH 4/9] redirect + retry --- .github/workflows/test_old_cpu.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_old_cpu.yml b/.github/workflows/test_old_cpu.yml index e8f563c..076997d 100644 --- a/.github/workflows/test_old_cpu.yml +++ b/.github/workflows/test_old_cpu.yml @@ -39,8 +39,15 @@ jobs: - name: Install Intel SDE run: | - curl -o /tmp/sde.tar.xz https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz - mkdir /tmp/sde && tar -xvf /tmp/sde.tar.xz -C /tmp/sde/ + URL="https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz" + UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" + for i in $(seq 1 10); do + curl -fSL -A "$UA" -o /tmp/sde.tar.xz "$URL" && tar -tJf /tmp/sde.tar.xz >/dev/null 2>&1 && break + echo "SDE download attempt $i returned no valid archive (HTTP 202 staging); retrying in 15s..." + sleep 15 + done + tar -tJf /tmp/sde.tar.xz >/dev/null 2>&1 || { echo "ERROR: could not download a valid SDE archive from $URL"; exit 1; } + mkdir /tmp/sde && tar -xf /tmp/sde.tar.xz -C /tmp/sde/ sudo mv /tmp/sde/* /opt/sde && sudo ln -s /opt/sde/sde64 /usr/bin/sde - name: Install system dependencies From 33deedc35e5ed23b3a8b22e40db5e5e1fea70535 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 05:04:07 +0000 Subject: [PATCH 5/9] math.h + new-sde-url --- .github/workflows/test_old_cpu.yml | 11 ++--------- src/csrc/scalar.c | 1 + src/csrc/scalar_ops.cpp | 1 + 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test_old_cpu.yml b/.github/workflows/test_old_cpu.yml index 076997d..b75c0a5 100644 --- a/.github/workflows/test_old_cpu.yml +++ b/.github/workflows/test_old_cpu.yml @@ -39,15 +39,8 @@ jobs: - name: Install Intel SDE run: | - URL="https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz" - UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" - for i in $(seq 1 10); do - curl -fSL -A "$UA" -o /tmp/sde.tar.xz "$URL" && tar -tJf /tmp/sde.tar.xz >/dev/null 2>&1 && break - echo "SDE download attempt $i returned no valid archive (HTTP 202 staging); retrying in 15s..." - sleep 15 - done - tar -tJf /tmp/sde.tar.xz >/dev/null 2>&1 || { echo "ERROR: could not download a valid SDE archive from $URL"; exit 1; } - mkdir /tmp/sde && tar -xf /tmp/sde.tar.xz -C /tmp/sde/ + curl -fSL -o /tmp/sde.tar.xz https://downloadmirror.intel.com/915934/sde-external-10.8.0-2026-03-15-lin.tar.xz + mkdir /tmp/sde && tar -xvf /tmp/sde.tar.xz -C /tmp/sde/ sudo mv /tmp/sde/* /opt/sde && sudo ln -s /opt/sde/sde64 /usr/bin/sde - name: Install system dependencies diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index d0ef851..f60d33b 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -2,6 +2,7 @@ #include #include #include +#include #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index fdc656a..1ee28e3 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -5,6 +5,7 @@ extern "C" { #include +#include #include "numpy/arrayobject.h" #include "numpy/ndarraytypes.h" From 4cb8f84e64fac26d2196e03db64901c08642e2f0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 05:07:02 +0000 Subject: [PATCH 6/9] use gitub asset --- .github/workflows/test_old_cpu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_old_cpu.yml b/.github/workflows/test_old_cpu.yml index b75c0a5..8ae79e2 100644 --- a/.github/workflows/test_old_cpu.yml +++ b/.github/workflows/test_old_cpu.yml @@ -39,7 +39,7 @@ jobs: - name: Install Intel SDE run: | - curl -fSL -o /tmp/sde.tar.xz https://downloadmirror.intel.com/915934/sde-external-10.8.0-2026-03-15-lin.tar.xz + curl -fSL -o /tmp/sde.tar.xz https://github.com/nihui/ncnn-assets/releases/download/toolchain/sde-external-10.8.0-2026-03-15-lin.tar.xz mkdir /tmp/sde && tar -xvf /tmp/sde.tar.xz -C /tmp/sde/ sudo mv /tmp/sde/* /opt/sde && sudo ln -s /opt/sde/sde64 /usr/bin/sde From c0e251d1938ea81ec32e49b2d31c4faf56b6e75e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 05:10:00 +0000 Subject: [PATCH 7/9] portable macros --- src/csrc/scalar.c | 2 +- src/csrc/scalar_ops.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/csrc/scalar.c b/src/csrc/scalar.c index f60d33b..7606d3f 100644 --- a/src/csrc/scalar.c +++ b/src/csrc/scalar.c @@ -513,7 +513,7 @@ PyObject* longdouble_to_pylong(long double value) static Sleef_quad longdouble_to_quad(long double value) { - if (isnanl(value) || isinfl(value) || value == 0.0L) + if (isnan(value) || isinf(value) || value == 0.0L) return Sleef_cast_from_doubleq1((double)value); int exp; diff --git a/src/csrc/scalar_ops.cpp b/src/csrc/scalar_ops.cpp index 1ee28e3..8690658 100644 --- a/src/csrc/scalar_ops.cpp +++ b/src/csrc/scalar_ops.cpp @@ -3,9 +3,10 @@ #define NPY_TARGET_VERSION NPY_2_4_API_VERSION #define NO_IMPORT_ARRAY +#include + extern "C" { #include -#include #include "numpy/arrayobject.h" #include "numpy/ndarraytypes.h" @@ -243,12 +244,12 @@ QuadPrecision_int(QuadPrecisionObject *self) } long double value = self->value.longdouble_value; - if(isnanl(value)) + if(std::isnan(value)) { PyErr_SetString(PyExc_ValueError, "cannot convert float NaN to integer"); return NULL; } - if(isinfl(value)) + if(std::isinf(value)) { PyErr_SetString(PyExc_OverflowError, "cannot convert float infinity to integer"); return NULL; From 93dc7c92dfdffef11a454aef6324062d566a8ec2 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 05:26:07 +0000 Subject: [PATCH 8/9] use np.ld.nmant + add SHA to sde url --- .github/workflows/test_old_cpu.yml | 5 +- tests/test_quaddtype.py | 95 +++++++++++++++++++++--------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test_old_cpu.yml b/.github/workflows/test_old_cpu.yml index 8ae79e2..e5569ae 100644 --- a/.github/workflows/test_old_cpu.yml +++ b/.github/workflows/test_old_cpu.yml @@ -39,7 +39,10 @@ jobs: - name: Install Intel SDE run: | - curl -fSL -o /tmp/sde.tar.xz https://github.com/nihui/ncnn-assets/releases/download/toolchain/sde-external-10.8.0-2026-03-15-lin.tar.xz + SDE_URL="https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz" + SDE_SHA256="f849acecad4c9b108259c643b2688fd65c35723cd23368abe5dd64b917cc18c0" + curl -o /tmp/sde.tar.xz "$SDE_URL" + echo "$SDE_SHA256 /tmp/sde.tar.xz" | sha256sum -c - mkdir /tmp/sde && tar -xvf /tmp/sde.tar.xz -C /tmp/sde/ sudo mv /tmp/sde/* /opt/sde && sudo ln -s /opt/sde/sde64 /usr/bin/sde diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 1164e5b..19296c0 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -4896,7 +4896,7 @@ def test_as_integer_ratio_compatibility_with_float(self, value): BACKENDS = ["sleef", "longdouble"] -EXACT_BOTH_BACKENDS = [ +LARGE_INTS = [ 2**63 + 1, # 9223372036854775809, one past INT64_MAX, 64-bit span -(2**63) - 1, # one past INT64_MIN 12345678901234567, # 17-digit int, ~2^53.4, needs 54 bits @@ -4904,6 +4904,38 @@ def test_as_integer_ratio_compatibility_with_float(self, value): -18014398509481984001, ] +# Mantissa bits each backend can hold. SLEEF is always binary128. The longdouble +# backend is the platform's C long double: 80-bit (64 bits) on x86/x86-64, but only +# IEEE double (53 bits) on arm64 macOS / MSVC, and binary128 (113 bits) on some +# aarch64 Linux. np.longdouble is that same type, so finfo gives the right width. +_MANTISSA_BITS = {"sleef": 113, "longdouble": np.finfo(np.longdouble).nmant + 1} + + +def _round_to_significand(n, bits): + """Round integer n to `bits` significant binary digits, round-half-to-even, + exactly as a binary float of that precision would store it. Pure integer math + so no float round-trip can taint the reference value.""" + if n == 0: + return 0 + neg = n < 0 + n = abs(n) + shift = n.bit_length() - bits + if shift <= 0: + return -n if neg else n + lower = n & ((1 << shift) - 1) + half = 1 << (shift - 1) + r = n >> shift + if lower > half or (lower == half and (r & 1)): + r += 1 + r <<= shift + return -r if neg else r + + +def expected_int(n, backend): + """The exact integer the given backend actually stores for integer n: n itself + where the backend's mantissa is wide enough, else the correctly-rounded value.""" + return _round_to_significand(n, _MANTISSA_BITS[backend]) + class TestIntConversion: """Regression tests for issue #97: int(QuadPrecision(...)) must @@ -4949,39 +4981,42 @@ def test_int_truncates_toward_zero(self, backend, value_str, expected): assert int(QuadPrecision(value_str, backend=backend)) == int(float(value_str)) @pytest.mark.parametrize("backend", BACKENDS) - @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + @pytest.mark.parametrize("n", LARGE_INTS) def test_int_exact_when_double_would_lose_precision(self, backend, n): - assert int(QuadPrecision(str(n), backend=backend)) == n + # >53 bits: routing the longdouble backend through double (the original + # bug) returned the rounded neighbour even where long double is wider. + # int() must be as precise as the backend's own float type, never worse. + assert int(QuadPrecision(str(n), backend=backend)) == expected_int(n, backend) # ---- Beyond int64: the original bug ---- @pytest.mark.parametrize("backend", BACKENDS) def test_int_beyond_int64_positive(self, backend): # 2^63 = 9223372036854775808 — one past INT64_MAX. The old code returned - # INT64_MAX (9223372036854775807). Must now be exact. + # INT64_MAX (9223372036854775807). 2^63 is a power of two: exact everywhere. n = 2**63 assert int(QuadPrecision(str(n), backend=backend)) == n @pytest.mark.parametrize("backend", BACKENDS) def test_int_beyond_int64_negative(self, backend): n = -(2**63) - 1 # one past INT64_MIN - assert int(QuadPrecision(str(n), backend=backend)) == n + assert int(QuadPrecision(str(n), backend=backend)) == expected_int(n, backend) @pytest.mark.parametrize("backend", BACKENDS) @pytest.mark.parametrize("exponent", [40, 60, 80, 100]) def test_int_powers_of_two_far_above_int64(self, backend, exponent): - # Powers of two are exact in both the 64-bit and 113-bit mantissas. + # Powers of two have a 1-bit mantissa: exact in every float format. n = 2 ** exponent assert int(QuadPrecision(str(n), backend=backend)) == n @pytest.mark.parametrize("backend", BACKENDS) def test_int_int64_max_exact(self, backend): m = 2**63 - 1 - assert int(QuadPrecision(str(m), backend=backend)) == m + assert int(QuadPrecision(str(m), backend=backend)) == expected_int(m, backend) @pytest.mark.parametrize("backend", BACKENDS) def test_int_int64_min_exact(self, backend): - m = -(2**63) + m = -(2**63) # power of two: exact everywhere assert int(QuadPrecision(str(m), backend=backend)) == m # ---- 1e30 from the issue ---- @@ -4994,21 +5029,21 @@ def test_int_1e30_sleef_exact(self): @pytest.mark.parametrize("backend", BACKENDS) def test_int_1e30_not_saturated(self, backend): - # Both backends must return a ~1e30 magnitude integer, never the old - # INT64_MAX saturation. (longdouble rounds 1e30; we only check it is the - # correctly-rounded huge value, within long double's ~19-digit precision.) + # The original bug saturated at INT64_MAX. Both backends must return a + # ~1e30 magnitude integer: the value the backend's float type rounds to. result = int(QuadPrecision("1e30", backend=backend)) assert result > 2**64 - assert abs(result - 10**30) < 10**(30 - 17) + assert result == expected_int(10**30, backend) @pytest.mark.parametrize("backend", BACKENDS) def test_int_huge_value_not_truncated(self, backend): - # int(1e1000) needs ~1001 digits. The old fixed 128-byte buffer truncated - # to 127 garbage digits with no error. Must now return the full exact - # integer of the nearest representable value. - result = int(QuadPrecision("1e1000", backend=backend)) - assert len(str(result)) > 900 - assert abs(result - 10**1000) < 10**(1000 - 17) + # The old fixed 128-byte buffer truncated huge values to ~127 garbage + # digits with no error. Use a value huge yet finite in the backend's float + # type: binary128 holds 1e1000 (~1001 digits); long double tops out near + # 1e308 where it is only IEEE double, so use 1e300 (~301 digits) there. + value_str = "1e1000" if backend == "sleef" else "1e300" + result = int(QuadPrecision(value_str, backend=backend)) + assert len(str(result)) > 250 # far beyond the old ~127-char buffer # ---- Return type ---- @@ -5031,7 +5066,7 @@ def test_int_of_huge_value_returns_python_int(self, backend): -(2**63), -(2**63) - 1, -(2**70), ]) def test_int_quad_int_roundtrip(self, backend, n): - assert int(QuadPrecision(str(n), backend=backend)) == n + assert int(QuadPrecision(str(n), backend=backend)) == expected_int(n, backend) class TestLongdoubleBackendExactness: @@ -5040,8 +5075,10 @@ class TestLongdoubleBackendExactness: with the exact value on BOTH backends for integers needing >53 mantissa bits.""" @pytest.mark.parametrize("backend", BACKENDS) - @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + @pytest.mark.parametrize("n", LARGE_INTS) def test_is_integer_true_for_large_exact_ints(self, backend, n): + # Rounding a large int to the backend's float still yields an integer, so + # is_integer() is True on both backends regardless of mantissa width. assert QuadPrecision(str(n), backend=backend).is_integer() is True @pytest.mark.parametrize("backend", BACKENDS) @@ -5050,19 +5087,21 @@ def test_is_integer_false_for_non_integers(self, backend, value_str): assert QuadPrecision(value_str, backend=backend).is_integer() is False @pytest.mark.parametrize("backend", BACKENDS) - @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS) + @pytest.mark.parametrize("n", LARGE_INTS) def test_as_integer_ratio_reconstructs_large_exact_ints(self, backend, n): - # For an integer value the ratio is exactly n/1; n == num/den must hold - # exactly even without assuming the impl reduces the fraction. + # For an integer-valued scalar the ratio is exactly v/1, where v is the + # value the backend actually stores. num == v * den must hold exactly. + v = expected_int(n, backend) num, den = QuadPrecision(str(n), backend=backend).as_integer_ratio() - assert num == n * den + assert num == v * den @pytest.mark.parametrize("backend", BACKENDS) - @pytest.mark.parametrize("n", EXACT_BOTH_BACKENDS + [0, 1, -1, 42]) + @pytest.mark.parametrize("n", LARGE_INTS + [0, 1, -1, 42]) def test_hash_matches_python_int(self, backend, n): - # Key invariant: hash(QuadPrecision(n)) == hash(n) for integer-valued n, - # so a quad scalar and the equal Python int collide in dict/set lookups. - assert hash(QuadPrecision(str(n), backend=backend)) == hash(n) + # hash(QuadPrecision(n)) == hash(v), where v is the value the backend + # stores, so a quad scalar and the equal Python int collide in dict/sets. + v = expected_int(n, backend) + assert hash(QuadPrecision(str(n), backend=backend)) == hash(v) def test_quadprecision_scalar_dtype_expose(): From 24990a761a08e8f2d93ae525d7fabc3e997f85ed Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 5 Jun 2026 05:33:26 +0000 Subject: [PATCH 9/9] self hosting sde binary --- .github/workflows/test_old_cpu.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_old_cpu.yml b/.github/workflows/test_old_cpu.yml index e5569ae..c90c61b 100644 --- a/.github/workflows/test_old_cpu.yml +++ b/.github/workflows/test_old_cpu.yml @@ -39,9 +39,9 @@ jobs: - name: Install Intel SDE run: | - SDE_URL="https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz" - SDE_SHA256="f849acecad4c9b108259c643b2688fd65c35723cd23368abe5dd64b917cc18c0" - curl -o /tmp/sde.tar.xz "$SDE_URL" + SDE_URL="https://github.com/SwayamInSync/numpy-quaddtype/releases/download/sde-toolchain/sde-external-10.8.0-2026-03-15-lin.tar.xz" + SDE_SHA256="50b320cd226acef7a491f5b321fc1be3c3c7984f9e27a456e64894b5b0979dd3" + curl -fSL -o /tmp/sde.tar.xz "$SDE_URL" echo "$SDE_SHA256 /tmp/sde.tar.xz" | sha256sum -c - mkdir /tmp/sde && tar -xvf /tmp/sde.tar.xz -C /tmp/sde/ sudo mv /tmp/sde/* /opt/sde && sudo ln -s /opt/sde/sde64 /usr/bin/sde