Skip to content

Commit aba6644

Browse files
committed
Stop using pybind's implicit __hash__ (closes gh-102)
1 parent c3eb182 commit aba6644

File tree

3 files changed

+79
-5
lines changed

3 files changed

+79
-5
lines changed

gen_wrap.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,10 @@ def write_wrapper(outf, meth):
13701370

13711371
# {{{ exposer generator
13721372

1373+
def is_hash(meth):
1374+
return meth.name == "get_hash" and len(meth.args) == 1
1375+
1376+
13731377
def write_exposer(outf, meth, arg_names, doc_str):
13741378
func_name = f"isl::{meth.cls}_{meth.name}"
13751379
py_name = meth.name
@@ -1385,7 +1389,7 @@ def write_exposer(outf, meth, arg_names, doc_str):
13851389
if meth.name == "size" and len(meth.args) == 1:
13861390
py_name = "__len__"
13871391

1388-
if meth.name == "get_hash" and len(meth.args) == 1:
1392+
if is_hash(meth):
13891393
py_name = "__hash__"
13901394

13911395
extra_py_names = []
@@ -1509,6 +1513,16 @@ def gen_wrapper(include_dirs, include_barvinok=False, isl_version=None):
15091513
for cls in classes
15101514
for meth in fdata.classes_to_methods.get(cls, [])])
15111515

1516+
for cls in classes:
1517+
has_isl_hash = any(
1518+
is_hash(meth) for meth in fdata.classes_to_methods.get(cls, []))
1519+
1520+
if not has_isl_hash:
1521+
# pybind11's C++ object base class has an object identity
1522+
# __hash__ that everyone inherits automatically. We don't
1523+
# want that.
1524+
expf.write(f'wrap_{cls}.attr("__hash__") = py::none();\n')
1525+
15121526
expf.close()
15131527
wrapf.close()
15141528

islpy/__init__.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,6 @@ def generic_repr(self):
313313
cls.__str__ = generic_str
314314
cls.__repr__ = generic_repr
315315

316-
if not hasattr(cls, "__hash__"):
317-
raise AssertionError(f"not hashable: {cls}")
318-
319316
# }}}
320317

321318
# {{{ Python set-like behavior
@@ -349,6 +346,13 @@ def obj_sub(self, other):
349346

350347
# {{{ Space
351348

349+
def space_hash(self):
350+
return hash((type(self),
351+
self.dim(dim_type.param),
352+
self.dim(dim_type.in_),
353+
self.dim(dim_type.out),
354+
self.dim(dim_type.div)))
355+
352356
def space_get_id_dict(self, dimtype=None):
353357
"""Return a dictionary mapping variable :class:`Id` instances to tuples
354358
of (:class:`dim_type`, index).
@@ -446,6 +450,7 @@ def space_create_from_names(ctx, set=None, in_=None, out=None, params=()):
446450

447451
return result
448452

453+
Space.__hash__ = space_hash
449454
Space.create_from_names = staticmethod(space_create_from_names)
450455
Space.get_var_dict = space_get_var_dict
451456
Space.get_id_dict = space_get_id_dict
@@ -908,6 +913,12 @@ def val_to_python(self):
908913
# note: automatic upcasts for method arguments are provided through
909914
# 'implicitly_convertible' on the C++ side of the wrapper.
910915

916+
def make_upcasting_hash(special_method, upcast_method):
917+
def wrapper(basic_instance):
918+
return hash((type(basic_instance), upcast_method(basic_instance)))
919+
920+
return wrapper
921+
911922
def make_new_upcast_wrapper(method, upcast):
912923
# This function provides a scope in which method and upcast
913924
# are not changed from one iteration of the enclosing for
@@ -916,7 +927,6 @@ def make_new_upcast_wrapper(method, upcast):
916927
def wrapper(basic_instance, *args, **kwargs):
917928
special_instance = upcast(basic_instance)
918929
return method(special_instance, *args, **kwargs)
919-
920930
return wrapper
921931

922932
def make_existing_upcast_wrapper(basic_method, special_method, upcast):
@@ -938,6 +948,18 @@ def wrapper(basic_instance, *args, **kwargs):
938948
def add_upcasts(basic_class, special_class, upcast_method):
939949
from functools import update_wrapper
940950

951+
# {{{ implicitly upcast __hash__
952+
953+
# We don't use hasattr() here because in the C++ part of the wrapper
954+
# we overwrite pybind's unwanted default __hash__ implementation
955+
# with None.
956+
if (getattr(basic_class, "__hash__", None) is None
957+
and getattr(special_class, "__hash__", None) is not None):
958+
wrapper = make_upcasting_hash(special_class.__hash__, upcast_method)
959+
basic_class.__hash__ = update_wrapper(wrapper, basic_class.__hash__)
960+
961+
# }}}
962+
941963
def my_ismethod(class_, method_name):
942964
if method_name.startswith("_"):
943965
return False

test/test_isl.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,44 @@ def test_sched_constraints_set_validity():
457457
assert str(validity) == str(validity2)
458458

459459

460+
def test_space_hash():
461+
# Direct from-string Space constructors are recent and broke barvinok CI
462+
# which involves older isl versions.
463+
s1 = isl.Set("[n] -> {[i]: 1=1}").space
464+
s2 = isl.Set("[n] -> {[j]}: 1=1").space
465+
s3 = isl.Set("[n] -> {[j, k]: 1=1}").space
466+
s4 = isl.Set("[m, n] -> {[j, k]: 1=1}").space
467+
468+
i_id = isl.Id("i", context=isl.DEFAULT_CONTEXT)
469+
j_id = isl.Id("j", context=isl.DEFAULT_CONTEXT)
470+
471+
s1_i = s1.set_dim_id(isl.dim_type.set, 0, i_id)
472+
s2_i = s2.set_dim_id(isl.dim_type.set, 0, j_id)
473+
474+
def assert_equal(a, b):
475+
assert a == b
476+
assert hash(a) == hash(b)
477+
478+
def assert_not_equal(a, b):
479+
assert a != b
480+
# not guaranteed, but highly likely
481+
assert hash(a) != hash(b)
482+
483+
assert_equal(s1, s2)
484+
assert_equal(s1_i, s2_i)
485+
assert_not_equal(s3, s1)
486+
assert_not_equal(s4, s1)
487+
488+
489+
def test_basicset_hash():
490+
# https://github.com/inducer/islpy/issues/102
491+
# isl does not currently (2022-12-30) offer hashing for BasicSet.
492+
493+
a1 = isl.BasicSet("{[i]: 0<=i<512}")
494+
a2 = isl.BasicSet("{[i]: 0<=i<512}")
495+
assert hash(a1) == hash(a2)
496+
497+
460498
if __name__ == "__main__":
461499
import sys
462500
if len(sys.argv) > 1:

0 commit comments

Comments
 (0)