Sitelet https://github.com/opal/opal/pull/2802
Skip to content

Keep String keys distinct in a compare_by_identity Hash - #2802

Merged
elia merged 1 commit into
masterfrom
elia/2615-compare-by-identity-string-keys
Sep 11, 2026
Merged

elia merged 1 commit into
masterfrom
elia/2615-compare-by-identity-string-keys

Conversation

@elia

@elia elia commented Sep 10, 2026

Copy link
Copy Markdown
Member

Fixes #2615.

What

A String key was unwrapped to its primitive value (key.valueOf()) before being stored in the hash's underlying Map, so a compare_by_identity hash keyed such entries by value. A duped key then found the entry stored under the original:

str1 = "foo"
str2 = str1.dup
h = {}.compare_by_identity
h[str1] = 1
p [h[str1], h[str2]]  # Opal: [1, 1] — MRI: [1, nil]

Why this fix

When the hash compares by identity, the String object is now used as the Map key as-is. Map compares 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_delete and hash_rehash are 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 $prop cannot define a property on a primitive under use_strict.

Scope / known limitation

This fixes String objects — those from dup, 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 already true and object_id is identical, independent of Hash. 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

  • Unfiltered 2 previously failing specs — Set#compare_by_identity regards #dup'd objects as having different identities and the #clone'd equivalent — which this fixes for free, as Set is Hash-backed.
  • Full rake mspec_node green: 14626 + 693 examples, 0 failures.
  • Verified no regressions by running hash, string, array and set suites against a stashed tree and comparing example and failure counts: Hash 532/4 → 532/4 (all ruby2_keywords_hash, pre-existing), String 1362/7 unchanged, Array 1269/32 unchanged, Set 264/14 → 268/14 (+4 examples from the unfiltered specs).
  • Each behavior change was checked against MRI directly, including the corner case of calling compare_by_identity on an already-populated hash (moves toward MRI: [1, 2, 2] → [1, 2, nil], MRI [1, nil, nil]).

🤖 Generated with Claude Code

@elia elia self-assigned this Sep 11, 2026
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
elia force-pushed the elia/2615-compare-by-identity-string-keys branch from 3aff50c to 9a92e36 Compare September 11, 2026 14:46
@elia
elia merged commit 3ee76d7 into master Sep 11, 2026
26 checks passed
@elia
elia deleted the elia/2615-compare-by-identity-string-keys branch September 11, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Hash#compare_by_identity does not work with string keys

1 participant