Skip to content

Commit 7cc6073

Browse files
committed
[GR-33155] Implement interop exception messages on RubyException
PullRequest: truffleruby/3497
2 parents f2d0b66 + 2c34c01 commit 7cc6073

File tree

10 files changed

+240
-35
lines changed

10 files changed

+240
-35
lines changed

doc/contributor/interop_details.md

+62-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- **a `Class`**
1717
- **a `Hash`**
1818
- **an `Array`**
19+
- **an `Exception`**
20+
- **an `Exception` with a cause**
1921
- **`proc {...}`**
2022
- **`lambda {...}`**
2123
- **a `Method`**
@@ -307,11 +309,11 @@ When interop message `getHashValuesIterator` is sent
307309
## Members related messages (incomplete)
308310

309311
When interop message `readMember` is sent
310-
- to any non-immediate `Object` like **`nil`**, **`:symbol`**, **a `String`**, **a `BigDecimal`**, **an `Object`**, **a frozen `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
312+
- to any non-immediate `Object` like **`nil`**, **`:symbol`**, **a `String`**, **a `BigDecimal`**, **an `Object`**, **a frozen `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **an `Exception`**, **an `Exception` with a cause**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
311313
it returns a method with the given name when the method is defined.
312-
- to any non-immediate `Object` like **`nil`**, **`:symbol`**, **a `String`**, **a `BigDecimal`**, **an `Object`**, **a frozen `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
314+
- to any non-immediate `Object` like **`nil`**, **`:symbol`**, **a `String`**, **a `BigDecimal`**, **an `Object`**, **a frozen `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **an `Exception`**, **an `Exception` with a cause**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
313315
it fails with `UnknownIdentifierException` when the method is not defined.
314-
- to any non-immediate `Object` like **a `String`**, **an `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
316+
- to any non-immediate `Object` like **a `String`**, **an `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **an `Exception`**, **an `Exception` with a cause**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
315317
it reads the given instance variable.
316318
- to **polyglot members**
317319
it reads the value stored with the given name.
@@ -321,7 +323,7 @@ When interop message `readMember` is sent
321323
it fails with `UnsupportedMessageError`.
322324

