From 359bb4df0d26075cd38575a61f669412144c2c72 Mon Sep 17 00:00:00 2001 From: 1ndahous3 <9205230+1ndahous3@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:27:04 +0300 Subject: [PATCH] Reuse stored hashes when merging dictionaries Assisted-by: Codex:GPT-6 --- crates/vm/src/builtins/dict.rs | 9 ++-- crates/vm/src/dict_inner.rs | 17 +++++-- extra_tests/snippets/builtin_dict.py | 72 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 1afb16c0316..11cb0d47f99 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -278,11 +278,14 @@ impl PyDict { ) -> PyResult<()> { let dict = &self.entries; let dict_size = &dict_other.size(); - for (key, value) in dict_other { - if !override_existing && dict.contains(vm, &*key)? { + let mut position = 0; + while let Some((next, key, value, hash)) = dict_other.entries.next_entry_with_hash(position) + { + position = next; + if !override_existing && dict.contains_known_hash(vm, &*key, hash)? { continue; } - dict.insert(vm, &*key, value)?; + dict.insert_known_hash(vm, &*key, hash, value)?; } if dict_other.entries.has_changed_size(dict_size) { return Err(vm.new_runtime_error("dict mutated during update")); diff --git a/crates/vm/src/dict_inner.rs b/crates/vm/src/dict_inner.rs index b2cf0788609..c80ae611e13 100644 --- a/crates/vm/src/dict_inner.rs +++ b/crates/vm/src/dict_inner.rs @@ -486,8 +486,9 @@ impl Dict { /// Store a key whose hash the caller already knows. /// /// `hash` must be computed for `key` or read from [`Self::keys_with_hashes`] - /// on a container holding this same key. A wrong hash lands the entry in a - /// bucket no lookup probes, silently losing the key. + /// or [`Self::next_entry_with_hash`] on a container holding this same key. + /// A wrong hash lands the entry in a bucket no lookup probes, silently + /// losing the key. pub(crate) fn insert_known_hash( &self, vm: &VirtualMachine, @@ -1037,13 +1038,21 @@ impl Dict { } } - pub(crate) fn next_entry(&self, mut position: EntryIndex) -> Option<(usize, PyObjectRef, T)> { + pub(crate) fn next_entry(&self, position: EntryIndex) -> Option<(usize, PyObjectRef, T)> { + self.next_entry_with_hash(position) + .map(|(position, key, value, _)| (position, key, value)) + } + + pub(crate) fn next_entry_with_hash( + &self, + mut position: EntryIndex, + ) -> Option<(usize, PyObjectRef, T, HashValue)> { let inner = self.read(); loop { let entry = inner.entries.get(position)?; position += 1; if let Some(entry) = entry { - break Some((position, entry.key.clone(), entry.value.clone())); + break Some((position, entry.key.clone(), entry.value.clone(), entry.hash)); } } } diff --git a/extra_tests/snippets/builtin_dict.py b/extra_tests/snippets/builtin_dict.py index 41ab8522754..53b7fd944f6 100644 --- a/extra_tests/snippets/builtin_dict.py +++ b/extra_tests/snippets/builtin_dict.py @@ -615,3 +615,75 @@ def __eq__(self, other): lookup = LookupOrderSubclass("lookup", True) assert {stored: 1}[lookup] == 1 assert LookupOrderKey.calls == ["lookup"] + + +class MergeHashKey: + hash_disabled = False + + def __init__(self, value): + self.value = value + + def __hash__(self): + assert not self.hash_disabled, "dictionary merge rehashed a stored key" + return 42 + + def __eq__(self, other): + if not isinstance(other, MergeHashKey): + return NotImplemented + return self.value == other.value + + +def merge_with_update(source): + result = {} + result.update(source) + return result + + +def merge_with_ior(source): + result = {} + result |= source + return result + + +# Exact dictionary merges reuse hashes, including colliding keys and holes. +merge_keys = [MergeHashKey(i) for i in range(3)] +merge_source = dict(zip(merge_keys, ("first", "removed", "last"))) +del merge_source[merge_keys[1]] +existing_merge_key = MergeHashKey(2) +merge_target = {existing_merge_key: "old"} +MergeHashKey.hash_disabled = True +for merge in ( + dict, + merge_with_update, + merge_with_ior, + lambda source: {} | source, + lambda source: dict.__ror__(source, {}), + lambda source: {**source}, +): + assert list(merge(merge_source).items()) == list(merge_source.items()) + +# Overwriting a matching key keeps its identity and position. +merge_target.update(merge_source) +assert next(iter(merge_target)) is existing_merge_key +assert list(merge_target.values()) == ["last", "first"] +merge_source.update(merge_source) +merge_source |= merge_source +assert list(merge_source.values()) == ["first", "last"] +MergeHashKey.hash_disabled = False + + +class MergeMapping(dict): + def __iter__(self): + return iter(("virtual",)) + + def keys(self): + return ["virtual"] + + def __getitem__(self, key): + assert key == "virtual" + return 42 + + +# Generic mappings retain their lookup hooks instead of exposing dict storage. +for merge in (dict, merge_with_update, merge_with_ior): + assert merge(MergeMapping(stored=0)) == {"virtual": 42}