Skip to content

Commit 5ed94f9

Browse files
committed
Implement #instance_variables and #methods on foreign objects and remove the old #keys
1 parent d3f2159 commit 5ed94f9

File tree

9 files changed

+69
-26
lines changed

9 files changed

+69
-26
lines changed

CHANGELOG.md

Lines changed: 2 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

@@ -46,6 +47,7 @@ Changes:
4647

4748
* `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).
4849
* `Java.import name` imports a Java class in the enclosing module instead of always as a top-level constant.
50+
* `foreign_object.keys` no longer returns members, use `foreign_object.instance_variables` or `foreign_object.methods` instead.
4951

5052
# 21.2.0
5153

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +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-
115113
`object.respond_to?(:class)` will tell you if an object is a Java class.
116114

117115
`Polyglot.as_enumerable(object)` will create a Ruby `Enumerable` from the foreign object, using its size or length, and reading from it.

spec/truffle/interop/foreign_inspect_to_s_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
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/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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def self.foreign_inspect_nonrecursive(object)
8888
show_members = false
8989
string << " {#{object.map { |k, v| "#{basic_inspect_for k}=>#{basic_inspect_for v}" }.join(', ')}}"
9090
end
91-
if show_members && Truffle::Interop.has_members?(object)
91+
if show_members
9292
pairs = pairs_from_object(object)
9393
unless pairs.empty?
9494
string << " #{pairs.map { |k, v| "#{k}=#{basic_inspect_for v}" }.join(', ')}"
@@ -131,8 +131,7 @@ def self.java_type?(object)
131131
end
132132

133133
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]] }
134+
object.instance_variables.map { |key| [key, Truffle::Interop.read_member(object, key)] }
136135
end
137136

138137
def self.ruby_class_and_language(object)

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,6 @@ def inspect
344344
class ForeignObject < Object
345345
def respond_to?(name, include_all = false)
346346
case symbol = name.to_sym
347-
when :keys
348-
Truffle::Interop.has_members?(self)
349347
when :class
350348
Truffle::Interop.java_class?(self)
351349
else
@@ -389,8 +387,25 @@ def is_a?(klass)
389387
end
390388
alias_method :kind_of?, :is_a?
391389

392-
def keys
393-
Truffle::Interop.members(self)
390+
def instance_variables
391+
return [] unless Truffle::Interop.has_members?(self)
392+
Truffle::Interop.members_without_conversion(self).filter_map do |member|
393+
# Ruby does not have the concept of non-readable members, ignore those
394+
if Truffle::Interop.member_readable?(self, member) &&
395+
!Truffle::Interop.member_invocable?(self, member)
396+
member.to_s.to_sym
397+
end
398+
end
399+
end
400+
401+
def methods(regular = true)
402+
if regular
403+
super() | Truffle::Interop.members_without_conversion(self).filter_map do |member|
404+
member.to_s.to_sym if Truffle::Interop.member_invocable?(self, member)
405+
end
406+
else
407+
super(regular)
408+
end
394409
end
395410

396411
def [](member)

0 commit comments

Comments
 (0)