323325
When interop message `writeMember` is sent
324-
- to any non-immediate non-frozen `Object` like **a `String`**, **an `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
326+
- to any non-immediate non-frozen `Object` like **a `String`**, **an `Object`**, **a `StructWithValue`**, **a `Class`**, **a `Hash`**, **an `Array`**, **an `Exception`**, **an `Exception` with a cause**, **`proc {...}`**, **`lambda {...}`**, **a `Method`**, **a `Truffle::FFI::Pointer`**, **polyglot pointer**, **polyglot array** or **polyglot hash**
325327
it writes the given instance variable.
326328
- to **polyglot members**
327329
it writes the given value under the given name.
@@ -332,10 +334,64 @@ When interop message `writeMember` is sent
332334
- otherwise
333335
it fails with `UnsupportedMessageError`.
334336

337+
## Exception related messages
338+
339+
When interop message `isException` is sent
340+
- to **an `Exception`** or **an `Exception` with a cause**
341+
it returns true.
342+
- otherwise
343+
it returns false.
344+
345+
When interop message `throwException` is sent
346+
- to **an `Exception`** or **an `Exception` with a cause**
347+
it throws the exception.
348+
- otherwise
349+
it fails with `UnsupportedMessageError`.
350+
351+
When interop message `getExceptionType` is sent
352+
- to **an `Exception`** or **an `Exception` with a cause**
353+
it returns the exception type.
354+
- otherwise
355+
it fails with `UnsupportedMessageError`.
356+
357+
When interop message `hasExceptionMessage` is sent
358+
- to **an `Exception`** or **an `Exception` with a cause**
359+
it returns true.
360+
- otherwise
361+
it returns false.
362+
363+
When interop message `getExceptionMessage` is sent
364+
- to **an `Exception`** or **an `Exception` with a cause**
365+
it returns the message of the exception.
366+
- otherwise
367+
it fails with `UnsupportedMessageError`.
368+
369+
When interop message `hasExceptionStackTrace` is sent
370+
- to **an `Exception`** or **an `Exception` with a cause**
371+
it returns true.
372+
- otherwise
373+
it returns false.
374+
375+
When interop message `getExceptionStackTrace` is sent
376+
- to **an `Exception`** or **an `Exception` with a cause**
377+
it returns the stacktrace of the exception.
378+
- otherwise
379+
it fails with `UnsupportedMessageError`.
380+
381+
When interop message `hasExceptionCause` is sent
382+
- to **an `Exception` with a cause**
383+
it returns true.
384+
- otherwise
385+
it returns false.
386+
387+
When interop message `getExceptionCause` is sent
388+
- to **an `Exception` with a cause**
389+
it returns the cause of the exception.
390+
- otherwise
391+
it fails with `UnsupportedMessageError`.
392+
335393
## Number related messages (missing)
336394

337395
## Instantiation related messages (missing)
338396

339-
## Exception related messages (missing)
340-
341397
## Time related messages (unimplemented)

spec/truffle/interop/matrix_spec.rb

+72-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# truffleruby_primitives: true
2+
13
# Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved. This
24
# code is released under a tri EPL/GPL/LGPL license. You can use it,
35
# redistribute it and/or modify it under the terms of the:
@@ -243,6 +245,33 @@ def spec_it(subject)
243245
module: Subject.(name: AN_INSTANCE) { Module.new },
244246
hash: Subject.(name: AN_INSTANCE, doc: true) { {} },
245247
array: Subject.(name: AN_INSTANCE, doc: true) { [] },
248+
# raise & rescue to give it a backtrace
249+
exception: Subject.(name: "an `Exception`", doc: true) do
250+
begin
251+
raise "the exception message"
252+
rescue => e
253+
e
254+
end
255+
end,
256+
exception_with_cause: Subject.(name: "an `Exception` with a cause", doc: true) do
257+
begin
258+
raise "the cause"
259+
rescue
260+
begin
261+
raise "the exception message"
262+
rescue => e
263+
e
264+
end
265+
end
266+
end,
267+
# also test RaiseException since it is what other languages see when they catch an exception from Ruby
268+
raise_exception: Subject.(name: AN_INSTANCE) do
269+
begin
270+
raise "the exception message"
271+
rescue => e
272+
Primitive.exception_get_raise_exception(e)
273+
end
274+
end,
246275

247276
proc: Subject.(proc { |v| v }, name: code("proc {...}"), doc: true),
248277
lambda: Subject.(-> v { v }, name: code("lambda {...}"), doc: true),
@@ -286,6 +315,7 @@ def spec_it(subject)
286315
immediate_subjects = [:false, :true, :zero, :small_integer, :zero_float, :small_float]
287316
non_immediate_subjects = SUBJECTS.keys - immediate_subjects
288317
frozen_subjects = [:big_decimal, :nil, :symbol, :strange_symbol, :frozen_object]
318+
exception_subjects = [:exception, :exception_with_cause, :raise_exception]
289319

290320
# not part of the standard matrix, not considered in last rest case
291321
EXTRA_SUBJECTS = {
@@ -298,7 +328,7 @@ def spec_it(subject)
298328
def predicate(name, is, *message_args, &setup)
299329
-> subject do
300330
setup.call subject if setup
301-
Truffle::Interop.send(name, subject, *message_args).send(is ? :should : :should_not, be_true)
331+
Truffle::Interop.send(name, subject, *message_args).should == is
302332
end
303333
end
304334

@@ -580,7 +610,7 @@ def array_element_predicate(message, predicate, insert_on_true_case)
580610
Delimiter["Members related messages (incomplete)"],
581611
Message[:readMember,
582612
Test.new("returns a method with the given name when the method is defined", "any non-immediate `Object`",
583-
*non_immediate_subjects - [:polyglot_object]) do |subject|
613+
*non_immediate_subjects - [:polyglot_object, :raise_exception]) do |subject|
584614
Truffle::Interop.read_member(subject, 'to_s').should == subject.method(:to_s)
585615
end,
586616
Test.new("fails with `UnknownIdentifierException` when the method is not defined", "any non-immediate `Object`",
@@ -626,9 +656,48 @@ def array_element_predicate(message, predicate, insert_on_true_case)
626656
end,
627657
unsupported_test { |subject| Truffle::Interop.write_member(subject, :something, 'val') }],
628658

659+
Delimiter["Exception related messages"],
660+
Message[:isException,
661+
Test.new("returns true", *exception_subjects, &predicate(:exception?, true)),
662+
Test.new("returns false", &predicate(:exception?, false))],
663+
Message[:throwException,
664+
Test.new("throws the exception", *exception_subjects) do |subject|
665+
-> { Truffle::Interop.throw_exception(subject) }.should raise_error { |e| e.should.equal?(subject) }
666+
end,
667+
unsupported_test { |subject| Truffle::Interop.throw_exception(subject) }],
668+
Message[:getExceptionType,
669+
Test.new("returns the exception type", *exception_subjects) do |subject|
670+
Truffle::Interop.exception_type(subject).should == :RUNTIME_ERROR
671+
end,
672+
unsupported_test { |subject| Truffle::Interop.exception_type(subject) }],
673+
Message[:hasExceptionMessage,
674+
Test.new("returns true", *exception_subjects, &predicate(:has_exception_message?, true)),
675+
Test.new("returns false", &predicate(:has_exception_message?, false))],
676+
Message[:getExceptionMessage,
677+
Test.new("returns the message of the exception", *exception_subjects) do |subject|
678+
Truffle::Interop.exception_message(subject).should == "the exception message"
679+
end,
680+
unsupported_test { |subject| Truffle::Interop.exception_message(subject) }],
681+
Message[:hasExceptionStackTrace,
682+
Test.new("returns true", *exception_subjects, &predicate(:has_exception_stack_trace?, true)),
683+
Test.new("returns false", &predicate(:has_exception_stack_trace?, false))],
684+
Message[:getExceptionStackTrace,
685+
Test.new("returns the stacktrace of the exception", *exception_subjects) do |subject|
686+
stacktrace = Truffle::Interop.exception_stack_trace(subject)
687+
Truffle::Interop.should.has_array_elements?(stacktrace)
688+
end,
689+
unsupported_test { |subject| Truffle::Interop.exception_stack_trace(subject) }],
690+
Message[:hasExceptionCause,
691+
Test.new("returns true", :exception_with_cause, &predicate(:has_exception_cause?, true)),
692+
Test.new("returns false", &predicate(:has_exception_cause?, false))],
693+
Message[:getExceptionCause,
694+
Test.new("returns the cause of the exception", :exception_with_cause) do |subject|
695+
Truffle::Interop.exception_cause(subject).should == subject.cause
696+
end,
697+
unsupported_test { |subject| Truffle::Interop.exception_cause(subject) }],
698+
629699
Delimiter["Number related messages (missing)"],
630700
Delimiter["Instantiation related messages (missing)"],
631-
Delimiter["Exception related messages (missing)"],
632701
Delimiter["Time related messages (unimplemented)"],
633702
]
634703

src/main/java/org/truffleruby/core/exception/ExceptionNodes.java

+15
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,19 @@ protected int limit() {
353353

354354
}
355355

356+
@Primitive(name = "exception_get_raise_exception")
357+
public abstract static class GetRaiseExceptionNode extends CoreMethodArrayArgumentsNode {
358+
359+
@Specialization
360+
protected Object getRaiseException(RubyException exception) {
361+
RaiseException raiseException = exception.backtrace.getRaiseException();
362+
if (raiseException != null) {
363+
return raiseException;
364+
} else {
365+
return nil;
366+
}
367+
}
368+
369+
}
370+
356371
}

src/main/java/org/truffleruby/core/exception/ExceptionOperations.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public static String getMessage(Throwable throwable) {
9797
}
9898

9999
@TruffleBoundary
100-
private static String messageFieldToString(RubyException exception) {
100+
public static String messageFieldToString(RubyException exception) {
101101
Object message = exception.message;
102102
RubyStringLibrary strings = RubyStringLibrary.getUncached();
103103
if (message == null || message == Nil.INSTANCE) {
@@ -115,7 +115,7 @@ public static String messageToString(RubyException exception) {
115115
Object messageObject = null;
116116
try {
117117
messageObject = DispatchNode.getUncached().call(exception, "message");
118-
} catch (Throwable e) {
118+
} catch (RaiseException e) {
119119
// Fall back to the internal message field
120120
}
121121
if (messageObject != null && RubyStringLibrary.getUncached().isRubyString(messageObject)) {

src/main/java/org/truffleruby/core/exception/RubyException.java

+49
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111

1212
import java.util.Set;
1313

14+
import com.oracle.truffle.api.TruffleStackTraceElement;
1415
import com.oracle.truffle.api.interop.ExceptionType;
1516
import com.oracle.truffle.api.interop.InteropLibrary;
17+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
1618
import com.oracle.truffle.api.library.CachedLibrary;
1719
import com.oracle.truffle.api.library.ExportLibrary;
1820
import com.oracle.truffle.api.library.ExportMessage;
1921
import com.oracle.truffle.api.nodes.Node;
2022
import org.truffleruby.RubyContext;
23+
import org.truffleruby.RubyLanguage;
2124
import org.truffleruby.core.VMPrimitiveNodes.VMRaiseExceptionNode;
25+
import org.truffleruby.core.array.ArrayHelpers;
2226
import org.truffleruby.core.array.RubyArray;
2327
import org.truffleruby.core.klass.RubyClass;
2428
import org.truffleruby.core.proc.RubyProc;
@@ -33,6 +37,8 @@
3337
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
3438
import com.oracle.truffle.api.object.Shape;
3539

40+
import static org.truffleruby.language.RubyBaseNode.nil;
41+
3642
@ExportLibrary(InteropLibrary.class)
3743
public class RubyException extends RubyDynamicObject implements ObjectGraphNode {
3844

@@ -96,6 +102,49 @@ public RuntimeException throwException(
96102
public ExceptionType getExceptionType() {
97103
return ExceptionType.RUNTIME_ERROR;
98104
}
105+
106+
@ExportMessage
107+
public boolean hasExceptionCause() {
108+
return this.cause != nil;
109+
}
110+
111+
@ExportMessage
112+
public Object getExceptionCause() throws UnsupportedMessageException {
113+
if (!hasExceptionCause()) {
114+
throw UnsupportedMessageException.create();
115+
}
116+
return this.cause;
117+
}
118+
119+
@ExportMessage
120+
public boolean hasExceptionMessage() {
121+
return true;
122+
}
123+
124+
@ExportMessage
125+
public Object getExceptionMessage() {
126+
return ExceptionOperations.messageToString(this);
127+
}
128+
129+
@ExportMessage
130+
public boolean hasExceptionStackTrace() {
131+
return this.backtrace != null;
132+
}
133+
134+
@TruffleBoundary
135+
@ExportMessage
136+
public Object getExceptionStackTrace() throws UnsupportedMessageException {
137+
if (!hasExceptionStackTrace()) {
138+
throw UnsupportedMessageException.create();
139+
}
140+
141+
TruffleStackTraceElement[] stackTrace = this.backtrace.getStackTrace();
142+
Object[] items = new Object[stackTrace.length];
143+
for (int i = 0; i < items.length; i++) {
144+
items[i] = stackTrace[i].getGuestObject();
145+
}
146+
return ArrayHelpers.createArray(RubyContext.get(null), RubyLanguage.get(null), items);
147+
}
99148
// endregion
100149

101150
}

src/main/java/org/truffleruby/language/control/RaiseException.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@
1515
import org.truffleruby.RubyContext;
1616
import org.truffleruby.core.exception.ExceptionOperations;
1717
import org.truffleruby.core.exception.RubyException;
18-
import org.truffleruby.core.module.ModuleFields;
1918
import org.truffleruby.language.backtrace.Backtrace;
2019

21-
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
22-
2320
/** A ControlFlowException holding a Ruby exception. */
2421
@SuppressWarnings("serial")
2522
@ExportLibrary(value = InteropLibrary.class, delegateTo = "exception")
@@ -55,11 +52,8 @@ public RubyException getException() {
5552
}
5653

5754
@Override
58-
@TruffleBoundary
5955
public String getMessage() {
60-
final ModuleFields exceptionClass = exception.getLogicalClass().fields;
61-
final String message = ExceptionOperations.messageToString(exception);
62-
return String.format("%s (%s)", message, exceptionClass.getName());
56+
return ExceptionOperations.messageFieldToString(exception);
6357
}
6458

6559
}

0 commit comments

Comments
 (0)