Skip to content

Commit 0f64063

Browse files
committed
[GR-19220] Fix singleton class lookup bug when singleton class is replaced on the instance (#3088)
PullRequest: truffleruby/3860
2 parents e0c429e + 400a93a commit 0f64063

File tree

7 files changed

+59
-4
lines changed

7 files changed

+59
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ New features:
66
Bug fixes:
77

88
* Fix `Dir.glob` returning blank string entry with leading `**/` in glob and `base:` argument (@rwstauner).
9+
* Fix class lookup after an object's class has been replaced by `IO#reopen` (@itarato, @eregon).
910

1011
Compatibility:
1112

spec/ruby/core/kernel/singleton_class_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# truffleruby_primitives: true
12
require_relative '../../spec_helper'
23

34
describe "Kernel#singleton_class" do
@@ -42,4 +43,32 @@
4243
obj.freeze
4344
obj.singleton_class.frozen?.should be_true
4445
end
46+
47+
context "for an IO object with a replaced singleton class" do
48+
it "looks up singleton methods from the fresh singleton class after an object instance got a new one" do
49+
proxy = -> (io) { io.foo }
50+
if RUBY_ENGINE == 'truffleruby'
51+
# We need an inline cache with only this object seen, the best way to do that is to use a Primitive
52+
sclass = -> (io) { Primitive.singleton_class(io) }
53+
else
54+
sclass = -> (io) { io.singleton_class }
55+
end
56+
57+
io = File.new(__FILE__)
58+
io.define_singleton_method(:foo) { "old" }
59+
sclass1 = sclass.call(io)
60+
proxy.call(io).should == "old"
61+
62+
# IO#reopen is the only method which can replace an object's singleton class
63+
io2 = File.new(__FILE__)
64+
io.reopen(io2)
65+
io.define_singleton_method(:foo) { "new" }
66+
sclass2 = sclass.call(io)
67+
sclass2.should_not.equal?(sclass1)
68+
proxy.call(io).should == "new"
69+
ensure
70+
io2.close
71+
io.close
72+
end
73+
end
4574
end

src/main/java/org/truffleruby/core/VMPrimitiveNodes.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@
6868
import org.truffleruby.core.proc.ProcOperations;
6969
import org.truffleruby.core.proc.RubyProc;
7070
import org.truffleruby.core.string.RubyString;
71+
import org.truffleruby.core.support.RubyIO;
7172
import org.truffleruby.core.symbol.RubySymbol;
7273
import org.truffleruby.core.thread.RubyThread;
7374
import org.truffleruby.extra.ffi.Pointer;
7475
import org.truffleruby.interop.TranslateInteropExceptionNode;
75-
import org.truffleruby.language.RubyDynamicObject;
7676
import org.truffleruby.language.RubyGuards;
7777
import org.truffleruby.language.SafepointAction;
7878
import org.truffleruby.language.arguments.ArgumentsDescriptor;
@@ -423,9 +423,10 @@ protected Object getSection(Object section, RubyProc block,
423423
@Primitive(name = "vm_set_class")
424424
public abstract static class VMSetClassNode extends PrimitiveArrayArgumentsNode {
425425

426+
/** Only support it on IO for IO#reopen since this is the only case which needs it */
426427
@TruffleBoundary
427428
@Specialization
428-
protected RubyDynamicObject setClass(RubyDynamicObject object, RubyClass newClass) {
429+
protected RubyIO setClass(RubyIO object, RubyClass newClass) {
429430
SharedObjects.propagate(getLanguage(), object, newClass);
430431
synchronized (object) {
431432
object.setMetaClass(newClass);

src/main/java/org/truffleruby/core/support/TypeNodes.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.truffleruby.language.objects.IsFrozenNode;
5050
import org.truffleruby.language.objects.LogicalClassNode;
5151
import org.truffleruby.language.objects.MetaClassNode;
52+
import org.truffleruby.language.objects.SingletonClassNode;
5253
import org.truffleruby.language.objects.WriteObjectFieldNode;
5354

5455
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
@@ -103,6 +104,15 @@ protected RubyClass metaClass(Object object,
103104
}
104105
}
105106

107+
@Primitive(name = "singleton_class")
108+
public abstract static class SingletonClassPrimitiveNode extends PrimitiveArrayArgumentsNode {
109+
@Specialization
110+
protected RubyClass singletonClass(Object object,
111+
@Cached SingletonClassNode singletonClassNode) {
112+
return singletonClassNode.executeSingletonClass(object);
113+
}
114+
}
115+
106116
@Primitive(name = "equal?")
107117
public abstract static class EqualPrimitiveNode extends PrimitiveArrayArgumentsNode {
108118
@Specialization

src/main/java/org/truffleruby/language/RubyGuards.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.truffleruby.core.regexp.RubyRegexp;
3434
import org.truffleruby.core.string.ImmutableRubyString;
3535
import org.truffleruby.core.string.RubyString;
36+
import org.truffleruby.core.support.RubyIO;
3637
import org.truffleruby.core.symbol.RubySymbol;
3738
import org.truffleruby.interop.ToJavaStringNode;
3839
import org.truffleruby.language.arguments.ArgumentsDescriptor;
@@ -137,6 +138,11 @@ public static boolean isRubyModule(Object value) {
137138
return value instanceof RubyModule;
138139
}
139140

141+
@Idempotent
142+
public static boolean isRubyIO(Object value) {
143+
return value instanceof RubyIO;
144+
}
145+
140146
public static boolean isRubyRegexp(Object value) {
141147
return value instanceof RubyRegexp;
142148
}

src/main/java/org/truffleruby/language/objects/MetaClassNode.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ public final RubyClass executeCached(Object value) {
4242
public abstract RubyClass execute(Node node, Object value);
4343

4444
@Specialization(
45-
guards = { "isSingleContext()", "object == cachedObject", "metaClass.isSingleton" },
45+
guards = {
46+
"isSingleContext()",
47+
"object == cachedObject",
48+
"metaClass.isSingleton",
49+
"!isRubyIO(cachedObject)" },
4650
limit = "1")
4751
protected static RubyClass singleton(Node node, RubyDynamicObject object,
4852
@Cached("object") RubyDynamicObject cachedObject,

src/main/java/org/truffleruby/language/objects/SingletonClassNode.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ protected RubyClass singletonClassClassUncached(RubyClass rubyClass) {
6060

6161
@Specialization(
6262
// no need to guard on the context, the RubyDynamicObject is context-specific
63-
guards = { "isSingleContext()", "object == cachedObject", "!isRubyClass(cachedObject)" },
63+
guards = {
64+
"isSingleContext()",
65+
"object == cachedObject",
66+
"!isRubyClass(cachedObject)",
67+
"!isRubyIO(cachedObject)" },
6468
limit = "1")
6569
protected RubyClass singletonClassInstanceCached(RubyDynamicObject object,
6670
@Cached("object") RubyDynamicObject cachedObject,

0 commit comments

Comments
 (0)