Skip to content

Commit

Permalink
[GR-33155] Implement interop exception messages on RubyException
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/3497
  • Loading branch information
eregon committed Sep 29, 2022
2 parents f2d0b66 + 2c34c01 commit 7cc6073
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 35 deletions.
68 changes: 62 additions & 6 deletions doc/contributor/interop_details.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- **a `Class`**
- **a `Hash`**
- **an `Array`**
- **an `Exception`**
- **an `Exception` with a cause**
- **`proc {...}`**
- **`lambda {...}`**
- **a `Method`**
Expand Down Expand Up @@ -307,11 +309,11 @@ When interop message `getHashValuesIterator` is sent
## Members related messages (incomplete)

When interop message `readMember` is sent
- 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**
- 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**
it returns a method with the given name when the method is defined.
- 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**
- 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**
it fails with `UnknownIdentifierException` when the method is not defined.
- 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**
- 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**
it reads the given instance variable.
- to **polyglot members**
it reads the value stored with the given name.
Expand All @@ -321,7 +323,7 @@ When interop message `readMember` is sent
it fails with `UnsupportedMessageError`.

When interop message `writeMember` is sent
- 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**
- 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**
it writes the given instance variable.
- to **polyglot members**
it writes the given value under the given name.
Expand All @@ -332,10 +334,64 @@ When interop message `writeMember` is sent
- otherwise
it fails with `UnsupportedMessageError`.

## Exception related messages

When interop message `isException` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns true.
- otherwise
it returns false.

When interop message `throwException` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it throws the exception.
- otherwise
it fails with `UnsupportedMessageError`.

When interop message `getExceptionType` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns the exception type.
- otherwise
it fails with `UnsupportedMessageError`.

When interop message `hasExceptionMessage` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns true.
- otherwise
it returns false.

When interop message `getExceptionMessage` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns the message of the exception.
- otherwise
it fails with `UnsupportedMessageError`.

When interop message `hasExceptionStackTrace` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns true.
- otherwise
it returns false.

When interop message `getExceptionStackTrace` is sent
- to **an `Exception`** or **an `Exception` with a cause**
it returns the stacktrace of the exception.
- otherwise
it fails with `UnsupportedMessageError`.

When interop message `hasExceptionCause` is sent
- to **an `Exception` with a cause**
it returns true.
- otherwise
it returns false.

When interop message `getExceptionCause` is sent
- to **an `Exception` with a cause**
it returns the cause of the exception.
- otherwise
it fails with `UnsupportedMessageError`.

## Number related messages (missing)

## Instantiation related messages (missing)

## Exception related messages (missing)

## Time related messages (unimplemented)
75 changes: 72 additions & 3 deletions spec/truffle/interop/matrix_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# truffleruby_primitives: true

# Copyright (c) 2018, 2021 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:
Expand Down Expand Up @@ -243,6 +245,33 @@ def spec_it(subject)
module: Subject.(name: AN_INSTANCE) { Module.new },
hash: Subject.(name: AN_INSTANCE, doc: true) { {} },
array: Subject.(name: AN_INSTANCE, doc: true) { [] },
# raise & rescue to give it a backtrace
exception: Subject.(name: "an `Exception`", doc: true) do
begin
raise "the exception message"
rescue => e
e
end
end,
exception_with_cause: Subject.(name: "an `Exception` with a cause", doc: true) do
begin
raise "the cause"
rescue
begin
raise "the exception message"
rescue => e
e
end
end
end,
# also test RaiseException since it is what other languages see when they catch an exception from Ruby
raise_exception: Subject.(name: AN_INSTANCE) do
begin
raise "the exception message"
rescue => e
Primitive.exception_get_raise_exception(e)
end
end,

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

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

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

Delimiter["Exception related messages"],
Message[:isException,
Test.new("returns true", *exception_subjects, &predicate(:exception?, true)),
Test.new("returns false", &predicate(:exception?, false))],
Message[:throwException,
Test.new("throws the exception", *exception_subjects) do |subject|
-> { Truffle::Interop.throw_exception(subject) }.should raise_error { |e| e.should.equal?(subject) }
end,
unsupported_test { |subject| Truffle::Interop.throw_exception(subject) }],
Message[:getExceptionType,
Test.new("returns the exception type", *exception_subjects) do |subject|
Truffle::Interop.exception_type(subject).should == :RUNTIME_ERROR
end,
unsupported_test { |subject| Truffle::Interop.exception_type(subject) }],
Message[:hasExceptionMessage,
Test.new("returns true", *exception_subjects, &predicate(:has_exception_message?, true)),
Test.new("returns false", &predicate(:has_exception_message?, false))],
Message[:getExceptionMessage,
Test.new("returns the message of the exception", *exception_subjects) do |subject|
Truffle::Interop.exception_message(subject).should == "the exception message"
end,
unsupported_test { |subject| Truffle::Interop.exception_message(subject) }],
Message[:hasExceptionStackTrace,
Test.new("returns true", *exception_subjects, &predicate(:has_exception_stack_trace?, true)),
Test.new("returns false", &predicate(:has_exception_stack_trace?, false))],
Message[:getExceptionStackTrace,
Test.new("returns the stacktrace of the exception", *exception_subjects) do |subject|
stacktrace = Truffle::Interop.exception_stack_trace(subject)
Truffle::Interop.should.has_array_elements?(stacktrace)
end,
unsupported_test { |subject| Truffle::Interop.exception_stack_trace(subject) }],
Message[:hasExceptionCause,
Test.new("returns true", :exception_with_cause, &predicate(:has_exception_cause?, true)),
Test.new("returns false", &predicate(:has_exception_cause?, false))],
Message[:getExceptionCause,
Test.new("returns the cause of the exception", :exception_with_cause) do |subject|
Truffle::Interop.exception_cause(subject).should == subject.cause
end,
unsupported_test { |subject| Truffle::Interop.exception_cause(subject) }],

