Skip to content

Commit 4cdbeb7

Browse files
committed
[GR-33908] [GR-34933] Improve handling of foreign exception and make Polyglot::ForeignException a class inheriting from Exception
PullRequest: truffleruby/3210
2 parents b851608 + fc841b3 commit 4cdbeb7

29 files changed

+432
-265
lines changed

doc/user/polyglot.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,14 @@ Where boolean value is expected (e.g., in `if` conditions) the foreign value is
117117
### Rescuing Foreign Exceptions
118118

119119
Foreign exceptions can be caught by `rescue Polyglot::ForeignException => e` or by `rescue foreign_meta_object`.
120-
It is possible to rescue any exception (Ruby or foreign) with `rescue Object => e`.
120+
It is possible to rescue any exception (Ruby or foreign) with `rescue Exception => e`.
121121

122122
This naturally stems from the ancestors of a foreign exception:
123123
```ruby
124124
Java.type("java.lang.RuntimeException").new.class.ancestors
125-
# => [Polyglot::ForeignExceptionClass, Polyglot::ForeignException, Polyglot::ForeignObject, Object, Kernel, BasicObject]
125+
# => [Polyglot::ForeignException, Polyglot::ExceptionTrait, Polyglot::ObjectTrait, Exception, Object, Kernel, BasicObject]
126126
```
127127

128-
Note `Polyglot::ForeignException` is a module and not class, so to ensure it's included for all foreign exceptions.
129-
130128
## Accessing Java Objects
131129

132130
TruffleRuby's Java interoperability interface is similar to the interface from the Nashorn JavaScript implementation, as also implemented by GraalVM's JavaScript implementation.

spec/ruby/core/exception/full_message_spec.rb

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515
it "supports :highlight option and adds escape sequences to highlight some strings" do
1616
e = RuntimeError.new("Some runtime error")
1717

18-
full_message = e.full_message(highlight: true, order: :bottom)
19-
full_message.should include "\e[1mTraceback\e[m (most recent call last)"
20-
full_message.should include "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)"
18+
full_message = e.full_message(highlight: true, order: :top).lines
19+
full_message[0].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n"
2120

22-
full_message = e.full_message(highlight: false, order: :bottom)
23-
full_message.should include "Traceback (most recent call last)"
24-
full_message.should include "Some runtime error (RuntimeError)"
21+
full_message = e.full_message(highlight: true, order: :bottom).lines
22+
full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n"
23+
full_message[-1].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n"
24+
25+
full_message = e.full_message(highlight: false, order: :top).lines
26+
full_message[0].should.end_with? "Some runtime error (RuntimeError)\n"
27+
28+
full_message = e.full_message(highlight: false, order: :bottom).lines
29+
full_message[0].should == "Traceback (most recent call last):\n"
30+
full_message[-1].should.end_with? "Some runtime error (RuntimeError)\n"
2531
end
2632

2733
it "supports :order option and places the error message and the backtrace at the top or the bottom" do
@@ -35,22 +41,34 @@
3541
it "shows the caller if the exception has no backtrace" do
3642
e = RuntimeError.new("Some runtime error")
3743
e.backtrace.should == nil
38-
full_message = e.full_message(highlight: false, order: :top)
39-
full_message.should include("#{__FILE__}:#{__LINE__-1}:in `")
40-
full_message.should include("': Some runtime error (RuntimeError)\n")
44+
full_message = e.full_message(highlight: false, order: :top).lines
45+
full_message[0].should.start_with?("#{__FILE__}:#{__LINE__-1}:in `")
46+
full_message[0].should.end_with?("': Some runtime error (RuntimeError)\n")
4147
end
4248

4349
it "shows the exception class at the end of the first line of the message when the message contains multiple lines" do
4450
begin
4551
line = __LINE__; raise "first line\nsecond line"
4652
rescue => e
4753
full_message = e.full_message(highlight: false, order: :top).lines
48-
full_message[0].should include("#{__FILE__}:#{line}:in `")
49-
full_message[0].should include(": first line (RuntimeError)\n")
54+
full_message[0].should.start_with?("#{__FILE__}:#{line}:in `")
55+
full_message[0].should.end_with?(": first line (RuntimeError)\n")
5056
full_message[1].should == "second line\n"
5157
end
5258
end
5359

