Skip to content

Commit

Permalink
Integrate foreign objects better with Ruby objects
Browse files Browse the repository at this point in the history
* By giving foreign objects a proper Ruby class.
* Move the logic for all special methods on foreign objects (Implicit polyglot API)
  to regular Ruby methods of Polyglot::ForeignObject and Polyglot trait modules.
* Automatically generate Polyglot::Foreign* classes composing the InteropLibrary traits.
* Simplify Polyglot::ForeignObject#respond_to? now that most are defined as Ruby methods.
* Fixes oracle#2149
* From oracle#2153

Co-authored-by: Benoit Daloze <benoit.daloze@oracle.com>
  • Loading branch information
rbotafogo and eregon committed Aug 16, 2021
1 parent 7ccb875 commit 3d7e386
Show file tree
Hide file tree
Showing 21 changed files with 846 additions and 976 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ New features:
* [TRegex](https://github.com/oracle/graal/tree/master/regex) is now used by default, which provides large speedups for matching regular expressions.
* Add `Polyglot.languages` to expose the list of available languages.
* Add `Polyglot::InnerContext` to eval code in any available language in an inner isolated context (#2169).
* Foreign objects now have a dynamically-generated class based on their interop traits like `ForeignArray` and are better integrated with Ruby objects (#2149).
* Foreign arrays now have all methods of Ruby `Enumerable` and many methods of `Array` (#2149).

Bug fixes:

Expand Down
10 changes: 5 additions & 5 deletions doc/contributor/interop_implicit_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ Format: `Ruby code` sends `InteropLibrary message`
- `foreign_object.to_s` sends `asString(foreign_object)` when `isString(foreign_object)` is true
- `foreign_object.to_s` sends `toDisplayString(foreign_object)` otherwise
- `foreign_object.to_str` sends `asString(foreign_object)` when `isString(foreign_object)` is true
- `foreign_object.to_str` raises `NoMethodError` otherwise
- `foreign_object.to_str` raises `NameError` otherwise
- `foreign_object.to_a` converts to a Ruby `Array` with `Truffle::Interop.to_array(foreign_object)`
- `foreign_object.to_ary` converts to a Ruby `Array` with `Truffle::Interop.to_array(foreign_object)`
- `foreign_object.to_f` tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` or raises `TypeError`
- `foreign_object.to_i` tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` or raises `TypeError`
- `foreign_object.to_f` tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` or raises `NameError`
- `foreign_object.to_i` tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` or raises `NameError`
- `foreign_object.equal?(other)` sends `isIdentical(foreign_object, other)`
- `foreign_object.eql?(other)` sends `isIdentical(foreign_object, other)`
- `foreign_object.object_id` sends `identityHashCode(foreign_object)` when `hasIdentity()` is true (which might not be unique)
Expand All @@ -52,8 +52,8 @@ Use `.respond_to?` for calling `InteropLibrary` predicates:
- `foreign_object.respond_to?(:to_str)` sends `isString(foreign_object)`
- `foreign_object.respond_to?(:to_a)` sends `hasArrayElements(foreign_object)`
- `foreign_object.respond_to?(:to_ary)` sends `hasArrayElements(foreign_object)`
- `foreign_object.respond_to?(:to_f)` sends `fitsInDouble()` and `fitsInLong()`
- `foreign_object.respond_to?(:to_i)` sends `fitsInInt()` and `fitsInLong()`
- `foreign_object.respond_to?(:to_f)` sends `fitsInDouble()`
- `foreign_object.respond_to?(:to_i)` sends `fitsInLong()`
- `foreign_object.respond_to?(:size)` sends `hasArrayElements(foreign_object)`
- `foreign_object.respond_to?(:keys)` sends `hasMembers(foreign_object)`
- `foreign_object.respond_to?(:call)` sends `isExecutable(foreign_object)`
Expand Down
56 changes: 28 additions & 28 deletions spec/truffle/interop/foreign_inspect_to_s_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@
describe "Java null" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.java_null
foreign.inspect.should == "#<Java null>"
foreign.to_s.should == "#<Java null>"
foreign.inspect.should == "#<Polyglot::ForeignNull[Java] null>"
foreign.to_s.should == "#<Polyglot::ForeignNull[Java] null>"
end
end

describe "Java list" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.to_java_list([1, 2, 3])
foreign.inspect.should =~ /\A#<Java java\.util\.Arrays\$ArrayList:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Java [1, 2, 3]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray\[Java\] java\.util\.Arrays\$ArrayList:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Polyglot::ForeignArray[Java] [1, 2, 3]>"
end
end

describe "Java array" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.to_java_array([1, 2, 3])
foreign.inspect.should =~ /\A#<Java int\[\]:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Java [1, 2, 3]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray\[Java\] int\[\]:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Polyglot::ForeignArray[Java] [1, 2, 3]>"
end
end

Expand All @@ -40,24 +40,24 @@
foreign = Truffle::Interop.to_java_map(hash)

hash.to_s.should == '{:a=>1, :b=>2, :c=>3}' # for comparison
foreign.inspect.should =~ /\A#<Java java\.util\.HashMap:0x\h+ {"a"=>1, "b"=>2, "c"=>3}>\z/
foreign.to_s.should == "#<Java {a=1, b=2, c=3}>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject\[Java\] java\.util\.HashMap:0x\h+ {"a"=>1, "b"=>2, "c"=>3}>\z/
foreign.to_s.should == "#<Polyglot::ForeignObject[Java] {a=1, b=2, c=3}>"
end
end

describe "Java class" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.java_type("java.math.BigInteger")
foreign.inspect.should == "#<Java class java.math.BigInteger>"
foreign.to_s.should == "#<Java java.math.BigInteger>"
foreign.inspect.should == "#<Polyglot::ForeignInstantiable[Java] class java.math.BigInteger>"
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] java.math.BigInteger>"
end
end

describe "Java object" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.java_type("java.math.BigInteger").new('14')
foreign.inspect.should =~ /\A#<Java java\.math\.BigInteger:0x\h+ .+>\z/
foreign.to_s.should == "#<Java 14>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject\[Java\] java\.math\.BigInteger:0x\h+ .+>\z/
foreign.to_s.should == "#<Polyglot::ForeignObject[Java] 14>"
end
end
end
Expand All @@ -67,47 +67,47 @@
x = [1, 2, 3]
foreign = Truffle::Debug.foreign_array_from_java(Truffle::Interop.to_java_array(x))
foreign[0] = foreign
foreign.inspect.should =~ /\A#<Foreign:0x\h+ \[\[...\], 2, 3\]>\z/
foreign.to_s.should == "#<Foreign [foreign array]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray:0x\h+ \[\[...\], 2, 3\]>\z/
foreign.to_s.should == "#<Polyglot::ForeignArray [foreign array]>"
end
end

describe "null" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_null
foreign.inspect.should == "#<Foreign null>"
foreign.to_s.should == "#<Foreign [foreign null]>"
foreign.inspect.should == "#<Polyglot::ForeignNull null>"
foreign.to_s.should == "#<Polyglot::ForeignNull [foreign null]>"
end
end

describe "executable" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_executable(14)
foreign.inspect.should =~ /\A#<Foreign:0x\h+ proc>\z/
foreign.to_s.should == "#<Foreign [foreign executable]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignExecutable:0x\h+ proc>\z/
foreign.to_s.should == "#<Polyglot::ForeignExecutable [foreign executable]>"
end
end

describe "pointer" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_pointer(0x1234)
foreign.inspect.should == "#<Foreign pointer 0x1234>"
foreign.to_s.should == "#<Foreign [foreign pointer]>"
foreign.inspect.should == "#<Polyglot::ForeignPointer 0x1234>"
foreign.to_s.should == "#<Polyglot::ForeignPointer [foreign pointer]>"
end
end

guard -> { !TruffleRuby.native? } do
describe "array" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_array_from_java(Truffle::Interop.to_java_array([1, 2, 3]))
foreign.inspect.should =~ /\A#<Foreign:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Foreign [foreign array]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray:0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Polyglot::ForeignArray [foreign array]>"
end

it "gives a similar representation to Ruby, even if it is also a pointer" do
foreign = Truffle::Debug.foreign_pointer_array_from_java(Truffle::Interop.to_java_array([1, 2, 3]))
foreign.inspect.should =~ /\A#<Foreign pointer 0x\h+ \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Foreign [foreign pointer array]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignArrayPointer 0x0 \[1, 2, 3\]>\z/
foreign.to_s.should == "#<Polyglot::ForeignArrayPointer [foreign pointer array]>"
end
end
end
Expand All @@ -116,16 +116,16 @@
describe "object without members" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_object
foreign.inspect.should =~ /\A#<Foreign:0x\h+>\z/
foreign.to_s.should == "#<Foreign [foreign object]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+>\z/
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object]>"
end
end

describe "object with members" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_object_from_map(Truffle::Interop.to_java_map({a: 1, b: 2, c: 3}))
foreign.inspect.should =~ /\A#<Foreign:0x\h+ a=1, b=2, c=3>\z/
foreign.to_s.should == "#<Foreign [foreign object with members]>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+ a=1, b=2, c=3>\z/
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object with members]>"
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/truffle/interop/polyglot/inner_context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
Polyglot::InnerContext.new do |context|
obj = context.eval('ruby', "Object.new")
Truffle::Interop.should.foreign?(obj)
obj.to_s.should.start_with?("#<Ruby #<Object:")
obj.inspect.should.start_with?("#<Ruby Object:")
obj.to_s.should.start_with?("#<Polyglot::ForeignObject[Ruby] #<Object:")
obj.inspect.should.start_with?("#<Polyglot::ForeignObject[Ruby] Object:")
end
end

Expand Down
77 changes: 77 additions & 0 deletions spec/truffle/interop/polyglot/polyglot_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,83 @@
require_relative '../../../ruby/spec_helper'

describe "Polyglot" do
describe "Access to foreign array as ForeignArray" do
before do
@foreign = Truffle::Interop.to_java_array([1, 2, 3])
@foreign_db = Truffle::Interop.to_java_array([1.4, 2.8, 3.6])
end

it "should create a ForeignArray class" do
@foreign.inspect.should =~ /\A#<Polyglot::ForeignArray\[Java\] int\[\]:0x\h+ \[1, 2, 3\]>/
@foreign.length.should == 3
end

it "should index array with #[]" do
@foreign[0].should == 1
@foreign[1].should == 2
@foreign[2].should == 3
@foreign[5].should == nil
end

it "should index array with #at" do
@foreign.at(0).should == 1
@foreign.at(1).should == 2
@foreign.at(3).should == nil
end

it "should access the first and last elements with #first and #last" do
@foreign.first.should == 1
@foreign.last.should == 3
end

it "can call each on the foreign array" do
@foreign.each_with_index do |val, index|
@foreign[index].should == val
end
end

it "can receive an enumerator from a @foreign array with #each with no block given" do
enum = @foreign.each
enum.class.should == Enumerator
enum.each_with_index do |val, index|
@foreign[index].should == val
end
end

it "should allow the use of #take" do
slice = @foreign.take(2)
slice[0].should == 1
slice[1].should == 2
slice.at(2).should == nil
end

it "should allow assignment of array element with #[]=" do
@foreign[2] = 10
@foreign[2].should == 10
end

it "should allow the use of #next" do
skip
# NOT WORKING: enum.next says that the iteration reached an end
enum = @foreign_db.each
enum.class.should == Enumerator
puts enum.next
end

it "should count objects in the array" do
foreign = Truffle::Interop.to_java_array([1, 2, 4, 2])
foreign.count.should == 4
foreign.count(2).should == 2
foreign.count { |x| x % 2 == 0 }.should == 3
end

it "should indicate that Foreign" do
object = Object.new
Truffle::Interop.has_members?(object).should == true
end

end

describe ".eval(id, code)" do
it "evals code in Ruby" do
Polyglot.eval("ruby", "14 + 2").should == 16
Expand Down
Loading

0 comments on commit 3d7e386

Please sign in to comment.