Delimiter["Number related messages (missing)"],
Delimiter["Instantiation related messages (missing)"],
Delimiter["Exception related messages (missing)"],
Delimiter["Time related messages (unimplemented)"],
]

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/truffleruby/core/exception/ExceptionNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,19 @@ protected int limit() {

}

@Primitive(name = "exception_get_raise_exception")
public abstract static class GetRaiseExceptionNode extends CoreMethodArrayArgumentsNode {

@Specialization
protected Object getRaiseException(RubyException exception) {
RaiseException raiseException = exception.backtrace.getRaiseException();
if (raiseException != null) {
return raiseException;
} else {
return nil;
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static String getMessage(Throwable throwable) {
}

@TruffleBoundary
private static String messageFieldToString(RubyException exception) {
public static String messageFieldToString(RubyException exception) {
Object message = exception.message;
RubyStringLibrary strings = RubyStringLibrary.getUncached();
if (message == null || message == Nil.INSTANCE) {
Expand All @@ -115,7 +115,7 @@ public static String messageToString(RubyException exception) {
Object messageObject = null;
try {
messageObject = DispatchNode.getUncached().call(exception, "message");
} catch (Throwable e) {
} catch (RaiseException e) {
// Fall back to the internal message field
}
if (messageObject != null && RubyStringLibrary.getUncached().isRubyString(messageObject)) {
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/org/truffleruby/core/exception/RubyException.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@

import java.util.Set;

import com.oracle.truffle.api.TruffleStackTraceElement;
import com.oracle.truffle.api.interop.ExceptionType;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.library.ExportLibrary;
import com.oracle.truffle.api.library.ExportMessage;
import com.oracle.truffle.api.nodes.Node;
import org.truffleruby.RubyContext;
import org.truffleruby.RubyLanguage;
import org.truffleruby.core.VMPrimitiveNodes.VMRaiseExceptionNode;
import org.truffleruby.core.array.ArrayHelpers;
import org.truffleruby.core.array.RubyArray;
import org.truffleruby.core.klass.RubyClass;
import org.truffleruby.core.proc.RubyProc;
Expand All @@ -33,6 +37,8 @@
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.object.Shape;

import static org.truffleruby.language.RubyBaseNode.nil;

@ExportLibrary(InteropLibrary.class)
public class RubyException extends RubyDynamicObject implements ObjectGraphNode {

Expand Down Expand Up @@ -96,6 +102,49 @@ public RuntimeException throwException(
public ExceptionType getExceptionType() {
return ExceptionType.RUNTIME_ERROR;
}

@ExportMessage
public boolean hasExceptionCause() {
return this.cause != nil;
}

@ExportMessage
public Object getExceptionCause() throws UnsupportedMessageException {
if (!hasExceptionCause()) {
throw UnsupportedMessageException.create();
}
return this.cause;
}

@ExportMessage
public boolean hasExceptionMessage() {
return true;
}

@ExportMessage
public Object getExceptionMessage() {
return ExceptionOperations.messageToString(this);
}

@ExportMessage
public boolean hasExceptionStackTrace() {
return this.backtrace != null;
}

@TruffleBoundary
@ExportMessage
public Object getExceptionStackTrace() throws UnsupportedMessageException {
if (!hasExceptionStackTrace()) {
throw UnsupportedMessageException.create();
}

TruffleStackTraceElement[] stackTrace = this.backtrace.getStackTrace();
Object[] items = new Object[stackTrace.length];
for (int i = 0; i < items.length; i++) {
items[i] = stackTrace[i].getGuestObject();
}
return ArrayHelpers.createArray(RubyContext.get(null), RubyLanguage.get(null), items);
}
// endregion

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
import org.truffleruby.RubyContext;
import org.truffleruby.core.exception.ExceptionOperations;
import org.truffleruby.core.exception.RubyException;
import org.truffleruby.core.module.ModuleFields;
import org.truffleruby.language.backtrace.Backtrace;

import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;

/** A ControlFlowException holding a Ruby exception. */
@SuppressWarnings("serial")
@ExportLibrary(value = InteropLibrary.class, delegateTo = "exception")
Expand Down Expand Up @@ -55,11 +52,8 @@ public RubyException getException() {
}

@Override
@TruffleBoundary
public String getMessage() {
final ModuleFields exceptionClass = exception.getLogicalClass().fields;
final String message = ExceptionOperations.messageToString(exception);
return String.format("%s (%s)", message, exceptionClass.getName());
return ExceptionOperations.messageFieldToString(exception);
}

}
Loading

0 comments on commit 7cc6073

Please sign in to comment.