Skip to content

Commit 46fef17

Browse files
committed
gh-111545: Add PyHash_Double() function
* Cleanup PyHash_Double() implementation based _Py_HashDouble(): * Move variable declaration to their first assignment. * Add braces (PEP 7). * Cast result to signed Py_hash_t before the final "== -1" test, to reduce the number of casts. * Add an assertion on Py_IS_NAN(v) in the only code path which can return -1. * Add tests: Modules/_testcapi/hash.c and Lib/test/test_capi/test_hash.py.
1 parent 55f3cce commit 46fef17

File tree

7 files changed

+129
-18
lines changed

7 files changed

+129
-18
lines changed

Doc/c-api/hash.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ PyHash API
55

66
See also the :c:member:`PyTypeObject.tp_hash` member.
77

8+
Types
9+
^^^^^
10+
811
.. c:type:: Py_hash_t
912
1013
Hash value type: signed integer.
1114

1215
.. versionadded:: 3.2
1316

17+
1418
.. c:type:: Py_uhash_t
1519
1620
Hash value type: unsigned integer.
@@ -41,8 +45,23 @@ See also the :c:member:`PyTypeObject.tp_hash` member.
4145
.. versionadded:: 3.4
4246

4347

48+
Functions
49+
^^^^^^^^^
50+
51+
.. c:function:: Py_hash_t PyHash_Double(double value)
52+
53+
Hash a C double number.
54+
55+
Return ``-1`` if *value* is not-a-number (NaN).
56+
57+
.. versionadded:: 3.13
58+
59+
4460
.. c:function:: PyHash_FuncDef* PyHash_GetFuncDef(void)
4561
4662
Get the hash function definition.
4763
64+
.. seealso::
65+
:pep:`456` "Secure and interchangeable hash algorithm".
66+
4867
.. versionadded:: 3.4

Doc/whatsnew/3.13.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,9 @@ New Features
11811181
:exc:`KeyError` if the key missing.
11821182
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)
11831183

1184+
* Add :c:func:`PyHash_Double` function to hash a C double number.
1185+
(Contributed by Victor Stinner in :gh:`111545`.)
1186+
11841187

11851188
Porting to Python 3.13
11861189
----------------------

Include/cpython/pyhash.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ typedef struct {
1111
} PyHash_FuncDef;
1212

1313
PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);
14+
15+
PyAPI_FUNC(Py_hash_t) PyHash_Double(double value);

