Skip to content
Open
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
21 changes: 19 additions & 2 deletions src/csrc/scalar_ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,29 @@ 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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this and the similar cast alluded to in the comment will lose precision on any platform with extended precision native long double.

Instead of doing this, you should write a utility function that checks for NaN and inf with std::isnan and std::isinf, which do support extended precision long double natively and then write the value to a string buffer, which you can parse however you need it.

}

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;
}

Sleef_quad truncated = Sleef_truncq1(value);
return quad_to_pylong(truncated);
}

template <binary_op_quad_def sleef_op, binary_op_longdouble_def longdouble_op>
Expand Down
4 changes: 4 additions & 0 deletions src/include/scalar.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ extern "C" {

#include <Python.h>
#include <sleef.h>
#include <sleefquad.h>
#include "quad_common.h"

typedef struct {
Expand All @@ -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
Expand Down
98 changes: 98 additions & 0 deletions tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and many other tests in this file aren't parametrized by the backend, which leads to missing the issue I pointed out above.

# 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")
Expand Down
Loading