diff --git a/Doc/c-api/hash.rst b/Doc/c-api/hash.rst index 4dc121d7fbaa9b4..6f699a93065fba8 100644 --- a/Doc/c-api/hash.rst +++ b/Doc/c-api/hash.rst @@ -5,12 +5,16 @@ PyHash API See also the :c:member:`PyTypeObject.tp_hash` member. +Types +^^^^^ + .. c:type:: Py_hash_t Hash value type: signed integer. .. versionadded:: 3.2 + .. c:type:: Py_uhash_t Hash value type: unsigned integer. @@ -41,8 +45,23 @@ See also the :c:member:`PyTypeObject.tp_hash` member. .. versionadded:: 3.4 +Functions +^^^^^^^^^ + +.. c:function:: Py_hash_t PyHash_Double(double value) + + Hash a C double number. + + Return ``-1`` if *value* is not-a-number (NaN). + + .. versionadded:: 3.13 + + .. c:function:: PyHash_FuncDef* PyHash_GetFuncDef(void) Get the hash function definition. + .. seealso:: + :pep:`456` "Secure and interchangeable hash algorithm". + .. versionadded:: 3.4 diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 136fe901ce39fb8..54724a6fca794ea 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1181,6 +1181,9 @@ New Features :exc:`KeyError` if the key missing. (Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.) +* Add :c:func:`PyHash_Double` function to hash a C double number. + (Contributed by Victor Stinner in :gh:`111545`.) + Porting to Python 3.13 ---------------------- diff --git a/Include/cpython/pyhash.h b/Include/cpython/pyhash.h index 62ae6084bbcf533..aa46e91374f05b2 100644 --- a/Include/cpython/pyhash.h +++ b/Include/cpython/pyhash.h @@ -11,3 +11,5 @@ typedef struct { } PyHash_FuncDef; PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void); + +PyAPI_FUNC(Py_hash_t) PyHash_Double(double value); diff --git a/Lib/test/test_capi/test_hash.py b/Lib/test/test_capi/test_hash.py index 59dec15bc21445f..e7ceb5e01316ada 100644 --- a/Lib/test/test_capi/test_hash.py +++ b/Lib/test/test_capi/test_hash.py @@ -1,3 +1,4 @@ +import math import sys import unittest from test.support import import_helper @@ -31,3 +32,49 @@ def test_hash_getfuncdef(self): self.assertEqual(func_def.name, hash_info.algorithm) self.assertEqual(func_def.hash_bits, hash_info.hash_bits) self.assertEqual(func_def.seed_bits, hash_info.seed_bits) + + def test_hash_double(self): + # Test PyHash_Double() + hash_double = _testcapi.hash_double + + # test integers + def python_hash_int(x): + negative = (x < 0) + x = abs(x) % sys.hash_info.modulus + if negative: + x = -x + if x == -1: + x = -2 + return x + + integers = [ + *range(1, 30), + 2**30 - 1, + 2 ** 233, + int(sys.float_info.max), + ] + integers.extend([-x for x in integers]) + integers.append(0) + + for x in integers: + self.assertEqual(hash_double(float(x)), python_hash_int(x), x) + + # test non-finite values + self.assertEqual(hash_double(float('inf')), sys.hash_info.inf) + self.assertEqual(hash_double(float('-inf')), -sys.hash_info.inf) + self.assertEqual(hash_double(float('nan')), -1) + + # special values: compare with Python hash() function + def python_hash_double(x): + return hash(x) + + special_values = ( + sys.float_info.max, + sys.float_info.min, + sys.float_info.epsilon, + math.nextafter(0.0, 1.0), + ) + for x in special_values: + with self.subTest(x=x): + self.assertEqual(hash_double(x), python_hash_double(x)) + self.assertEqual(hash_double(-x), python_hash_double(-x)) diff --git a/Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst b/Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst new file mode 100644 index 000000000000000..dc06d282a7bff81 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyHash_Double` function to hash a C double number. Patch by +Victor Stinner. diff --git a/Modules/_testcapi/hash.c b/Modules/_testcapi/hash.c index d0b8127020c5c14..2ac4f845edae8f1 100644 --- a/Modules/_testcapi/hash.c +++ b/Modules/_testcapi/hash.c @@ -1,6 +1,7 @@ #include "parts.h" #include "util.h" + static PyObject * hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -44,8 +45,23 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) return result; } + +static PyObject * +hash_double(PyObject *Py_UNUSED(module), PyObject *args) +{ + double value; + if (!PyArg_ParseTuple(args, "d", &value)) { + return NULL; + } + Py_hash_t hash = PyHash_Double(value); + Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash)); + return PyLong_FromLongLong(hash); +} + + static PyMethodDef test_methods[] = { {"hash_getfuncdef", hash_getfuncdef, METH_NOARGS}, + {"hash_double", hash_double, METH_VARARGS}, {NULL}, }; diff --git a/Python/pyhash.c b/Python/pyhash.c index f9060b8003a0a7d..85962bcfb076ad2 100644 --- a/Python/pyhash.c +++ b/Python/pyhash.c @@ -86,49 +86,71 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0}; Py_hash_t _Py_HashPointer(const void *); Py_hash_t -_Py_HashDouble(PyObject *inst, double v) +PyHash_Double(double v) { - int e, sign; - double m; - Py_uhash_t x, y; - if (!Py_IS_FINITE(v)) { - if (Py_IS_INFINITY(v)) + if (Py_IS_INFINITY(v)) { return v > 0 ? _PyHASH_INF : -_PyHASH_INF; - else - return _Py_HashPointer(inst); + } + else { + assert(Py_IS_NAN(v)); + return -1; + } } - m = frexp(v, &e); - - sign = 1; + int e; + double m = frexp(v, &e); + int sign; if (m < 0) { sign = -1; m = -m; } + else { + sign = 1; + } /* process 28 bits at a time; this should work well both for binary and hexadecimal floating point. */ - x = 0; + Py_uhash_t x = 0; while (m) { x = ((x << 28) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - 28); m *= 268435456.0; /* 2**28 */ e -= 28; - y = (Py_uhash_t)m; /* pull out integer part */ + + Py_uhash_t y = (Py_uhash_t)m; /* pull out integer part */ m -= y; x += y; - if (x >= _PyHASH_MODULUS) + if (x >= _PyHASH_MODULUS) { x -= _PyHASH_MODULUS; + } } /* adjust for the exponent; first reduce it modulo _PyHASH_BITS */ - e = e >= 0 ? e % _PyHASH_BITS : _PyHASH_BITS-1-((-1-e) % _PyHASH_BITS); + if (e >= 0) { + e = e % _PyHASH_BITS; + } + else { + e = _PyHASH_BITS - 1 - ((-1 - e) % _PyHASH_BITS); + } x = ((x << e) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - e); x = x * sign; - if (x == (Py_uhash_t)-1) - x = (Py_uhash_t)-2; - return (Py_hash_t)x; + + Py_hash_t result = (Py_hash_t)x; + if (result == -1) { + result = -2; + } + return (Py_hash_t)result; +} + +Py_hash_t +_Py_HashDouble(PyObject *inst, double v) +{ + Py_hash_t hash = PyHash_Double(v); + if (hash == -1) { + return _Py_HashPointer(inst); + } + return hash; } Py_hash_t