60+
it "highlights the entire message when the message contains multiple lines" do
61+
begin
62+
line = __LINE__; raise "first line\nsecond line\nthird line"
63+
rescue => e
64+
full_message = e.full_message(highlight: true, order: :top).lines
65+
full_message[0].should.start_with?("#{__FILE__}:#{line}:in `")
66+
full_message[0].should.end_with?(": \e[1mfirst line (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n")
67+
full_message[1].should == "\e[1msecond line\e[m\n"
68+
full_message[2].should == "\e[1mthird line\e[m\n"
69+
end
70+
end
71+
5472
it "contains cause of exception" do
5573
begin
5674
begin

spec/ruby/core/thread/shared/exit.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,34 @@
6666
ScratchPad.recorded.should == nil
6767
end
6868

69+
it "does not reset $!" do
70+
ScratchPad.record []
71+
72+
exc = RuntimeError.new("foo")
73+
thread = Thread.new do
74+
begin
75+
raise exc
76+
ensure
77+
ScratchPad << $!
78+
begin
79+
Thread.current.send(@method)
80+
ensure
81+
ScratchPad << $!
82+
end
83+
end
84+
end
85+
thread.join
86+
ScratchPad.recorded.should == [exc, exc]
87+
end
88+
6989
it "cannot be rescued" do
7090
thread = Thread.new do
7191
begin
7292
Thread.current.send(@method)
7393
rescue Exception
7494
ScratchPad.record :in_rescue
7595
end
76-
ScratchPad.record :end_of_thread_block
96+
ScratchPad.record :end_of_thread_block
7797
end
7898

7999
thread.join
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
slow:Polyglot::ForeignException when reaching the top-level is printed like a Ruby exception

spec/truffle/interop/foreign_inspect_to_s_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
-> {
5252
integer_class.valueOf("abc")
5353
}.should raise_error(Polyglot::ForeignException) { |exc|
54-
exc.inspect.should =~ /\A#<Polyglot::ForeignExceptionClass\[Java\] java\.lang\.NumberFormatException:0x\h+: For input string: "abc">\z/
55-
exc.to_s.should == '#<Polyglot::ForeignExceptionClass[Java] java.lang.NumberFormatException: For input string: "abc">'
54+
exc.inspect.should =~ /\A#<Polyglot::ForeignException\[Java\] java\.lang\.NumberFormatException:0x\h+: For input string: "abc">\z/
55+
exc.to_s.should == '#<Polyglot::ForeignException[Java] java.lang.NumberFormatException: For input string: "abc">'
5656
}
5757
end
5858
end
@@ -145,8 +145,8 @@
145145
describe "exception" do
146146
it "gives a similar representation to Ruby" do
147147
exc = Truffle::Debug.foreign_exception("foo")
148-
exc.inspect.should =~ /\A#<Polyglot::ForeignExceptionClass:0x\h+: foo>\z/
149-
exc.to_s.should == '#<Polyglot::ForeignExceptionClass [foreign exception]>'
148+
exc.inspect.should =~ /\A#<Polyglot::ForeignException:0x\h+: foo>\z/
149+
exc.to_s.should == '#<Polyglot::ForeignException [foreign exception]>'
150150
end
151151
end
152152

