@@ -4896,14 +4896,46 @@ def test_as_integer_ratio_compatibility_with_float(self, value):
48964896
48974897BACKENDS = ["sleef" , "longdouble" ]
48984898
4899- EXACT_BOTH_BACKENDS = [
4899+ LARGE_INTS = [
49004900 2 ** 63 + 1 , # 9223372036854775809, one past INT64_MAX, 64-bit span
49014901 - (2 ** 63 ) - 1 , # one past INT64_MIN
49024902 12345678901234567 , # 17-digit int, ~2^53.4, needs 54 bits
49034903 18014398509481984001 , # ~2^64, full 64-bit span
49044904 - 18014398509481984001 ,
49054905]
49064906
4907+ # Mantissa bits each backend can hold. SLEEF is always binary128. The longdouble
4908+ # backend is the platform's C long double: 80-bit (64 bits) on x86/x86-64, but only
4909+ # IEEE double (53 bits) on arm64 macOS / MSVC, and binary128 (113 bits) on some
4910+ # aarch64 Linux. np.longdouble is that same type, so finfo gives the right width.
4911+ _MANTISSA_BITS = {"sleef" : 113 , "longdouble" : np .finfo (np .longdouble ).nmant + 1 }
4912+
4913+
4914+ def _round_to_significand (n , bits ):
4915+ """Round integer n to `bits` significant binary digits, round-half-to-even,
4916+ exactly as a binary float of that precision would store it. Pure integer math
4917+ so no float round-trip can taint the reference value."""
4918+ if n == 0 :
4919+ return 0
4920+ neg = n < 0
4921+ n = abs (n )
4922+ shift = n .bit_length () - bits
4923+ if shift <= 0 :
4924+ return - n if neg else n
4925+ lower = n & ((1 << shift ) - 1 )
4926+ half = 1 << (shift - 1 )
4927+ r = n >> shift
4928+ if lower > half or (lower == half and (r & 1 )):
4929+ r += 1
4930+ r <<= shift
4931+ return - r if neg else r
4932+
4933+
4934+ def expected_int (n , backend ):
4935+ """The exact integer the given backend actually stores for integer n: n itself
4936+ where the backend's mantissa is wide enough, else the correctly-rounded value."""
4937+ return _round_to_significand (n , _MANTISSA_BITS [backend ])
4938+
49074939
49084940class TestIntConversion :
49094941 """Regression tests for issue #97: int(QuadPrecision(...)) must
@@ -4949,39 +4981,42 @@ def test_int_truncates_toward_zero(self, backend, value_str, expected):
49494981 assert int (QuadPrecision (value_str , backend = backend )) == int (float (value_str ))
49504982
49514983 @pytest .mark .parametrize ("backend" , BACKENDS )
4952- @pytest .mark .parametrize ("n" , EXACT_BOTH_BACKENDS )
4984+ @pytest .mark .parametrize ("n" , LARGE_INTS )
49534985 def test_int_exact_when_double_would_lose_precision (self , backend , n ):
4954- assert int (QuadPrecision (str (n ), backend = backend )) == n
4986+ # >53 bits: routing the longdouble backend through double (the original
4987+ # bug) returned the rounded neighbour even where long double is wider.
4988+ # int() must be as precise as the backend's own float type, never worse.
4989+ assert int (QuadPrecision (str (n ), backend = backend )) == expected_int (n , backend )
49554990
49564991 # ---- Beyond int64: the original bug ----
49574992
49584993 @pytest .mark .parametrize ("backend" , BACKENDS )
49594994 def test_int_beyond_int64_positive (self , backend ):
49604995 # 2^63 = 9223372036854775808 — one past INT64_MAX. The old code returned
4961- # INT64_MAX (9223372036854775807). Must now be exact.
4996+ # INT64_MAX (9223372036854775807). 2^63 is a power of two: exact everywhere .
49624997 n = 2 ** 63
49634998 assert int (QuadPrecision (str (n ), backend = backend )) == n
49644999
49655000 @pytest .mark .parametrize ("backend" , BACKENDS )
49665001 def test_int_beyond_int64_negative (self , backend ):
49675002 n = - (2 ** 63 ) - 1 # one past INT64_MIN
4968- assert int (QuadPrecision (str (n ), backend = backend )) == n
5003+ assert int (QuadPrecision (str (n ), backend = backend )) == expected_int ( n , backend )
49695004
49705005 @pytest .mark .parametrize ("backend" , BACKENDS )
49715006 @pytest .mark .parametrize ("exponent" , [40 , 60 , 80 , 100 ])
49725007 def test_int_powers_of_two_far_above_int64 (self , backend , exponent ):
4973- # Powers of two are exact in both the 64-bit and 113-bit mantissas .
5008+ # Powers of two have a 1-bit mantissa: exact in every float format .
49745009 n = 2 ** exponent
49755010 assert int (QuadPrecision (str (n ), backend = backend )) == n
49765011
49775012 @pytest .mark .parametrize ("backend" , BACKENDS )
49785013 def test_int_int64_max_exact (self , backend ):
49795014 m = 2 ** 63 - 1
4980- assert int (QuadPrecision (str (m ), backend = backend )) == m
5015+ assert int (QuadPrecision (str (m ), backend = backend )) == expected_int ( m , backend )
49815016
49825017 @pytest .mark .parametrize ("backend" , BACKENDS )
49835018 def test_int_int64_min_exact (self , backend ):
4984- m = - (2 ** 63 )
5019+ m = - (2 ** 63 ) # power of two: exact everywhere
49855020 assert int (QuadPrecision (str (m ), backend = backend )) == m
49865021
49875022 # ---- 1e30 from the issue ----
@@ -4994,21 +5029,21 @@ def test_int_1e30_sleef_exact(self):
49945029
49955030 @pytest .mark .parametrize ("backend" , BACKENDS )
49965031 def test_int_1e30_not_saturated (self , backend ):
4997- # Both backends must return a ~1e30 magnitude integer, never the old
4998- # INT64_MAX saturation. (longdouble rounds 1e30; we only check it is the
4999- # correctly-rounded huge value, within long double's ~19-digit precision.)
5032+ # The original bug saturated at INT64_MAX. Both backends must return a
5033+ # ~1e30 magnitude integer: the value the backend's float type rounds to.
50005034 result = int (QuadPrecision ("1e30" , backend = backend ))
50015035 assert result > 2 ** 64
5002- assert abs ( result - 10 ** 30 ) < 10 ** ( 30 - 17 )
5036+ assert result == expected_int ( 10 ** 30 , backend )
50035037
50045038 @pytest .mark .parametrize ("backend" , BACKENDS )
50055039 def test_int_huge_value_not_truncated (self , backend ):
5006- # int(1e1000) needs ~1001 digits. The old fixed 128-byte buffer truncated
5007- # to 127 garbage digits with no error. Must now return the full exact
5008- # integer of the nearest representable value.
5009- result = int (QuadPrecision ("1e1000" , backend = backend ))
5010- assert len (str (result )) > 900
5011- assert abs (result - 10 ** 1000 ) < 10 ** (1000 - 17 )
5040+ # The old fixed 128-byte buffer truncated huge values to ~127 garbage
5041+ # digits with no error. Use a value huge yet finite in the backend's float
5042+ # type: binary128 holds 1e1000 (~1001 digits); long double tops out near
5043+ # 1e308 where it is only IEEE double, so use 1e300 (~301 digits) there.
5044+ value_str = "1e1000" if backend == "sleef" else "1e300"
5045+ result = int (QuadPrecision (value_str , backend = backend ))
5046+ assert len (str (result )) > 250 # far beyond the old ~127-char buffer
50125047
50135048 # ---- Return type ----
50145049
@@ -5031,7 +5066,7 @@ def test_int_of_huge_value_returns_python_int(self, backend):
50315066 - (2 ** 63 ), - (2 ** 63 ) - 1 , - (2 ** 70 ),
50325067 ])
50335068 def test_int_quad_int_roundtrip (self , backend , n ):
5034- assert int (QuadPrecision (str (n ), backend = backend )) == n
5069+ assert int (QuadPrecision (str (n ), backend = backend )) == expected_int ( n , backend )
50355070
50365071
50375072class TestLongdoubleBackendExactness :
@@ -5040,8 +5075,10 @@ class TestLongdoubleBackendExactness:
50405075 with the exact value on BOTH backends for integers needing >53 mantissa bits."""
50415076
50425077 @pytest .mark .parametrize ("backend" , BACKENDS )
5043- @pytest .mark .parametrize ("n" , EXACT_BOTH_BACKENDS )
5078+ @pytest .mark .parametrize ("n" , LARGE_INTS )
50445079 def test_is_integer_true_for_large_exact_ints (self , backend , n ):
5080+ # Rounding a large int to the backend's float still yields an integer, so
5081+ # is_integer() is True on both backends regardless of mantissa width.
50455082 assert QuadPrecision (str (n ), backend = backend ).is_integer () is True
50465083
50475084 @pytest .mark .parametrize ("backend" , BACKENDS )
@@ -5050,19 +5087,21 @@ def test_is_integer_false_for_non_integers(self, backend, value_str):
50505087 assert QuadPrecision (value_str , backend = backend ).is_integer () is False
50515088
50525089 @pytest .mark .parametrize ("backend" , BACKENDS )
5053- @pytest .mark .parametrize ("n" , EXACT_BOTH_BACKENDS )
5090+ @pytest .mark .parametrize ("n" , LARGE_INTS )
50545091 def test_as_integer_ratio_reconstructs_large_exact_ints (self , backend , n ):
5055- # For an integer value the ratio is exactly n/1; n == num/den must hold
5056- # exactly even without assuming the impl reduces the fraction.
5092+ # For an integer-valued scalar the ratio is exactly v/1, where v is the
5093+ # value the backend actually stores. num == v * den must hold exactly.
5094+ v = expected_int (n , backend )
50575095 num , den = QuadPrecision (str (n ), backend = backend ).as_integer_ratio ()
5058- assert num == n * den
5096+ assert num == v * den
50595097
50605098 @pytest .mark .parametrize ("backend" , BACKENDS )
5061- @pytest .mark .parametrize ("n" , EXACT_BOTH_BACKENDS + [0 , 1 , - 1 , 42 ])
5099+ @pytest .mark .parametrize ("n" , LARGE_INTS + [0 , 1 , - 1 , 42 ])
50625100 def test_hash_matches_python_int (self , backend , n ):
5063- # Key invariant: hash(QuadPrecision(n)) == hash(n) for integer-valued n,
5064- # so a quad scalar and the equal Python int collide in dict/set lookups.
5065- assert hash (QuadPrecision (str (n ), backend = backend )) == hash (n )
5101+ # hash(QuadPrecision(n)) == hash(v), where v is the value the backend
5102+ # stores, so a quad scalar and the equal Python int collide in dict/sets.
5103+ v = expected_int (n , backend )
5104+ assert hash (QuadPrecision (str (n ), backend = backend )) == hash (v )
50665105
50675106
50685107def test_quadprecision_scalar_dtype_expose ():
0 commit comments