@@ -5229,3 +5229,142 @@ def test_add_regression_zero_plus_small(self):
52295229
52305230 assert result_yx == result_xy , f"0 + x = { result_yx } , but x + 0 = { result_xy } "
52315231 assert result_yx == x , f"0 + x = { result_yx } , expected { x } "
5232+
5233+
5234+ class TestQuadPrecisionHash :
5235+ """Test suite for QuadPrecision hash function.
5236+
5237+ The hash implementation follows CPython's _Py_HashDouble algorithm to ensure
5238+ the invariant: hash(x) == hash(y) when x and y are numerically equal,
5239+ even across different types.
5240+ """
5241+
5242+ @pytest .mark .parametrize ("value" , [
5243+ # Values that are exactly representable in binary floating point
5244+ "0.0" , "1.0" , "-1.0" , "2.0" , "-2.0" ,
5245+ "0.5" , "0.25" , "1.5" , "-0.5" ,
5246+ "100.0" , "-100.0" ,
5247+ # Powers of 2 are exactly representable
5248+ "0.125" , "0.0625" , "4.0" , "8.0" ,
5249+ ])
5250+ def test_hash_matches_float (self , value ):
5251+ """Test that hash(QuadPrecision) == hash(float) for exactly representable values.
5252+
5253+ Note: Only values that are exactly representable in both float64 and float128
5254+ should match. Values like 0.1, 0.3 will have different hashes because they
5255+ have different binary representations at different precisions.
5256+ """
5257+ quad_val = QuadPrecision (value )
5258+ float_val = float (value )
5259+ assert hash (quad_val ) == hash (float_val )
5260+
5261+ @pytest .mark .parametrize ("value" , [0.1 , 0.3 , 0.7 , 1.1 , 2.3 , 1e300 , 1e-300 ])
5262+ def test_hash_matches_float_from_float (self , value ):
5263+ """Test that QuadPrecision created from float has same hash as that float.
5264+
5265+ When creating QuadPrecision from a Python float, the value is converted
5266+ from the float's double precision representation, so they should be
5267+ numerically equal and have the same hash.
5268+ """
5269+ quad_val = QuadPrecision (value ) # Created from float, not string
5270+ assert hash (quad_val ) == hash (value )
5271+
5272+ @pytest .mark .parametrize ("value" , [0 , 1 , - 1 , 2 , - 2 , 100 , - 100 , 1000 , - 1000 ])
5273+ def test_hash_matches_int (self , value ):
5274+ """Test that hash(QuadPrecision) == hash(int) for integer values."""
5275+ quad_val = QuadPrecision (value )
5276+ assert hash (quad_val ) == hash (value )
5277+
5278+ def test_hash_matches_large_int (self ):
5279+ """Test that hash(QuadPrecision) == hash(int) for large integers."""
5280+ big_int = 10 ** 20
5281+ quad_val = QuadPrecision (str (big_int ))
5282+ assert hash (quad_val ) == hash (big_int )
5283+
5284+ def test_hash_infinity (self ):
5285+ """Test that infinity hash matches Python's float infinity hash."""
5286+ assert hash (QuadPrecision ("inf" )) == hash (float ("inf" ))
5287+ assert hash (QuadPrecision ("-inf" )) == hash (float ("-inf" ))
5288+ # Standard PyHASH_INF values
5289+ assert hash (QuadPrecision ("inf" )) == 314159
5290+ assert hash (QuadPrecision ("-inf" )) == - 314159
5291+
5292+ def test_hash_nan_unique (self ):
5293+ """Test that each NaN instance gets a unique hash (pointer-based)."""
5294+ nan1 = QuadPrecision ("nan" )
5295+ nan2 = QuadPrecision ("nan" )
5296+ # NaN instances should have different hashes (based on object identity)
5297+ assert hash (nan1 ) != hash (nan2 )
5298+
5299+ def test_hash_nan_same_instance (self ):
5300+ """Test that the same NaN instance has consistent hash."""
5301+ nan = QuadPrecision ("nan" )
5302+ assert hash (nan ) == hash (nan )
5303+
5304+ def test_hash_negative_one (self ):
5305+ """Test that hash(-1) returns -2 (Python's hash convention)."""
5306+ # In Python, hash(-1) returns -2 because -1 is reserved for errors
5307+ assert hash (QuadPrecision (- 1.0 )) == - 2
5308+ assert hash (QuadPrecision ("-1.0" )) == - 2
5309+
5310+ def test_hash_set_membership (self ):
5311+ """Test that QuadPrecision values work correctly in sets."""
5312+ vals = [QuadPrecision (1.0 ), QuadPrecision (2.0 ), QuadPrecision (1.0 )]
5313+ unique_set = set (vals )
5314+ assert len (unique_set ) == 2
5315+
5316+ def test_hash_set_cross_type (self ):
5317+ """Test that QuadPrecision and float with same value are in same set bucket."""
5318+ s = {QuadPrecision (1.0 )}
5319+ s .add (1.0 )
5320+ assert len (s ) == 1
5321+
5322+ def test_hash_dict_key (self ):
5323+ """Test that QuadPrecision values work as dict keys."""
5324+ d = {QuadPrecision (1.0 ): "one" , QuadPrecision (2.0 ): "two" }
5325+ assert d [QuadPrecision (1.0 )] == "one"
5326+ assert d [QuadPrecision (2.0 )] == "two"
5327+
5328+ def test_hash_dict_cross_type_lookup (self ):
5329+ """Test that dict lookup works with float keys when hash matches."""
5330+ d = {QuadPrecision (1.0 ): "one" }
5331+ # Float lookup should work if hash and eq both work
5332+ assert d .get (1.0 ) == "one"
5333+
5334+ @pytest .mark .parametrize ("value" , [
5335+ # Powers of 2 outside double range but within quad range
5336+ # Double max exponent is ~1024, quad max is ~16384
5337+ 2 ** 1100 , 2 ** 2000 , 2 ** 5000 , 2 ** 10000 ,
5338+ - (2 ** 1100 ), - (2 ** 2000 ),
5339+ # Small powers of 2 (subnormal in double, normal in quad)
5340+ 2 ** (- 1100 ), 2 ** (- 2000 ),
5341+ ])
5342+ def test_hash_extreme_integers_outside_double_range (self , value ):
5343+ """Test hash matches Python int for values outside double range.
5344+
5345+ We use powers of 2 which are exactly representable in quad precision.
5346+ Since these integers are exact, hash(QuadPrecision(x)) must equal hash(x).
5347+ """
5348+ quad_val = QuadPrecision (value )
5349+ assert hash (quad_val ) == hash (value )
5350+
5351+ @pytest .mark .parametrize ("value" , [
5352+ "1e500" , "-1e500" , "1e1000" , "-1e1000" , "1e-500" , "-1e-500" ,
5353+ "1.23456789e500" , "-9.87654321e-600" ,
5354+ ])
5355+ def test_hash_matches_mpmath (self , value ):
5356+ """Test hash matches mpmath at quad precision (113 bits).
5357+
5358+ mpmath with 113-bit precision represents the same value as QuadPrecision,
5359+ so their hashes must match.
5360+ """
5361+ mp .prec = 113
5362+ quad_val = QuadPrecision (value )
5363+ mpf_val = mp .mpf (value )
5364+ assert hash (quad_val ) == hash (mpf_val )
5365+
5366+ @pytest .mark .parametrize ("backend" , ["sleef" , "longdouble" ])
5367+ def test_hash_backends (self , backend ):
5368+ """Test hash works for both backends."""
5369+ quad_val = QuadPrecision (1.5 , backend = backend )
5370+ assert hash (quad_val ) == hash (1.5 )
0 commit comments