spec/truffle/interop/polyglot/class_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
Truffle::Debug.foreign_hash.class.should == Polyglot::ForeignHash
1616
Truffle::Debug.foreign_array.class.should == Polyglot::ForeignArray
17-
Truffle::Debug.foreign_exception("").class.should == Polyglot::ForeignExceptionClass
17+
Truffle::Debug.foreign_exception("").class.should == Polyglot::ForeignException
1818
Truffle::Debug.foreign_executable(14).class.should == Polyglot::ForeignExecutable
1919
# ForeignClass, ForeignMetaObject
2020
Truffle::Debug.foreign_iterable.class.should == Polyglot::ForeignIterable
@@ -31,7 +31,7 @@
3131
it "gives a Ruby Class to Ruby objects behind a foreign proxy" do
3232
Truffle::Interop.proxy_foreign_object({}).class.should == Polyglot::ForeignHashIterable
3333
Truffle::Interop.proxy_foreign_object([1, 2, 3]).class.should == Polyglot::ForeignArray
34-
Truffle::Interop.proxy_foreign_object(Exception.new("")).class.should == Polyglot::ForeignExceptionClass
34+
Truffle::Interop.proxy_foreign_object(Exception.new("")).class.should == Polyglot::ForeignException
3535
Truffle::Interop.proxy_foreign_object(-> { nil }).class.should == Polyglot::ForeignExecutable
3636
Truffle::Interop.proxy_foreign_object(String).class.should == Polyglot::ForeignClass
3737
Truffle::Interop.proxy_foreign_object(Enumerable).class.should == Polyglot::ForeignMetaObject
@@ -51,7 +51,7 @@
5151
Truffle::Interop.to_java_map({ a: 1 }).class.should == Polyglot::ForeignHash
5252
Truffle::Interop.to_java_array([1, 2, 3]).class.should == Polyglot::ForeignArray
5353
Truffle::Interop.to_java_list([1, 2, 3]).class.should == Polyglot::ForeignArray
54-
Java.type('java.lang.RuntimeException').new.class.should == Polyglot::ForeignExceptionClass
54+
Java.type('java.lang.RuntimeException').new.class.should == Polyglot::ForeignException
5555
# ForeignExecutable
5656
Java.type('java.math.BigInteger').class.should == Polyglot::ForeignClass
5757
Java.type('java.math.BigInteger')[:class].class.should == Polyglot::ForeignClass

spec/truffle/interop/polyglot/foreign_exception_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
@foreign = Truffle::Debug.foreign_exception("exception message")
1414
end
1515

16+
it "subclasses Exception" do
17+
@foreign.class.should == Polyglot::ForeignException
18+
@foreign.class.superclass.should == Exception
19+
end
20+
1621
it "supports #message" do
1722
@foreign.message.should == "exception message"
1823
end
@@ -21,6 +26,15 @@
2126
@foreign.cause.should == nil
2227
end
2328

29+
it "supports #full_message" do
30+
-> {
31+
raise @foreign
32+
}.should raise_error(Polyglot::ForeignException) {
33+
full_message = @foreign.full_message(highlight: false, order: :top).lines
34+
full_message[0].should == "#{__FILE__}:#{__LINE__-3}:in `Kernel#raise': exception message (Polyglot::ForeignException)\n"
35+
}
36+
end
37+
2438
it "supports rescue Polyglot::ForeignException" do
2539
begin
2640
raise @foreign
@@ -29,6 +43,14 @@
2943
end
3044
end
3145

46+
it "supports rescue Exception" do
47+
begin
48+
raise @foreign
49+
rescue Exception => e # rubocop:disable Lint/RescueException
50+
e.should.equal?(@foreign)
51+
end
52+
end
53+
3254
it "supports rescue Object" do
3355
begin
3456
raise @foreign
@@ -67,4 +89,16 @@
6789
entry.label.should.is_a?(String)
6890
end
6991
end
92+
93+
describe "when reaching the top-level" do
94+
it "is printed like a Ruby exception" do
95+
out = ruby_exe('raise Truffle::Debug.foreign_exception "main"', args: "2>&1", exit_status: 1, escape: false)
96+
out.should == "-e:1:in `Kernel#raise': main (Polyglot::ForeignException)\n" \
97+
"\tfrom -e:1:in `<main>'\n"
98+
99+
out = ruby_exe('at_exit { raise Truffle::Debug.foreign_exception "at exit" }', args: "2>&1", exit_status: 1, escape: false)
100+
out.should == "-e:1:in `Kernel#raise': at exit (Polyglot::ForeignException)\n" \
101+
"\tfrom -e:1:in `block in <main>'\n"
102+
end
103+
end
70104
end

