diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a2dafa65c0..5e0d3fa6613d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # 1.0 RC 3 +New features: + +* `is_a?` can be called on foreign objects. + Bug fixes: * It is no longer needed to have `ruby` in `$PATH` to run the post-install hook. diff --git a/doc/contributor/interop.md b/doc/contributor/interop.md index 8e4dfc60ad68..257cff2310ce 100644 --- a/doc/contributor/interop.md +++ b/doc/contributor/interop.md @@ -417,6 +417,16 @@ looks at the underlying Java object. `object.to_str` will try to `UNBOX` the object and return it if it's a `String`, or will raise `NoMethodError` if it isn't. +`java_object.is_a?(java_class)` does `java_object instanceof java_class`, using +the host object instance, rather than any runtime interop wrapper. + +`object.is_a?(java_class)` does `object instanceof java_class`, using the +runtime object instance. + +`foreign_object.is_a?(ruby_class)` returns `false`. + +`foreign_object.is_a?(foreign_class)` raises a `TypeError`. + `object.respond_to?(:to_a)`, `respond_to?(:to_ary)` and `respond_to?(:size)` sends `HAS_SIZE`. @@ -426,14 +436,12 @@ sends `HAS_SIZE`. `object.respond_to?(:class)` calls `Truffle::Interop.java_class?(object)`. -`object.respond_to?(name)` for other names returns `false`. - -`object.respond_to?(:inspect)` is `true`. - -`object.respond_to?(:to_s)` is `true`. +`object.respond_to?(:inspect)`, `:to_s`, `:is_a?`, is `true`. `object.respond_to?(:to_str)` is `true` if the object `UNBOXes` to a `String`. +`object.respond_to?(name)` for other names returns `false`. + `object.__send__(name, *args)` works in the same way as literal method call on the foreign object, including allowing the special-forms listed above (see [notes on method resolution](#notes-on-method-resolution)). @@ -509,6 +517,8 @@ type is supported for interop. `Truffle::Interop.java_class?(object)` +`Truffle::Interop.java_instanceof?(object, class)` + `Truffle::Interop.java_string?(object)` `Truffle::Interop.to_java_string(ruby_string)` diff --git a/spec/truffle/interop/java_instanceof_spec.rb b/spec/truffle/interop/java_instanceof_spec.rb new file mode 100644 index 000000000000..f72bc641eda7 --- /dev/null +++ b/spec/truffle/interop/java_instanceof_spec.rb @@ -0,0 +1,60 @@ +# Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. This +# code is released under a tri EPL/GPL/LGPL license. You can use it, +# redistribute it and/or modify it under the terms of the: +# +# Eclipse Public License version 1.0, or +# GNU General Public License version 2, or +# GNU Lesser General Public License version 2.1. + +require_relative '../../ruby/spec_helper' + +guard -> { !TruffleRuby.native? } do + describe "Truffle::Interop.java_instanceof?" do + + it "returns true for a directly matching Java object and class" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + Truffle::Interop.java_instanceof?(big_integer, big_integer_class).should be_true + end + + it "returns true for a matching Java object and superclass" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + number_class = Truffle::Interop.java_type("java.lang.Number") + Truffle::Interop.java_instanceof?(big_integer, number_class).should be_true + end + + it "returns true for a matching Java object and interface" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + serializable_interface = Truffle::Interop.java_type("java.io.Serializable") + Truffle::Interop.java_instanceof?(big_integer, serializable_interface).should be_true + end + + it "returns false for an unrelated Java object and Java class" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + big_decimal_class = Truffle::Interop.java_type("java.math.BigDecimal") + Truffle::Interop.java_instanceof?(big_integer, big_decimal_class).should be_false + end + + it "returns false for an unrelated Ruby object and Java class" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + Truffle::Interop.java_instanceof?(Object.new, big_integer_class).should be_false + end + + it "handles boxing of primitives like Java does" do + integer_class = Truffle::Interop.java_type("java.lang.Integer") + Truffle::Interop.java_instanceof?(14, integer_class).should be_true + double_class = Truffle::Interop.java_type("java.lang.Double") + Truffle::Interop.java_instanceof?(14.2, double_class).should be_true + end + + it "raises a type error if passed something that is not a Java class" do + lambda { Truffle::Interop.java_instanceof?(14, nil) }.should raise_error(TypeError) + lambda { Truffle::Interop.java_instanceof?(14, String) }.should raise_error(TypeError) + lambda { Truffle::Interop.java_instanceof?(14, Truffle::Debug.java_object) }.should raise_error(TypeError) + end + + end +end diff --git a/spec/truffle/interop/respond_to_spec.rb b/spec/truffle/interop/respond_to_spec.rb index c237fb724d26..09e23f495f38 100644 --- a/spec/truffle/interop/respond_to_spec.rb +++ b/spec/truffle/interop/respond_to_spec.rb @@ -102,6 +102,30 @@ end + describe "for :is_a?" do + + it "and a Java class returns true" do + Truffle::Interop.respond_to?(Truffle::Debug.java_class, :is_a?).should be_true + end + + it "and a Java object returns true" do + Truffle::Interop.respond_to?(Truffle::Debug.java_object, :is_a?).should be_true + end + + it "and a Ruby object returns true" do + Truffle::Interop.respond_to?(Object.new, :is_a?).should be_true + end + + describe "via a direct call" do + + it "and a Java array returns true" do + Truffle::Interop.java_array(1, 2, 3).respond_to?(:is_a?).should be_true + end + + end + + end + describe "for :class" do it "and a Java class returns true" do diff --git a/spec/truffle/interop/special_forms_spec.rb b/spec/truffle/interop/special_forms_spec.rb index 62fd14d54d50..f1b15d66774d 100644 --- a/spec/truffle/interop/special_forms_spec.rb +++ b/spec/truffle/interop/special_forms_spec.rb @@ -77,6 +77,67 @@ Truffle::Debug.foreign_object.inspect.should =~ /#/ end + it "#inspect returns a useful string" do + Truffle::Debug.foreign_object.inspect.should =~ /#/ + end + + describe "#is_a?" do + + guard -> { !TruffleRuby.native? } do + + it "returns true for a directly matching Java object and class" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + big_integer.is_a?(big_integer_class).should be_true + end + + it "returns true for a directly matching Java object and superclass" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + number_class = Truffle::Interop.java_type("java.lang.Number") + big_integer.is_a?(number_class).should be_true + end + + it "returns true for a directly matching Java object and interface" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + serializable_interface = Truffle::Interop.java_type("java.io.Serializable") + big_integer.is_a?(serializable_interface).should be_true + end + + it "returns false for an unrelated Java object and Java class" do + big_integer_class = Truffle::Interop.java_type("java.math.BigInteger") + big_integer = big_integer_class.new("14") + big_decimal_class = Truffle::Interop.java_type("java.math.BigDecimal") + big_integer.is_a?(big_decimal_class).should be_false + end + + it "returns false for a Java object and a Ruby class" do + java_hash = Truffle::Interop.java_type("java.util.HashMap").new + java_hash.is_a?(Hash).should be_false + end + + it "raises a type error for a non-Java foreign object and a non-Java foreign class" do + lambda { + Truffle::Debug.foreign_object.is_a?(Truffle::Debug.foreign_object) + }.should raise_error(TypeError, /cannot check if a foreign object is an instance of a foreign class/) + end + + it "works with boxed primitives" do + boxed_integer = Truffle::Debug.foreign_boxed_number(14) + boxed_integer.is_a?(Integer).should be_true + boxed_double = Truffle::Debug.foreign_boxed_number(14.2) + boxed_double.is_a?(Float).should be_true + end + + end + + it "returns false for a non-Java foreign object and a Ruby class" do + Truffle::Debug.foreign_object.is_a?(Hash).should be_false + end + + end + it "#respond_to?(:to_a) sends HAS_SIZE" do @object.respond_to?(:to_a) @object.log.should include("HAS_SIZE") diff --git a/src/main/java/org/truffleruby/debug/ForeignObjectMessageResolution.java b/src/main/java/org/truffleruby/debug/ForeignObjectMessageResolution.java new file mode 100644 index 000000000000..c8e644ac017e --- /dev/null +++ b/src/main/java/org/truffleruby/debug/ForeignObjectMessageResolution.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. This + * code is released under a tri EPL/GPL/LGPL license. You can use it, + * redistribute it and/or modify it under the terms of the: + * + * Eclipse Public License version 1.0, or + * GNU General Public License version 2, or + * GNU Lesser General Public License version 2.1. + */ +package org.truffleruby.debug; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.interop.CanResolve; +import com.oracle.truffle.api.interop.MessageResolution; +import com.oracle.truffle.api.interop.Resolve; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.nodes.Node; +import org.truffleruby.debug.TruffleDebugNodes.ForeignObjectNode.ForeignObject; + +@MessageResolution(receiverType = ForeignObject.class) +public class ForeignObjectMessageResolution { + + @CanResolve + public abstract static class Check extends Node { + + protected static boolean test(TruffleObject receiver) { + return receiver instanceof ForeignObject; + } + + } + + @Resolve(message = "IS_BOXED") + public static abstract class ForeignIsBoxedNode extends Node { + + protected Object access(VirtualFrame frame, ForeignObject number) { + return false; + } + + } + +} diff --git a/src/main/java/org/truffleruby/debug/TruffleDebugNodes.java b/src/main/java/org/truffleruby/debug/TruffleDebugNodes.java index 825b5b769355..d75f81509297 100644 --- a/src/main/java/org/truffleruby/debug/TruffleDebugNodes.java +++ b/src/main/java/org/truffleruby/debug/TruffleDebugNodes.java @@ -406,13 +406,13 @@ public Object javaObject() { @CoreMethod(names = "foreign_object", onSingleton = true) public abstract static class ForeignObjectNode extends CoreMethodArrayArgumentsNode { - private static class ForeignObject implements TruffleObject { + public static class ForeignObject implements TruffleObject { @Override public ForeignAccess getForeignAccess() { - throw new UnsupportedOperationException(); + return ForeignObjectMessageResolutionForeign.ACCESS; } - + } @TruffleBoundary diff --git a/src/main/java/org/truffleruby/interop/InteropNodes.java b/src/main/java/org/truffleruby/interop/InteropNodes.java index 0ca12fef2b3f..b76dedbd8492 100644 --- a/src/main/java/org/truffleruby/interop/InteropNodes.java +++ b/src/main/java/org/truffleruby/interop/InteropNodes.java @@ -832,6 +832,39 @@ public boolean isJavaString(Object value) { } + @CoreMethod(names = "java_instanceof?", isModuleFunction = true, required = 2) + public abstract static class InteropJavaInstanceOfNode extends CoreMethodArrayArgumentsNode { + + @Specialization(guards = { + "isJavaObject(boxedInstance)", + "isJavaClassOrInterface(boxedJavaClass)" + }) + public boolean javaInstanceOfJava(Object boxedInstance, TruffleObject boxedJavaClass) { + final Object hostInstance = getContext().getEnv().asHostObject(boxedInstance); + final Class javaClass = (Class) getContext().getEnv().asHostObject(boxedJavaClass); + return javaClass.isAssignableFrom(hostInstance.getClass()); + } + + @Specialization(guards = { + "!isJavaObject(instance)", + "isJavaClassOrInterface(boxedJavaClass)" + }) + public boolean javaInstanceOfNotJava(Object instance, TruffleObject boxedJavaClass) { + final Class javaClass = (Class) getContext().getEnv().asHostObject(boxedJavaClass); + return javaClass.isInstance(instance); + } + + protected boolean isJavaObject(Object object) { + return object instanceof TruffleObject && getContext().getEnv().isHostObject(object); + } + + protected boolean isJavaClassOrInterface(TruffleObject object) { + return getContext().getEnv().isHostObject(object) + && getContext().getEnv().asHostObject(object) instanceof Class; + } + + } + @CoreMethod(names = "to_java_string", isModuleFunction = true, required = 1) public abstract static class InteropToJavaStringNode extends CoreMethodArrayArgumentsNode { diff --git a/src/main/java/org/truffleruby/interop/OutgoingForeignCallNode.java b/src/main/java/org/truffleruby/interop/OutgoingForeignCallNode.java index e3428f2077b0..a9ad8da49f4e 100644 --- a/src/main/java/org/truffleruby/interop/OutgoingForeignCallNode.java +++ b/src/main/java/org/truffleruby/interop/OutgoingForeignCallNode.java @@ -119,11 +119,13 @@ protected OutgoingNode createHelperNode(int argsLength) { || name.equals("keys") || name.equals("class") || name.equals("to_s") - || name.equals("to_str")) { + || name.equals("to_str") + || name.equals("is_a?")) { final int expectedArgsLength; switch (name) { case "delete": + case "is_a?": expectedArgsLength = 1; break; case "size": diff --git a/src/main/ruby/core/truffle/interop.rb b/src/main/ruby/core/truffle/interop.rb index b8adc002abe0..834233e1dc9c 100644 --- a/src/main/ruby/core/truffle/interop.rb +++ b/src/main/ruby/core/truffle/interop.rb @@ -166,6 +166,24 @@ def self.special_form(receiver, name, *args) receiver = Truffle::Interop.unbox_if_needed(receiver) raise NameError, 'no method to_str' unless receiver.is_a?(String) receiver + when :is_a? + receiver = Truffle::Interop.unbox_if_needed(receiver) + check_class = args.first + if Truffle::Interop.foreign?(receiver) + if !TruffleRuby.native? && Truffle::Interop.java_class?(check_class) + # Checking against a Java class + Truffle::Interop.java_instanceof?(receiver, check_class) + elsif Truffle::Interop.foreign?(check_class) + # Checking a foreign (not Java) object against a foreign (not Java) class + raise TypeError, 'cannot check if a foreign object is an instance of a foreign class' + else + # Checking a foreign or Java object against a Ruby class + false + end + else + # The receiver unboxed to a Ruby object or a primitive + receiver.is_a?(check_class) + end else raise end @@ -185,13 +203,11 @@ def self.respond_to?(object, name) Truffle::Interop.executable?(object) when :class Truffle::Interop.java_class?(object) - when :inspect - true - when :to_s - true when :to_str object = Truffle::Interop.unbox_if_needed(object) !Truffle::Interop.foreign?(object) && object.is_a?(String) + when :inspect, :to_s, :is_a? + true else false end