Project

General

Profile

Actions

Bug #22174

open

Set operations (&, ^, collect!, flatten, classify, divide) do not preserve compare_by_identity

Bug #22174: Set operations (&, ^, collect!, flatten, classify, divide) do not preserve compare_by_identity

Added by gil.desmarais (Gil Desmarais) 2 days ago. Updated 2 days ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]
[ruby-core:125919]

Description

Since Set was transitioned to a C-level core class in Ruby 4.0, several operations that allocate new sets or partition subsets (&, ^, collect!, flatten, classify, and divide) lost the compare_by_identity behavior for the receiver set or its subsets.

  • For &, collect!, flatten, classify, and divide, the resulting sets/subsets are allocated without propagating the compare_by_identity flag (returning false for #compare_by_identity?).
  • For ^ (XOR) with a generic Enumerable, the returned set has the flag set (via dup), but it allocates a temporary set tmp to hold the RHS elements without propagating the compare_by_identity flag. This causes the RHS elements to be deduplicated incorrectly using value equality instead of identity equality.

A pull request has been opened with the fix and tests: https://github.com/ruby/ruby/pull/17633

Reproduction

require 'set'

# 1. Intersection &
s1 = Set.new.compare_by_identity
s2 = Set.new
s1 << "a"
s2 << "a"
puts "Intersection preserves compare_by_identity: #{(s1 & s2).compare_by_identity?}"
# Expected: true
# Actual:   false

# 2. XOR ^ (with duplicate objects by value on RHS)
s_xor = Set.new.compare_by_identity
x1 = +"x"
x2 = +"x"
result = s_xor ^ [x1, x2]
puts "XOR with Enumerable preserves identity comparison: #{result.size == 2}"
# Expected: true (size should be 2, because x1 and x2 are distinct objects)
# Actual:   false (size is 1)

# 3. collect! / map!
s_collect = Set.new(["a", "b"]).compare_by_identity
s_collect.collect! { |x| x }
puts "collect! preserves compare_by_identity: #{s_collect.compare_by_identity?}"
# Expected: true
# Actual:   false

# 4. flatten
s_flat = Set.new([Set.new([1])]).compare_by_identity
puts "flatten preserves compare_by_identity: #{s_flat.flatten.compare_by_identity?}"
# Expected: true
# Actual:   false

# 5. classify
s_classify = Set.new(["a", "b"]).compare_by_identity
classified = s_classify.classify { |x| x }
puts "classify subsets preserve compare_by_identity: #{classified.values.all?(&:compare_by_identity?)}"
# Expected: true
# Actual:   false

# 6. divide
s_divide = Set.new(["a", "b"]).compare_by_identity
divided = s_divide.divide { |x| x }
puts "divide subsets preserve compare_by_identity: #{divided.all?(&:compare_by_identity?)}"
# Expected: true
# Actual:   false

Updated by jeremyevans0 (Jeremy Evans) 2 days ago Actions #1

  • Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED
Actions

Also available in: PDF Atom