src/main/java/org/truffleruby/RubyContext.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.oracle.truffle.api.RootCallTarget;
3030
import com.oracle.truffle.api.TruffleLanguage.ContextReference;
3131
import com.oracle.truffle.api.TruffleLogger;
32+
import com.oracle.truffle.api.exception.AbstractTruffleException;
3233
import com.oracle.truffle.api.nodes.EncapsulatingNodeReference;
3334
import com.oracle.truffle.api.nodes.Node;
3435
import com.oracle.truffle.api.utilities.AssumedValue;
@@ -66,7 +67,6 @@
6667
import org.truffleruby.language.RubyBaseNode;
6768
import org.truffleruby.language.SafepointManager;
6869
import org.truffleruby.language.backtrace.BacktraceFormatter;
69-
import org.truffleruby.language.control.RaiseException;
7070
import org.truffleruby.language.dispatch.DispatchNode;
7171
import org.truffleruby.language.globals.GlobalVariableStorage;
7272
import org.truffleruby.language.loader.CodeLoader;
@@ -334,9 +334,9 @@ protected boolean patch(Env newEnv) {
334334
protected boolean patchContext(Env newEnv) {
335335
try {
336336
return patch(newEnv);
337-
} catch (RaiseException e) {
337+
} catch (AbstractTruffleException e) {
338338
getDefaultBacktraceFormatter()
339-
.printRubyExceptionOnEnvStderr("Exception during RubyContext.patch():\n", e.getException());
339+
.printRubyExceptionOnEnvStderr("Exception during RubyContext.patch():\n", e);
340340
throw e;
341341
} catch (Throwable e) {
342342
System.err.println("Exception during RubyContext.patch():");

src/main/java/org/truffleruby/core/CoreLibrary.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.concurrent.ConcurrentMap;
2424

2525
import com.oracle.truffle.api.RootCallTarget;
26+
import com.oracle.truffle.api.exception.AbstractTruffleException;
2627
import org.graalvm.collections.Pair;
2728
import org.jcodings.specific.USASCIIEncoding;
2829
import org.jcodings.transcode.EConvFlags;
@@ -54,7 +55,6 @@
5455
import org.truffleruby.core.string.ImmutableRubyString;
5556
import org.truffleruby.language.Nil;
5657
import org.truffleruby.language.NotProvided;
57-
import org.truffleruby.language.control.RaiseException;
5858
import org.truffleruby.language.globals.GlobalVariableReader;
5959
import org.truffleruby.language.globals.GlobalVariables;
6060
import org.truffleruby.language.loader.CodeLoader;
@@ -786,10 +786,9 @@ public void loadRubyCoreLibraryAndPostBoot() {
786786
}
787787
} catch (IOException e) {
788788
throw CompilerDirectives.shouldNotReachHere(e);
789-
} catch (RaiseException e) {
790-
context.getDefaultBacktraceFormatter().printRubyExceptionOnEnvStderr(
791-
"Exception while loading core library:\n",
792-
e.getException());
789+
} catch (AbstractTruffleException e) {
790+
context.getDefaultBacktraceFormatter()
791+
.printRubyExceptionOnEnvStderr("Exception while loading core library:\n", e);
793792
throw CompilerDirectives.shouldNotReachHere("couldn't load the core library", e);
794793
}
795794
}

src/main/java/org/truffleruby/core/VMPrimitiveNodes.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,17 @@ public abstract static class VMRaiseExceptionNode extends PrimitiveArrayArgument
208208
protected Object vmRaiseException(RubyException exception,
209209
@Cached ConditionProfile reRaiseProfile) {
210210
final Backtrace backtrace = exception.backtrace;
211-
if (reRaiseProfile.profile(backtrace != null && backtrace.getRaiseException() != null)) {
211+
RaiseException raiseException = null;
212+
if (reRaiseProfile.profile(backtrace != null && (raiseException = backtrace.getRaiseException()) != null)) {
212213
// We need to rethrow the existing RaiseException, otherwise we would lose the
213214
// TruffleStackTrace stored in it.
214-
assert backtrace.getRaiseException().getException() == exception;
215+
assert raiseException.getException() == exception;
215216

216217
if (getContext().getOptions().BACKTRACE_ON_RAISE) {
217-
getContext().getDefaultBacktraceFormatter().printRubyExceptionOnEnvStderr("raise: ", exception);
218+
getContext().getDefaultBacktraceFormatter().printRubyExceptionOnEnvStderr("raise: ",
219+
raiseException);
218220
}
219-
throw backtrace.getRaiseException();
221+
throw raiseException;
220222
} else {
221223
throw new RaiseException(getContext(), exception);
222224
}

0 commit comments

Comments
 (0)