Skip to content

Commit ce427b2

Browse files
committed
Reuse stored hashes for frozenset hashing and in-place set union
Assisted-by: Codex:GPT-6
1 parent f871707 commit ce427b2

3 files changed

Lines changed: 61 additions & 28 deletions

File tree

‎crates/vm/src/builtins/set.rs‎

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -491,20 +491,6 @@ impl PySetInner {
491491
}
492492
}
493493

494-
fn update(
495-
&self,
496-
others: impl core::iter::Iterator<Item = ArgIterable>,
497-
vm: &VirtualMachine,
498-
) -> PyResult<()> {
499-
for iterable in others {
500-
for item in iterable.iter(vm)? {
501-
let item = item?;
502-
self.add(&item, vm)?;
503-
}
504-
}
505-
Ok(())
506-
}
507-
508494
fn update_internal(&self, iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
509495
// check AnySet
510496
if let Ok(any_set) = AnySet::try_from_object(vm, iterable.to_owned()) {
@@ -592,15 +578,15 @@ impl PySetInner {
592578
Ok(())
593579
}
594580

595-
fn hash(&self, vm: &VirtualMachine) -> PyResult<PyHash> {
596-
let hasher = self.content.try_fold_keys(
581+
fn hash(&self) -> PyHash {
582+
let hasher = self.content.fold_hashes(
597583
hash::FrozenSetHash::new(self.len()),
598-
|mut hasher, element| {
599-
hasher.add(element.hash(vm)?);
600-
Ok(hasher)
584+
|mut hasher, element_hash| {
585+
hasher.add(element_hash);
586+
hasher
601587
},
602-
)?;
603-
Ok(hasher.finish())
588+
);
589+
hasher.finish()
604590
}
605591

606592
// Run operation, on failure, if item is a set/set subclass, convert it
@@ -887,7 +873,7 @@ impl PySet {
887873
}
888874

889875
fn __ior__(zelf: PyRef<Self>, set: AnySet, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
890-
zelf.inner.update(set.into_iterable_iter(vm)?, vm)?;
876+
zelf.inner.merge_set(set, vm)?;
891877
Ok(zelf)
892878
}
893879

@@ -1424,10 +1410,10 @@ impl AsSequence for PyFrozenSet {
14241410

14251411
impl Hashable for PyFrozenSet {
14261412
#[inline]
1427-
fn hash(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyHash> {
1413+
fn hash(zelf: &crate::Py<Self>, _vm: &VirtualMachine) -> PyResult<PyHash> {
14281414
let hash = match zelf.hash.load(Ordering::Relaxed) {
14291415
hash::SENTINEL => {
1430-
let hash = zelf.inner.hash(vm)?;
1416+
let hash = zelf.inner.hash();
14311417
match Radium::compare_exchange(
14321418
&zelf.hash,
14331419
hash::SENTINEL,

‎crates/vm/src/dict_inner.rs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,15 +1105,16 @@ impl<T: Clone> Dict<T> {
11051105
.collect()
11061106
}
11071107

1108-
pub(crate) fn try_fold_keys<Acc, Fold>(&self, init: Acc, f: Fold) -> PyResult<Acc>
1108+
/// Fold stored hashes under the read lock; the callback must not run Python.
1109+
pub(crate) fn fold_hashes<Acc, Fold>(&self, init: Acc, f: Fold) -> Acc
11091110
where
1110-
Fold: FnMut(Acc, &PyObject) -> PyResult<Acc>,
1111+
Fold: FnMut(Acc, HashValue) -> Acc,
11111112
{
11121113
self.read()
11131114
.entries
11141115
.iter()
1115-
.filter_map(|v| v.as_ref().map(|v| v.key.as_object()))
1116-
.try_fold(init, f)
1116+
.filter_map(|v| v.as_ref().map(|v| v.hash))
1117+
.fold(init, f)
11171118
}
11181119

11191120
/// Lookup the index for the given key.

‎extra_tests/snippets/builtin_set.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,49 @@ def matched_then_error():
566566
dictionary = {key: None}
567567
key.hash_enabled = False
568568
assert_raises(AssertionError, source.intersection, dictionary)
569+
570+
571+
# Native operations reuse hashes stored when an element enters the collection.
572+
class RemainingHashKey:
573+
blocked = False
574+
575+
def __init__(self, value):
576+
self.value = value
577+
578+
def __hash__(self):
579+
assert not type(self).blocked, "stored key was hashed again"
580+
return 7
581+
582+
def __eq__(self, other):
583+
return isinstance(other, RemainingHashKey) and self.value == other.value
584+
585+
586+
class NativeSetSource(set):
587+
def __iter__(self):
588+
raise AssertionError("native set operation called __iter__")
589+
590+
591+
class NativeFrozenSetSource(frozenset):
592+
def __iter__(self):
593+
raise AssertionError("native set operation called __iter__")
594+
595+
596+
stored_keys = [RemainingHashKey(1), RemainingHashKey(2)]
597+
stored_frozen = frozenset(stored_keys)
598+
stored_frozen_reversed = frozenset(reversed(stored_keys))
599+
stored_sources = [
600+
source_type(stored_keys)
601+
for source_type in (set, frozenset, NativeSetSource, NativeFrozenSetSource)
602+
]
603+
RemainingHashKey.blocked = True
604+
stored_hash = hash(stored_frozen)
605+
assert stored_hash == hash(stored_frozen_reversed)
606+
assert stored_hash == hash(stored_frozen)
607+
for stored_source in stored_sources:
608+
stored_target = set()
609+
assert stored_target.__ior__(stored_source) is stored_target
610+
assert len(stored_target) == 2
611+
assert {item.value for item in stored_target} == {1, 2}
612+
stored_target |= stored_target
613+
assert len(stored_target) == 2
614+
RemainingHashKey.blocked = False

0 commit comments

Comments
 (0)