Skip to content

Commit 5e1d0f0

Browse files
committed
[GR-33155] Implement #instance_variables and #methods on foreign objects and remove #keys, respond_to?(:keys || :class)
PullRequest: truffleruby/2879
2 parents fb6e9d9 + 5fbccd8 commit 5e1d0f0

File tree

11 files changed

+72
-56
lines changed

11 files changed

+72
-56
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ New features:
99
* Foreign arrays now have all methods of Ruby `Enumerable` and many methods of `Array` (#2149).
1010
* Foreign hashes now have all methods of Ruby `Enumerable` and many methods of `Hash` (#2149).
1111
* Foreign iterables (`InteropLibrary#hasIterator`) now have all methods of Ruby `Enumerable` (#2149).
12+
* Foreign objects now implement `#instance_variables` (readable non-invocable members) and `#methods` (invocable members + Ruby methods).
1213

1314
Bug fixes:
1415

@@ -47,6 +48,8 @@ Changes:
4748

4849
* `foreign_object.class` on foreign objects is no longer special and uses `Kernel#class` (it used to return the `java.lang.Class` object for a Java type or `getMetaObject()`, but that is too incompatible with Ruby code).
4950
* `Java.import name` imports a Java class in the enclosing module instead of always as a top-level constant.
51+
* `foreign_object.keys` no longer returns members, use `foreign_object.instance_variables` or `foreign_object.methods` instead.
52+
* `foreign_object.respond_to?(:class)` is now always true (before it was only for Java classes), since the method is always defined.
5053

5154
# 21.2.0
5255

doc/contributor/interop.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,6 @@ runtime object instance.
113113

114114
`foreign_object.kind_of?` works like `foreign_object.is_a?`.
115115

116-
`object.respond_to?(:class)` calls `Truffle::Interop.java_class?(object)`.
117-
118116
`object.respond_to?(name)` for other names returns `false`.
119117

120118
`object.__send__(name, *args)` works in the same way as literal method call on

doc/contributor/interop_implicit_api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Format: `Ruby code` sends `InteropLibrary message`
2222
- `foreign_object.call(*arguments)` sends `execute(foreign_object, *arguments)`
2323
- `foreign_object.nil?` sends `isNull(foreign_object)`
2424
- `foreign_object.size` sends `getArraySize(foreign_object)`
25-
- `foreign_object.keys` sends `getMembers(foreign_object)`
25+
- `foreign_object.instance_variables` sends `getMembers(foreign_object)` and returns readable non-invocable members
26+
- `foreign_object.methods` sends `getMembers(foreign_object)` and returns invocable members merged with available Ruby methods
2627
- `foreign_object.method_name` sends `invokeMember(foreign_object, method_name)` if member is invocable
2728
- `foreign_object.method_name` sends `readMember(foreign_object, method_name)` if member is readable but not invocable
2829
- `foreign_object.method_name` sends `readMember(foreign_object, method_name)` and raises if member is neither invocable nor readable
@@ -56,6 +57,5 @@ Use `.respond_to?` for calling `InteropLibrary` predicates:
5657
- `foreign_object.respond_to?(:to_f)` sends `fitsInDouble()`
5758
- `foreign_object.respond_to?(:to_i)` sends `fitsInLong()`
5859
- `foreign_object.respond_to?(:size)` sends `hasArrayElements(foreign_object)`
59-
- `foreign_object.respond_to?(:keys)` sends `hasMembers(foreign_object)`
6060
- `foreign_object.respond_to?(:call)` sends `isExecutable(foreign_object)`
6161
- `foreign_object.respond_to?(:new)` sends `isInstantiable(foreign_object)`

doc/user/polyglot.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,6 @@ If you want to pass a Ruby object to another language for fields to be read and
110110

111111
`object.respond_to?(:new)` will tell you if a foreign object can be used to create a new object (if it's a class).
112112

113-
`object.respond_to?(:keys)` will tell you if a foreign object can give you a list of members.
114-
115-
`object.respond_to?(:class)` will tell you if an object is a Java class.
116-
117113
`Polyglot.as_enumerable(object)` will create a Ruby `Enumerable` from the foreign object, using its size or length, and reading from it.
118114

119115
Where boolean value is expected (e.g., in `if` conditions) the foreign value is converted to boolean if possible or considered to be true.

spec/truffle/interop/foreign_inspect_to_s_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,18 @@
5353
end
5454
end
5555

56-
describe "Java j.l.Class instance" do
56+
describe "Java java.lang.Class instance" do
5757
it "gives a similar representation to Ruby" do
5858
foreign = Truffle::Interop.java_type("java.math.BigInteger")[:class]
59-
foreign.inspect.should =~ /\A#<Polyglot::ForeignInstantiable\[Java\] java.lang.Class:0x\h+ java.math.BigInteger>\z/
59+
foreign.inspect.should =~ /\A#<Polyglot::ForeignInstantiable\[Java\] java.lang.Class:0x\h+ java.math.BigInteger static={...}>\z/
6060
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] java.math.BigInteger>"
6161
end
6262
end
6363

6464
describe "Java object" do
6565
it "gives a similar representation to Ruby" do
6666
foreign = Truffle::Interop.java_type("java.math.BigInteger").new('14')
67-
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject\[Java\] java\.math\.BigInteger:0x\h+ .+>\z/
67+
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject\[Java\] java\.math\.BigInteger:0x\h+>\z/
6868
foreign.to_s.should == "#<Polyglot::ForeignObject[Java] 14>"
6969
end
7070
end

spec/truffle/interop/respond_to_spec.rb

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,4 @@
6262
Truffle::Interop.java_array(1, 2, 3).should.respond_to?(:is_a?)
6363
end
6464
end
65-
66-
describe "for :class" do
67-
it "and a Java class returns true" do
68-
Truffle::Debug.java_class.should.respond_to?(:class)
69-
end
70-
71-
it "and a Java object returns false" do
72-
Truffle::Debug.java_object.should_not.respond_to?(:class)
73-
end
74-
75-
it "and a Java array returns false" do
76-
Truffle::Interop.java_array(1, 2, 3).should_not.respond_to?(:class)
77-
end
78-
end
7965
end

spec/truffle/interop/special_forms_spec.rb

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,18 @@
169169
-> { pfo.size }.should raise_error(NameError)
170170
end
171171

172-
it description['.keys', :getMembers] do
173-
pfo, _, l = proxy[Object.new]
174-
pfo.keys
172+
it description['.instance_variables', :getMembers, [], 'and returns readable non-invocable members'] do
173+
pfo, _, l = proxy[Truffle::Debug.foreign_object_with_members]
174+
pfo.instance_variables.should == [:a, :b, :c]
175+
l.log.should include(['getMembers', false])
176+
end
177+
178+
it description['.methods', :getMembers, [], 'and returns invocable members merged with available Ruby methods'] do
179+
pfo, _, l = proxy[Truffle::Debug.foreign_object_with_members]
180+
methods = pfo.methods
175181
l.log.should include(['getMembers', false])
182+
methods.last(2).should == [:method1, :method2]
183+
methods.should include(*Object.public_instance_methods)
176184
end
177185

178186
it description['.method_name', :invokeMember, ['method_name'], 'if member is invocable'] do
@@ -407,12 +415,6 @@
407415
l.log.should include(["hasArrayElements"])
408416
end
409417

410-
it description['.respond_to?(:keys)', :hasMembers] do
411-
pfo, _, l = proxy[Object.new]
412-
pfo.respond_to?(:keys)
413-
l.log.should include(["hasMembers"])
414-
end
415-
416418
it description['.respond_to?(:call)', :isExecutable] do
417419
pfo, _, l = proxy[Object.new]
418420
pfo.respond_to?(:call)

src/main/java/org/truffleruby/debug/TruffleDebugNodes.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
import com.oracle.truffle.api.frame.Frame;
2323
import com.oracle.truffle.api.frame.FrameInstance;
2424
import com.oracle.truffle.api.frame.MaterializedFrame;
25+
import com.oracle.truffle.api.interop.ArityException;
2526
import com.oracle.truffle.api.interop.StopIterationException;
2627
import com.oracle.truffle.api.interop.UnknownKeyException;
28+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
29+
import com.oracle.truffle.api.interop.UnsupportedTypeException;
2730
import com.oracle.truffle.api.nodes.RootNode;
2831
import com.oracle.truffle.api.object.DynamicObjectLibrary;
2932
import com.oracle.truffle.api.source.SourceSection;
@@ -596,25 +599,49 @@ protected boolean hasMembers() {
596599
@ExportMessage
597600
@TruffleBoundary
598601
protected Object getMembers(boolean includeInternal) {
599-
return new ForeignArrayNode.ForeignArray("a", "b", "c");
602+
return new ForeignArrayNode.ForeignArray("a", "b", "c", "method1", "method2");
600603
}
601604

602605
@TruffleBoundary
603606
@ExportMessage
604607
protected boolean isMemberReadable(String member) {
605-
return map.containsKey(member);
608+
return map.containsKey(member) || isMemberInvocable(member);
606609
}
607610

608611
@TruffleBoundary
609612
@ExportMessage
610-
protected Object readMember(String key) throws UnknownIdentifierException {
611-
final Object value = map.get(key);
613+
protected boolean isMemberInvocable(String member) {
614+
return "method1".equals(member) || "method2".equals(member);
615+
}
616+
617+
@TruffleBoundary
618+
@ExportMessage
619+
protected Object readMember(String member) throws UnknownIdentifierException {
620+
if (member.equals("method1")) {
621+
return new ForeignExecutableNode.ForeignExecutable(42);
622+
} else if (member.equals("method2")) {
623+
return new ForeignExecutableNode.ForeignExecutable(44);
624+
}
625+
626+
final Object value = map.get(member);
612627
if (value == null) {
613-
throw UnknownIdentifierException.create(key);
628+
throw UnknownIdentifierException.create(member);
614629
}
615630
return value;
616631
}
617632

633+
@TruffleBoundary
634+
@ExportMessage
635+
protected Object invokeMember(String member, Object[] arguments) throws UnsupportedMessageException,
636+
ArityException, UnknownIdentifierException, UnsupportedTypeException {
637+
if (!isMemberInvocable(member)) {
638+
throw UnknownIdentifierException.create(member);
639+
}
640+
641+
return InteropLibrary.getUncached().execute(readMember("method1"));
642+
}
643+
644+
618645
@ExportMessage
619646
protected String toDisplayString(boolean allowSideEffects) {
620647
return "[foreign object with members]";

src/main/ruby/truffleruby/core/truffle/interop_operations.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def self.foreign_inspect_nonrecursive(object)
7777

7878
show_members = true
7979
if Truffle::Interop.java_class?(object) # a java.lang.Class instance, treat it like a regular object
80-
show_members = false
8180
string << " #{Truffle::Interop.to_display_string(object)}"
8281
end
8382
if Truffle::Interop.has_array_elements?(object)
@@ -88,7 +87,7 @@ def self.foreign_inspect_nonrecursive(object)
8887
show_members = false
8988
string << " {#{object.map { |k, v| "#{basic_inspect_for k}=>#{basic_inspect_for v}" }.join(', ')}}"
9089
end
91-
if show_members && Truffle::Interop.has_members?(object)
90+
if show_members
9291
pairs = pairs_from_object(object)
9392
unless pairs.empty?
9493
string << " #{pairs.map { |k, v| "#{k}=#{basic_inspect_for v}" }.join(', ')}"
@@ -131,8 +130,7 @@ def self.java_type?(object)
131130
end
132131

133132
def self.pairs_from_object(object)
134-
readable_members = Truffle::Interop.members(object).select { |member| Truffle::Interop.member_readable?(object, member) }
135-
readable_members.map { |key| [key, object[key]] }
133+
object.instance_variables.map { |key| [key, Truffle::Interop.read_member(object, key)] }
136134
end
137135

138136
def self.ruby_class_and_language(object)

0 commit comments

Comments
 (0)