-
-
Notifications
You must be signed in to change notification settings - Fork 939
Description
Environment
- JRuby 10.0.2.0 (Ruby 3.4.2 compat)
- macOS / aarch64
Reproduction
Put this in a script:
# Reproduction script for JRuby Data#== bug when instances use `extend` in `initialize`.
#
# Expected: all assertions pass (as they do on CRuby/MRI).
# Actual on JRuby 10.0.2.0: == and eql? return false for identical Data instances.
module MyModule
def greet
"hello"
end
end
D = Data.define(:a, :b) do
def initialize(a:, b:)
extend(MyModule)
super(a: a, b: b)
end
end
x = D.new(a: 1, b: 2)
y = D.new(a: 1, b: 2)
puts "RUBY_ENGINE: #{RUBY_ENGINE}"
puts "RUBY_VERSION: #{RUBY_VERSION}"
puts "JRUBY_VERSION: #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : "N/A"}"
puts
# These all correctly return true:
puts "x.a == y.a: #{x.a == y.a}" # true
puts "x.b == y.b: #{x.b == y.b}" # true
puts "x.to_h == y.to_h: #{x.to_h == y.to_h}" # true
puts "x.deconstruct == y.deconstruct: #{x.deconstruct == y.deconstruct}" # true
puts "x.hash == y.hash: #{x.hash == y.hash}" # true
puts "x.class == y.class: #{x.class == y.class}" # true
puts
# These return false on JRuby, true on MRI:
puts "x == y: #{x == y}" # BUG: false on JRuby
puts "x.eql?(y): #{x.eql?(y)}" # BUG: false on JRuby
puts
# Root cause: each `extend` creates a unique singleton class, and JRuby's
# Data#== (in RubyData.java) compares getMetaClass() (singleton class) instead
# of getType() (real class). Data#hash correctly uses getType().
puts "x.singleton_class.equal?(y.singleton_class): #{x.singleton_class.equal?(y.singleton_class)}" # false
# Without extend, everything works:
E = Data.define(:a, :b)
x2 = E.new(a: 1, b: 2)
y2 = E.new(a: 1, b: 2)
puts
puts "Without extend: x2 == y2: #{x2 == y2}" # true
# Struct has the same bug for eql? (but == works):
S = Struct.new(:a, :b) do
def initialize(a, b)
extend(MyModule)
super(a, b)
end
end
s1 = S.new(1, 2)
s2 = S.new(1, 2)
puts
puts "Struct with extend:"
puts " s1 == s2: #{s1 == s2}" # true (Struct#== not affected)
puts " s1.eql?(s2): #{s1.eql?(s2)}" # BUG: false on JRuby
puts " s1.hash == s2.hash: #{s1.hash == s2.hash}" # trueExpected behavior (MRI)
Running the script on Ruby 4.0 produces:
RUBY_ENGINE: ruby
RUBY_VERSION: 4.0.0
JRUBY_VERSION: N/A
x.a == y.a: true
x.b == y.b: true
x.to_h == y.to_h: true
x.deconstruct == y.deconstruct: true
x.hash == y.hash: true
x.class == y.class: true
x == y: true
x.eql?(y): true
x.singleton_class.equal?(y.singleton_class): false
Without extend: x2 == y2: true
Struct with extend:
s1 == s2: true
s1.eql?(s2): true
s1.hash == s2.hash: true
Actual behavior (JRuby 10.0.2.0)
Running the script on JRuby produces:
RUBY_ENGINE: jruby
RUBY_VERSION: 3.4.2
JRUBY_VERSION: 10.0.2.0
x.a == y.a: true
x.b == y.b: true
x.to_h == y.to_h: true
x.deconstruct == y.deconstruct: true
x.hash == y.hash: true
x.class == y.class: true
x == y: false
x.eql?(y): false
x.singleton_class.equal?(y.singleton_class): false
Without extend: x2 == y2: true
Struct with extend:
s1 == s2: true
s1.eql?(s2): false
s1.hash == s2.hash: true
Struct has the same bug (partially)
Struct#eql? has the same getMetaClass() issue. Struct#== is unaffected (it apparently uses getType()).
S = Struct.new(:a, :b) do
def initialize(a, b)
extend(M)
super(a, b)
end
end
s1 = S.new(1, 2)
s2 = S.new(1, 2)
s1 == s2 # => true (Struct#== not affected)
s1.eql?(s2) # => false (expected: true)
s1.hash == s2.hash # => trueThe same getMetaClass() → getType() fix should be applied to RubyStruct#eql? as well.
Root cause
In RubyData.java, checkDataEquality compares instances using getMetaClass():
RubyClass metaClass = otherObj.getMetaClass();
if (metaClass != selfObj.getMetaClass()) return context.fals;When extend(M) is called on a Data instance, it creates a unique singleton class. Two instances extended with the same module get different singleton classes, so getMetaClass() returns different objects, causing == to return false.
The fix should use getType() instead of getMetaClass(), which returns the real class ignoring singleton classes. This is already what Data#hash does:
selfObj.getType().hashCode()This inconsistency (hash uses getType(), equality uses getMetaClass()) also means two "unequal" objects can have the same hash, which violates the contract that a.eql?(b) implies a.hash == b.hash (the converse is allowed but confusing).