Keep String keys distinct in a compare_by_identity Hash - #2802
Merged
Merged
Conversation
A String key was unwrapped to its primitive value before being stored in
the underlying Map, so an identity hash keyed it by value: a dup'ed key
found the entry stored under the original.
str1 = "foo"
str2 = str1.dup
h = {}.compare_by_identity
h[str1] = 1
h[str2] # => 1, MRI returns nil
When the hash compares by identity, use the String object itself as the
Map key. Map compares keys with SameValueZero, which is the identity
semantics we want, so such keys also need no entry in $$keys — indexing
them there by value would defeat the very thing we're preserving.
This also fixes the dup'ed and clone'd cases of Set#compare_by_identity,
which is backed by a Hash.
String keys that are JS primitives are unaffected: Opal compiles equal
string literals to the same primitive and Opal.id raises on them, so
distinguishing two such literals needs a change to how literals are
compiled. "Hash#compare_by_identity gives different identity for string
literals" stays filtered for that reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
elia
force-pushed
the
elia/2615-compare-by-identity-string-keys
branch
from
September 11, 2026 14:46
3aff50c to
9a92e36
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2615.
What
A
Stringkey was unwrapped to its primitive value (key.valueOf()) before being stored in the hash's underlyingMap, so acompare_by_identityhash keyed such entries by value. Aduped key then found the entry stored under the original:Why this fix
When the hash compares by identity, the
Stringobject is now used as theMapkey as-is.Mapcompares keys with SameValueZero, which is exactly the identity semantics wanted here — so no separate identity table is needed. Consequently such keys also get no entry in$$keys: indexing them there by value would defeat the property being preserved.hash_put,hash_get,hash_deleteandhash_rehashare updated consistently.Note this deliberately does not route string keys through
Opal.id: that raises on a primitive string (Cannot create property '$$id' on string), since$propcannot define a property on a primitive underuse_strict.Scope / known limitation
This fixes
Stringobjects — those fromdup,clone, etc., which have real identity.It does not fix equal string literals, which Opal compiles to the same JS primitive:
'foo'.equal?('foo')is alreadytrueandobject_idis identical, independent ofHash. Distinguishing two literals requires changing how string literals are compiled, so"Hash#compare_by_identity gives different identity for string literals"stays filtered, now with that as the reason.Testing
Set#compare_by_identityregards #dup'd objects as having different identitiesand the#clone'dequivalent — which this fixes for free, asSetisHash-backed.rake mspec_nodegreen: 14626 + 693 examples, 0 failures.hash,string,arrayandsetsuites against a stashed tree and comparing example and failure counts:Hash532/4 → 532/4 (allruby2_keywords_hash, pre-existing),String1362/7 unchanged,Array1269/32 unchanged,Set264/14 → 268/14 (+4 examples from the unfiltered specs).compare_by_identityon an already-populated hash (moves toward MRI:[1, 2, 2]→[1, 2, nil], MRI[1, nil, nil]).🤖 Generated with Claude Code