diff --git a/Doc/c-api/hash.rst b/Doc/c-api/hash.rst index 91d88ae27bc9f4..64af1a031a26fe 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,6 +45,29 @@ See also the :c:member:`PyTypeObject.tp_hash` member. .. versionadded:: 3.4 +Functions +^^^^^^^^^ + +.. c:function:: Py_hash_t Py_HashDouble(double value) + + Hash a C double number. + + * If *value* is positive infinity, return :data:`sys.hash_info.inf + `. + * If *value* is negative infinity, return :data:`-sys.hash_info.inf + `. + * If *value* is not-a-number (NaN), return :data:`sys.hash_info.nan + ` (``0``). + * Otherwise, return the hash value of the finite *value* number. + + .. note:: + Return the hash value ``0`` for the floating point numbers ``-0.0`` and + ``+0.0``, and for not-a-number (NaN). ``Py_IS_NAN(value)`` can be used to + check if *value* is not-a-number. + + .. versionadded:: 3.13 + + .. c:function:: PyHash_FuncDef* PyHash_GetFuncDef(void) Get the hash function definition. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e22257853d8333..710b8906f5e00c 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1281,6 +1281,23 @@ New Features * Add :c:func:`Py_HashPointer` function to hash a pointer. (Contributed by Victor Stinner in :gh:`111545`.) +* Add :c:func:`Py_HashDouble` function to hash a C double number. Existing code + using the private ``_Py_HashDouble()`` function can be updated to: + + .. code-block:: c + + Py_hash_t hash_double(PyObject *obj, double value) + { + if (!Py_IS_NAN(value)) { + return Py_HashDouble(value); + } + else { + return Py_HashPointer(obj); + } + } + + (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 396c208e1b106a..0f47711deabcd9 100644 --- a/Include/cpython/pyhash.h +++ b/Include/cpython/pyhash.h @@ -37,3 +37,4 @@ typedef struct { PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void); PyAPI_FUNC(Py_hash_t) Py_HashPointer(const void *ptr); +PyAPI_FUNC(Py_hash_t) Py_HashDouble(double value); diff --git a/Lib/test/test_capi/test_hash.py b/Lib/test/test_capi/test_hash.py index 8436da7c32df10..2cba164d557b7a 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 @@ -77,3 +78,43 @@ def python_hash_pointer(x): # Py_HashPointer((void*)(uintptr_t)-1) doesn't return -1 but -2 VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1) self.assertEqual(hash_pointer(VOID_P_MAX), -2) + + def test_hash_double(self): + # Test Py_HashDouble() + hash_double = _testcapi.hash_double + + # test some integers + integers = [ + *range(1, 30), + 2**30 - 1, + 2 ** 233, + int(sys.float_info.max), + ] + for x in integers: + with self.subTest(x=x): + self.assertEqual(hash_double(float(x)), hash(x)) + self.assertEqual(hash_double(float(-x)), hash(-x)) + + # test positive and negative zeros + self.assertEqual(hash_double(float(0.0)), 0) + self.assertEqual(hash_double(float(-0.0)), 0) + + # test +inf and -inf + inf = float("inf") + self.assertEqual(hash_double(inf), sys.hash_info.inf) + self.assertEqual(hash_double(-inf), -sys.hash_info.inf) + + # special float values: compare with Python hash() function + special_values = ( + math.nextafter(0.0, 1.0), # smallest positive subnormal number + sys.float_info.min, # smallest positive normal number + sys.float_info.epsilon, + sys.float_info.max, # largest positive finite number + ) + for x in special_values: + with self.subTest(x=x): + self.assertEqual(hash_double(x), hash(x)) + self.assertEqual(hash_double(-x), hash(-x)) + + # test not-a-number (NaN) + self.assertEqual(hash_double(float('nan')), 0) diff --git a/Misc/NEWS.d/next/C API/2023-12-06-15-32-30.gh-issue-111545.kSOygi.rst b/Misc/NEWS.d/next/C API/2023-12-06-15-32-30.gh-issue-111545.kSOygi.rst new file mode 100644 index 00000000000000..b6f1db895a3523 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-12-06-15-32-30.gh-issue-111545.kSOygi.rst @@ -0,0 +1,2 @@ +Add :c:func:`Py_HashDouble` function to hash a C double number. Patch by +Victor Stinner. diff --git a/Modules/_testcapi/hash.c b/Modules/_testcapi/hash.c index aee76787dcddb3..a298c358b654ae 100644 --- a/Modules/_testcapi/hash.c +++ b/Modules/_testcapi/hash.c @@ -1,6 +1,17 @@ #include "parts.h" #include "util.h" + +static PyObject * +long_from_hash(Py_hash_t hash) +{ + assert(hash != -1); + + Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash)); + return PyLong_FromLongLong(hash); +} + + static PyObject * hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -54,14 +65,27 @@ hash_pointer(PyObject *Py_UNUSED(module), PyObject *arg) } Py_hash_t hash = Py_HashPointer(ptr); - Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash)); - return PyLong_FromLongLong(hash); + return long_from_hash(hash); +} + + +static PyObject * +hash_double(PyObject *Py_UNUSED(module), PyObject *args) +{ + double value; + if (!PyArg_ParseTuple(args, "d", &value)) { + return NULL; + } + + Py_hash_t hash = Py_HashDouble(value); + return long_from_hash(hash); } static PyMethodDef test_methods[] = { {"hash_getfuncdef", hash_getfuncdef, METH_NOARGS}, {"hash_pointer", hash_pointer, METH_O}, + {"hash_double", hash_double, METH_VARARGS}, {NULL}, }; diff --git a/Python/pyhash.c b/Python/pyhash.c index 141407c265677a..f64edde4043185 100644 --- a/Python/pyhash.c +++ b/Python/pyhash.c @@ -84,17 +84,20 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0}; */ Py_hash_t -_Py_HashDouble(PyObject *inst, double v) +Py_HashDouble(double v) { int e, sign; double m; Py_uhash_t x, y; if (!Py_IS_FINITE(v)) { - if (Py_IS_INFINITY(v)) - return v > 0 ? _PyHASH_INF : -_PyHASH_INF; - else - return _Py_HashPointer(inst); + if (Py_IS_INFINITY(v)) { + return (v > 0 ? _PyHASH_INF : -_PyHASH_INF); + } + else { + assert(Py_IS_NAN(v)); + return 0; + } } m = frexp(v, &e); @@ -129,6 +132,19 @@ _Py_HashDouble(PyObject *inst, double v) return (Py_hash_t)x; } +Py_hash_t +_Py_HashDouble(PyObject *obj, double value) +{ + assert(obj != NULL); + + if (!Py_IS_NAN(value)) { + return Py_HashDouble(value); + } + else { + return Py_HashPointer(obj); + } +} + Py_hash_t Py_HashPointer(const void *ptr) {