@@ -5229,3 +5229,121 @@ 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 ])
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+ "1e-100" , "-1e-100" ,
5336+ "1e100" , "-1e100" ,
5337+ "1e-300" , "-1e-300" ,
5338+ ])
5339+ def test_hash_extreme_values (self , value ):
5340+ """Test hash works for extreme values without errors."""
5341+ quad_val = QuadPrecision (value )
5342+ h = hash (quad_val )
5343+ assert isinstance (h , int )
5344+
5345+ @pytest .mark .parametrize ("backend" , ["sleef" , "longdouble" ])
5346+ def test_hash_backends (self , backend ):
5347+ """Test hash works for both backends."""
5348+ quad_val = QuadPrecision (1.5 , backend = backend )
5349+ assert hash (quad_val ) == hash (1.5 )
0 commit comments