Lib/test/test_capi/test_hash.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
import sys
23
import unittest
34
from test.support import import_helper
@@ -31,3 +32,49 @@ def test_hash_getfuncdef(self):
3132
self.assertEqual(func_def.name, hash_info.algorithm)
3233
self.assertEqual(func_def.hash_bits, hash_info.hash_bits)
3334
self.assertEqual(func_def.seed_bits, hash_info.seed_bits)
35+
36+
def test_hash_double(self):
37+
# Test PyHash_Double()
38+
hash_double = _testcapi.hash_double
39+
40+
# test integers
41+
def python_hash_int(x):
42+
negative = (x < 0)
43+
x = abs(x) % sys.hash_info.modulus
44+
if negative:
45+
x = -x
46+
if x == -1:
47+
x = -2
48+
return x
49+
50+
integers = [
51+
*range(1, 30),
52+
2**30 - 1,
53+
2 ** 233,
54+
int(sys.float_info.max),
55+
]
56+
integers.extend([-x for x in integers])
57+
integers.append(0)
58+
59+
for x in integers:
60+
self.assertEqual(hash_double(float(x)), python_hash_int(x), x)
61+
62+
# test non-finite values
63+
self.assertEqual(hash_double(float('inf')), sys.hash_info.inf)
64+
self.assertEqual(hash_double(float('-inf')), -sys.hash_info.inf)
65+
self.assertEqual(hash_double(float('nan')), -1)
66+
67+
# special values: compare with Python hash() function
68+
def python_hash_double(x):
69+
return hash(x)
70+
71+
special_values = (
72+
sys.float_info.max,
73+
sys.float_info.min,
74+
sys.float_info.epsilon,
75+
math.nextafter(0.0, 1.0),
76+
)
77+
for x in special_values:
78+
with self.subTest(x=x):
79+
self.assertEqual(hash_double(x), python_hash_double(x))
80+
self.assertEqual(hash_double(-x), python_hash_double(-x))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyHash_Double` function to hash a C double number. Patch by
2+
Victor Stinner.

Modules/_testcapi/hash.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "parts.h"
22
#include "util.h"
33

4+
45
static PyObject *
56
hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
67
{
@@ -44,8 +45,23 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
4445
return result;
4546
}
4647

48+
49+
static PyObject *
50+
hash_double(PyObject *Py_UNUSED(module), PyObject *args)
51+
{
52+
double value;
53+
if (!PyArg_ParseTuple(args, "d", &value)) {
54+
return NULL;
55+
}
56+
Py_hash_t hash = PyHash_Double(value);
57+
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
58+
return PyLong_FromLongLong(hash);
59+
}
60+
61+
4762
static PyMethodDef test_methods[] = {
4863
{"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
64+
{"hash_double", hash_double, METH_VARARGS},
4965
{NULL},
5066
};
5167

Python/pyhash.c

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,49 +86,71 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0};
8686
Py_hash_t _Py_HashPointer(const void *);
8787

8888
Py_hash_t
89-
_Py_HashDouble(PyObject *inst, double v)
89+
PyHash_Double(double v)
9090
{
91-
int e, sign;
92-
double m;
93-
Py_uhash_t x, y;
94-
9591
if (!Py_IS_FINITE(v)) {
96-
if (Py_IS_INFINITY(v))
92+
if (Py_IS_INFINITY(v)) {
9793
return v > 0 ? _PyHASH_INF : -_PyHASH_INF;
98-
else
99-
return _Py_HashPointer(inst);
94+
}
95+
else {
96+
assert(Py_IS_NAN(v));
97+
return -1;
98+
}
10099
}
101100

102-
m = frexp(v, &e);
103-
104-
sign = 1;
101+
int e;
102+
double m = frexp(v, &e);
103+
int sign;
105104
if (m < 0) {
106105
sign = -1;
107106
m = -m;
108107
}
108+
else {
109+
sign = 1;
110+
}
109111

110112
/* process 28 bits at a time; this should work well both for binary
111113
and hexadecimal floating point. */
112-
x = 0;
114+
Py_uhash_t x = 0;
113115
while (m) {
114116
x = ((x << 28) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - 28);
115117
m *= 268435456.0; /* 2**28 */
116118
e -= 28;
117-
y = (Py_uhash_t)m; /* pull out integer part */
119+
120+
Py_uhash_t y = (Py_uhash_t)m; /* pull out integer part */
118121
m -= y;
119122
x += y;
120-
if (x >= _PyHASH_MODULUS)
123+
if (x >= _PyHASH_MODULUS) {
121124
x -= _PyHASH_MODULUS;
125+
}
122126
}
123127

124128
/* adjust for the exponent; first reduce it modulo _PyHASH_BITS */
125-
e = e >= 0 ? e % _PyHASH_BITS : _PyHASH_BITS-1-((-1-e) % _PyHASH_BITS);
129+
if (e >= 0) {
130+
e = e % _PyHASH_BITS;
131+
}
132+
else {
133+
e = _PyHASH_BITS - 1 - ((-1 - e) % _PyHASH_BITS);
134+
}
126135
x = ((x << e) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - e);
127136

128137
x = x * sign;
129-
if (x == (Py_uhash_t)-1)
130-
x = (Py_uhash_t)-2;
131-
return (Py_hash_t)x;
138+
139+
Py_hash_t result = (Py_hash_t)x;
140+
if (result == -1) {
141+
result = -2;
142+
}
143+
return (Py_hash_t)result;
144+
}
145+
146+
Py_hash_t
147+
_Py_HashDouble(PyObject *inst, double v)
148+
{
149+
Py_hash_t hash = PyHash_Double(v);
150+
if (hash == -1) {
151+
return _Py_HashPointer(inst);
152+
}
153+
return hash;
132154
}
133155

134156
Py_hash_t

0 commit comments

Comments
 (0)