Skip to content

Commit

Permalink
pythongh-111545: Add Py_HashDouble() function
Browse files Browse the repository at this point in the history
Add tests: Modules/_testcapi/hash.c and
Lib/test/test_capi/test_hash.py.
  • Loading branch information
vstinner committed Dec 14, 2023
1 parent 6873555 commit 9b00e3e
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 7 deletions.
27 changes: 27 additions & 0 deletions Doc/c-api/hash.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
<sys.hash_info>`.
* If *value* is negative infinity, return :data:`-sys.hash_info.inf
<sys.hash_info>`.
* If *value* is not-a-number (NaN), return :data:`sys.hash_info.nan
<sys.hash_info>` (``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.
Expand Down
15 changes: 15 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,21 @@ 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::

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
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/pyhash.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
41 changes: 41 additions & 0 deletions Lib/test/test_capi/test_hash.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import sys
import unittest
from test.support import import_helper
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`Py_HashDouble` function to hash a C double number. Patch by
Victor Stinner.
28 changes: 26 additions & 2 deletions Modules/_testcapi/hash.c
Original file line number Diff line number Diff line change
@@ -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))
{
Expand Down Expand Up @@ -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},
};

Expand Down
26 changes: 21 additions & 5 deletions Python/pyhash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down

0 comments on commit 9b00e3e

Please sign in to comment.