Skip to content

Commit 21d0dc2

Browse files
committed
[GR-33155] [GR-21765] Do not treat foreign_object.class specially and add Polyglot::{HashTrait,IterableTrait,IteratorTrait}
PullRequest: truffleruby/2876
2 parents eb057e7 + 818ee11 commit 21d0dc2

26 files changed

+695
-229
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ New features:
77
* Add `Polyglot::InnerContext` to eval code in any available language in an inner isolated context (#2169).
88
* Foreign objects now have a dynamically-generated class based on their interop traits like `ForeignArray` and are better integrated with Ruby objects (#2149).
99
* Foreign arrays now have all methods of Ruby `Enumerable` and many methods of `Array` (#2149).
10+
* Foreign hashes now have all methods of Ruby `Enumerable` and many methods of `Hash` (#2149).
11+
* Foreign iterables (`InteropLibrary#hasIterator`) now have all methods of Ruby `Enumerable` (#2149).
1012

1113
Bug fixes:
1214

@@ -42,6 +44,8 @@ Performance:
4244

4345
Changes:
4446

47+
* `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).
48+
* `Java.import name` imports a Java class in the enclosing module instead of always as a top-level constant.
4549

4650
# 21.2.0
4751

doc/contributor/interop_implicit_api.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ In the documentation below:
88

99
Format: `Ruby code` sends `InteropLibrary message`
1010

11+
- `foreign_object[key]` sends `readHashValue(foreign_object, key)` if `hasHashEntries(foreign_object)`
1112
- `foreign_object[name]` sends `readMember(foreign_object, name)`
1213
- `foreign_object[index]` sends `readArrayElement(foreign_object, index)`
14+
- `foreign_object[key] = value` sends `writeHashEntry(foreign_object, key)` if `hasHashEntries(foreign_object)`
1315
- `foreign_object[name] = value` sends `writeMember(foreign_object, name, value)`
1416
- `foreign_object[index] = value` sends `writeArrayElement(foreign_object, index, value)`
1517
- `foreign_object.name = value` sends `writeMember(foreign_object, name, value)`
1618
- `foreign_object.name = *arguments` sends `writeMember(foreign_object, name, arguments)`
19+
- `foreign_object.delete(key)` sends `removeHashEntry(foreign_object, key)` if `hasHashEntries(foreign_object)`
1720
- `foreign_object.delete(name)` sends `removeMember(foreign_object, name)`
1821
- `foreign_object.delete(index)` sends `removeArrayElement(foreign_object, index)`
1922
- `foreign_object.call(*arguments)` sends `execute(foreign_object, *arguments)`
@@ -26,8 +29,6 @@ Format: `Ruby code` sends `InteropLibrary message`
2629
- `foreign_object.method_name(*arguments)` sends `invokeMember(foreign_object, method_name, *arguments)`
2730
- `foreign_object.method_name(*arguments, &block)` sends `invokeMember(foreign_object, method_name, *arguments, block)`
2831
- `foreign_object.new(*arguments)` sends `instantiate(foreign_object, *arguments)`
29-
- `foreign_object.class` sends `readMember(foreign_object, "class")` when `foreign_object` is a `java.lang.Class`
30-
- `foreign_object.class` sends `getMetaObject(foreign_object)`
3132
- `foreign_object.inspect` returns a Ruby-style `#inspect` string showing members, array elements, etc
3233
- `foreign_object.to_s` sends `asString(foreign_object)` when `isString(foreign_object)` is true
3334
- `foreign_object.to_s` sends `toDisplayString(foreign_object)` otherwise
@@ -45,10 +46,10 @@ Format: `Ruby code` sends `InteropLibrary message`
4546
- `foreign_object.__id__` uses `System.identityHashCode()` otherwise (which might not be unique)
4647
- `foreign_object.hash` sends `identityHashCode(foreign_object)` when `hasIdentity()` is true (which might not be unique)
4748
- `foreign_object.hash` uses `System.identityHashCode()` otherwise (which might not be unique)
49+
- `foreign_object.map` and other `Enumerable` methods work for foreign arrays
50+
- `foreign_object.map` and other `Enumerable` methods work for foreign iterables (`hasIterator()`)
4851

4952
Use `.respond_to?` for calling `InteropLibrary` predicates:
50-
- `foreign_object.respond_to?(:inspect)` is always true
51-
- `foreign_object.respond_to?(:to_s)` is always true
5253
- `foreign_object.respond_to?(:to_str)` sends `isString(foreign_object)`
5354
- `foreign_object.respond_to?(:to_a)` sends `hasArrayElements(foreign_object)`
5455
- `foreign_object.respond_to?(:to_ary)` sends `hasArrayElements(foreign_object)`
@@ -58,4 +59,3 @@ Use `.respond_to?` for calling `InteropLibrary` predicates:
5859
- `foreign_object.respond_to?(:keys)` sends `hasMembers(foreign_object)`
5960
- `foreign_object.respond_to?(:call)` sends `isExecutable(foreign_object)`
6061
- `foreign_object.respond_to?(:new)` sends `isInstantiable(foreign_object)`
61-
- `foreign_object.respond_to?(:is_a?)` is always true

doc/user/jruby-migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ frame.setVisible(true)
354354
```
355355

356356
Instead of using Ruby metaprogramming to simulate a Java package name, we explicitly import classes.
357-
`Java.import` is similar to JRuby's `java_import`, and does `::ClassName = Java.type('package.ClassName')`.
357+
`Java.import` is similar to JRuby's `java_import`, and does `ClassName = Java.type('package.ClassName')`.
358358

359359
Constants are read by reading properties of the class rather than using Ruby notation.
360360

@@ -367,7 +367,7 @@ This is only available in GraalVM - not in the standalone distribution installed
367367

368368
In JRuby, Java classes can either be referenced in the `Java` module, such as `Java::ComFoo::Bar`, or if they have a common TLD they can be referenced as `com.foo.Bar`. `java_import com.foo.Bar` will define `Bar` as a top-level constant.
369369

370-
In TruffleRuby, Java classes are referred to using either `Java.type('com.foo.Bar')`, which you would then normally assign to a constant, or you can use `Java.import 'com.foo.Bar'` to have `Bar` defined as a top-level constant.
370+
In TruffleRuby, Java classes are referred to using either `Java.type('com.foo.Bar')`, which you would then normally assign to a constant, or you can use `Java.import 'com.foo.Bar'` to have `Bar` defined in the enclosing module.
371371

372372
### Wildcard Package Imports
373373

doc/user/polyglot.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ If you want to pass a Ruby object to another language for fields to be read and
102102

103103
`object.new(*args)` will create a new object from the foreign object (as if it is some kind of class).
104104

105-
`object.class` on a Java `Class` object will give you an object on which you can call instance methods, rather than static methods.
106-
107105
`object.respond_to?(:size)` will tell you if the foreign object has a size or length.
108106

109107
`object.nil?` will tell you if the foreign object represents the language's equivalent of `null` or `nil`.
@@ -128,11 +126,13 @@ It is easier to use Java interoperability in JVM mode (`--jvm`). Java interopera
128126
See [here](https://www.graalvm.org/reference-manual/embed-languages/#build-native-images-from-polyglot-applications)
129127
for more details.
130128

131-
`Java.type('name')` returns a Java class object, given a name such as `java.lang.Integer` or `int[]`. With the class object, `.new` will create an instance, `.foo` will call the static method `foo`, `[:FOO]` will read the field
129+
`Java.type('name')` returns a Java type, given a name such as `java.lang.Integer` or `int[]`.
130+
With the type object, `.new` will create an instance, `.foo` will call the static method `foo`, `[:FOO]` will read the static field
132131
`FOO`, and so on.
133-
To access instance methods use `.class`, such as `MyClass.class.getName`.
132+
To access methods of the `java.lang.Class` instance, use `[:class]`, such as `MyClass[:class].getName`.
133+
You can also go from the `java.lang.Class` instance to the Java type by using `[:static]`.
134134

135-
To import a Java class as a top-level constant, use `Java.import 'name'`.
135+
To import a Java class in the enclosing module, use `MyClass = Java.type 'java.lang.MyClass'` or `Java.import 'java.lang.MyClass'`.
136136

137137
## Embedding in Java
138138

spec/tags/truffle/interop/java_import_tags.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

spec/truffle/interop/foreign_inspect_to_s_spec.rb

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,23 @@
4040
foreign = Truffle::Interop.to_java_map(hash)
4141

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

48-
describe "Java class" do
48+
describe "Java type" do
4949
it "gives a similar representation to Ruby" do
5050
foreign = Truffle::Interop.java_type("java.math.BigInteger")
51-
foreign.inspect.should == "#<Polyglot::ForeignInstantiable[Java] class java.math.BigInteger>"
51+
foreign.inspect.should == "#<Polyglot::ForeignInstantiable[Java] type java.math.BigInteger>"
52+
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] type java.math.BigInteger>"
53+
end
54+
end
55+
56+
describe "Java j.l.Class instance" do
57+
it "gives a similar representation to Ruby" do
58+
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/
5260
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] java.math.BigInteger>"
5361
end
5462
end
@@ -64,8 +72,7 @@
6472

6573
describe "recursive array" do
6674
it "gives a similar representation to Ruby" do
67-
x = [1, 2, 3]
68-
foreign = Truffle::Debug.foreign_array_from_java(Truffle::Interop.to_java_array(x))
75+
foreign = Truffle::Debug.foreign_array
6976
foreign[0] = foreign
7077
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray:0x\h+ \[\[...\], 2, 3\]>\z/
7178
foreign.to_s.should == "#<Polyglot::ForeignArray [foreign array]>"
@@ -96,37 +103,42 @@
96103
end
97104
end
98105

99-
guard -> { !TruffleRuby.native? } do
100-
describe "array" do
101-
it "gives a similar representation to Ruby" do
102-
foreign = Truffle::Debug.foreign_array_from_java(Truffle::Interop.to_java_array([1, 2, 3]))
103-
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray:0x\h+ \[1, 2, 3\]>\z/
104-
foreign.to_s.should == "#<Polyglot::ForeignArray [foreign array]>"
105-
end
106+
describe "array" do
107+
it "gives a similar representation to Ruby" do
108+
foreign = Truffle::Debug.foreign_array
109+
foreign.inspect.should =~ /\A#<Polyglot::ForeignArray:0x\h+ \[1, 2, 3\]>\z/
110+
foreign.to_s.should == "#<Polyglot::ForeignArray [foreign array]>"
111+
end
106112

107-
it "gives a similar representation to Ruby, even if it is also a pointer" do
108-
foreign = Truffle::Debug.foreign_pointer_array_from_java(Truffle::Interop.to_java_array([1, 2, 3]))
109-
foreign.inspect.should =~ /\A#<Polyglot::ForeignArrayPointer 0x0 \[1, 2, 3\]>\z/
110-
foreign.to_s.should == "#<Polyglot::ForeignArrayPointer [foreign pointer array]>"
111-
end
113+
it "gives a similar representation to Ruby, even if it is also a pointer" do
114+
foreign = Truffle::Debug.foreign_pointer_array
115+
foreign.inspect.should =~ /\A#<Polyglot::ForeignArrayPointer 0x0 \[1, 2, 3\]>\z/
116+
foreign.to_s.should == "#<Polyglot::ForeignArrayPointer [foreign pointer array]>"
112117
end
113118
end
114119

115-
guard -> { !TruffleRuby.native? } do
116-
describe "object without members" do
117-
it "gives a similar representation to Ruby" do
118-
foreign = Truffle::Debug.foreign_object
119-
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+>\z/
120-
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object]>"
121-
end
120+
describe "hash" do
121+
it "gives a similar representation to Ruby" do
122+
foreign = Truffle::Debug.foreign_hash
123+
{ a: 1, b: 2 }.inspect.should == "{:a=>1, :b=>2}"
124+
foreign.inspect.should =~ /\A#<Polyglot::ForeignHash:0x\h+ {:a=>1, :b=>2}>\z/
125+
foreign.to_s.should == "#<Polyglot::ForeignHash [foreign hash]>"
122126
end
127+
end
123128

124-
describe "object with members" do
125-
it "gives a similar representation to Ruby" do
126-
foreign = Truffle::Debug.foreign_object_from_map(Truffle::Interop.to_java_map({a: 1, b: 2, c: 3}))
127-
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+ a=1, b=2, c=3>\z/
128-
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object with members]>"
129-
end
129+
describe "object without members" do
130+
it "gives a similar representation to Ruby" do
131+
foreign = Truffle::Debug.foreign_object
132+
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+>\z/
133+
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object]>"
134+
end
135+
end
136+
137+
describe "object with members" do
138+
it "gives a similar representation to Ruby" do
139+
foreign = Truffle::Debug.foreign_object_with_members
140+
foreign.inspect.should =~ /\A#<Polyglot::ForeignObject:0x\h+ a=1, b=2, c=3>\z/
141+
foreign.to_s.should == "#<Polyglot::ForeignObject [foreign object with members]>"
130142
end
131143
end
132144
end

spec/truffle/interop/java_import_spec.rb

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,37 @@
1010

1111
guard -> { !TruffleRuby.native? } do
1212
describe "Java.import" do
13-
14-
it "imports a class to the top-level" do
15-
ruby_exe(%{
13+
it "imports a class under the enclosing module" do
14+
module TruffleJavaImportSpecs1
1615
Java.import 'java.math.BigInteger'
17-
puts BigInteger.new('1234').toString
18-
}).should == "1234\n"
16+
BigInteger.new('1234').toString.should == "1234"
17+
end
18+
19+
TruffleJavaImportSpecs1.should.const_defined?('BigInteger', false)
20+
Object.should_not.const_defined?('BigInteger')
1921
end
2022

2123
it "returns the imported class" do
22-
ruby_exe(%{
23-
puts Java.import('java.math.BigInteger').class.getName
24-
}).should == "java.math.BigInteger\n"
24+
module TruffleJavaImportSpecs2
25+
Java.import('java.math.BigInteger')[:class].getName.should == "java.math.BigInteger"
26+
end
2527
end
2628

2729
it "can import classes twice" do
28-
ruby_exe(%{
30+
module TruffleJavaImportSpecs3
2931
Java.import 'java.math.BigInteger'
3032
Java.import 'java.math.BigInteger'
31-
puts BigInteger.new('1234').toString
32-
}).should == "1234\n"
33+
BigInteger.new('1234').toString.should == "1234"
34+
end
3335
end
3436

3537
it "raises an error if the constant is already defined" do
36-
ruby_exe(%{
38+
module TruffleJavaImportSpecs4
3739
BigInteger = '14'
38-
begin
40+
-> {
3941
Java.import 'java.math.BigInteger'
40-
rescue NameError => e
41-
puts e
42-
end
43-
}).should == "constant BigInteger already set\n"
42+
}.should raise_error(NameError, "constant BigInteger already set")
43+
end
4444
end
45-
4645
end
4746
end

spec/truffle/interop/java_type_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,27 @@
1313
describe "Truffle::Interop.java_type" do
1414

1515
it "returns a Java class for a known primitive name" do
16-
Truffle::Interop.java_type("int").class.getName.should == "int"
16+
Truffle::Interop.java_type("int")[:class].getName.should == "int"
1717
end
1818

1919
it "returns a Java class for known primitive name as an array" do
20-
Truffle::Interop.java_type("int[]").class.getName.should == "[I"
20+
Truffle::Interop.java_type("int[]")[:class].getName.should == "[I"
2121
end
2222

2323
it "returns a Java class for a known class name " do
24-
Truffle::Interop.java_type("java.math.BigInteger").class.getName.should == "java.math.BigInteger"
24+
Truffle::Interop.java_type("java.math.BigInteger")[:class].getName.should == "java.math.BigInteger"
2525
end
2626

2727
it "returns a Java class for known class name as an array" do
28-
Truffle::Interop.java_type("java.math.BigInteger[]").class.getName.should == "[Ljava.math.BigInteger;"
28+
Truffle::Interop.java_type("java.math.BigInteger[]")[:class].getName.should == "[Ljava.math.BigInteger;"
2929
end
3030

3131
it "throws RuntimeError for unknown class names" do
3232
-> { Truffle::Interop.java_type("does.not.Exist") }.should raise_error(RuntimeError)
3333
end
3434

3535
it "works with symbols" do
36-
Truffle::Interop.java_type(:int).class.getName.should == "int"
36+
Truffle::Interop.java_type(:int)[:class].getName.should == "int"
3737
end
3838

3939
end

spec/truffle/interop/matrix_spec.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,7 @@ def array_element_predicate(message, predicate, insert_on_true_case)
541541
end,
542542
Test.new("fails with StopIterationException if the hash is empty", :hash, :polyglot_hash) do |subject|
543543
iterator = Truffle::Interop.hash_entries_iterator(subject)
544-
# TODO introduce StopIterationException in Ruby
545-
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(Exception)
544+
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(StopIteration)
546545
end,
547546
unsupported_test { |subject| Truffle::Interop.hash_entries_iterator(subject) }],
548547
Message[:getHashKeysIterator,
@@ -558,8 +557,7 @@ def array_element_predicate(message, predicate, insert_on_true_case)
558557
end,
559558
Test.new("fails with StopIterationException if the hash is empty", :hash, :polyglot_hash) do |subject|
560559
iterator = Truffle::Interop.hash_keys_iterator(subject)
561-
# TODO introduce StopIterationException in Ruby
562-
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(Exception)
560+
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(StopIteration)
563561
end,
564562
unsupported_test { |subject| Truffle::Interop.hash_entries_iterator(subject) }],
565563
Message[:getHashValuesIterator,
@@ -575,8 +573,7 @@ def array_element_predicate(message, predicate, insert_on_true_case)
575573
end,
576574
Test.new("fails with StopIterationException if the hash is empty", :hash, :polyglot_hash) do |subject|
577575
iterator = Truffle::Interop.hash_values_iterator(subject)
578-
# TODO introduce StopIterationException in Ruby
579-
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(Exception)
576+
-> { Truffle::Interop.iterator_next_element(iterator) }.should raise_error(StopIteration)
580577
end,
581578
unsupported_test { |subject| Truffle::Interop.hash_entries_iterator(subject) }],
582579

0 commit comments

Comments
 (0)