Graal Truffle tutorial part 15 – exceptions
This article is part of a tutorial on GraalVM's Truffle language implementation framework.
- Part 0 – what is Truffle
- Part 1 – setup, Nodes, CallTarget
- Part 2 – introduction to specializations
- Part 3 – specializations with Truffle DSL, TypeSystem
- Part 4 – parsing, and the TruffleLanguage class
- Part 5 – global variables
- Part 6 – static function calls
- Part 7 – function definitions
- Part 8 – conditionals, loops, control flow
- Part 9 – performance benchmarking
- Part 10 – arrays, read-only properties
- Part 11 – strings, static method calls
- Part 12 – classes 1: methods, new
- Part 13 – classes 2: fields, this, constructors
- Part 14 – classes 3: inheritance, super
- Part 15 – exceptions
Introduction
In the previous chapter of the tutorial on GraalVM Truffle, we finished the implementation of classes in EasyScript, our simplified subset of JavaScript.
But there is an important subset of classes that have additional capabilities in many languages: exceptions. These are special objects that are used for signaling error conditions that the given piece of code does not know how to handle.
Since exceptions are a popular feature present in many languages,
including JavaScript, we will show how to implement them in Truffle.
As part of that implementation, we will introduce a few new Truffle concepts,
like the SourceSection
class,
the TruffleStackTrace
class,
and the TruffleStackTraceElement
class.
Parsing
As usual, we start with the
ANTLR grammar changes.
There are two statements related to exceptions:
throw
, which raises an exception,
and try
-catch
, which handles exceptions.
The tricky part of try
-catch
is that that statement can also have a finally
part,
which is a block of code that executes regardless whether the
try
part resulted in an exception being thrown, or not.
The reason that is tricky to parse is that,
while finally
is optional after try
-catch
,
there is also another form of try
where catch
is missing –
but in that case, finally
is required.
The way we handle that is by having two grammar rules –
one where catch
is required, but finally
is optional,
and another where catch
is not allowed, but finally
is required:
stmt : 'throw' expr1 ';'? #ThrowStmt
| 'try' t=stmt_block 'catch' '(' ID ')' c=stmt_block ('finally' f=stmt_block)? ';'? #TryCatchStmt
| 'try' t=stmt_block 'finally' f=stmt_block ';'? #TryFinallyStmt
...
stmt_block : '{' stmt* '}' ;
...
The throw
statement
The simplest part of exception handling is raising an exception.
In Truffle, the exception we raise must extend AbstractTruffleException
,
otherwise it will be considered a bug in your interpreter
(for example, if you forgot to check a value for potentially being null
,
and that resulting in a NullPointerException
).
Fortunately, we already have an exception in our implementation that extends
AbstractTruffleException
– EasyScriptException
.
Up to this part, we’ve only been using it for built-in errors,
like having a const
variable without an initializer,
or reading a property of undefined
–
however, we can repurpose it to handle user-defined exceptions as well.
In JavaScript, raising exceptions is accomplished with the throw
statement.
Unlike in many languages, JavaScript allows raising any value,
not only a subclass of a specific class, like Throwable
in Java.
In order to handle that capability, we add a value
field to EasyScriptException
that we will use later in the catch
statement:
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.nodes.Node;
public final class EasyScriptException extends AbstractTruffleException {
public final Object value;
public EasyScriptException(Object value) {
this.value = value;
}
// these two constructors are for the built-in errors,
// and were defined in previous parts
public EasyScriptException(String message) {
this(null, message);
}
public EasyScriptException(Node location, String message) {
super(message, location);
this.value = null;
}
}
The implementation of the throw
statement itself is very simple:
we evaluate the expression for the value being thrown,
and then use Java’s throw
statement to raise an instance of EasyScriptException
:
import com.oracle.truffle.api.frame.VirtualFrame;
public final class ThrowStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptExprNode exceptionExpr;
public ThrowStmtNode(EasyScriptExprNode exceptionExpr) {
this.exceptionExpr = exceptionExpr;
}
@Override
public Object executeStatement(VirtualFrame frame) {
Object value = this.exceptionExpr.executeGeneric(frame);
throw new EasyScriptException(value);
}
}
Filling polyglot stack traces
With that in place, we can try executing some EasyScript code with a throw
statement.
We will use the Source
class
and the Context.eval(Source)
method,
instead of the
Context.eval(String, String)
method
which we used in previous parts of the series,
as we want to make sure the line numbers from the source code are preserved in the stack trace:
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import java.io.File;
Source source = Source
.newBuilder("ezs", new File("exceptions-nested.js"))
.build();
Context context = Context.create();
context.eval(source);
Where exceptions-nested.js
is:
function main() {
f1();
}
function f1() {
let x = f2();
return x;
}
function f2() {
return f3();
}
function f3() {
throw 'Exception in f3()';
}
main();
The start of the stack trace we see when this code executes looks as follows:
org.graalvm.polyglot.PolyglotException
at <ezs> null(Unknown)
at <ezs> null(Unknown)
at <ezs> null(Unknown)
at <ezs> null(Unknown)
at <ezs> null(Unknown)
at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:399)
...
Clearly, this is missing some crucial information, so we need to fix this stack trace.
The first thing we can do is override the
getName()
method of RootNode
.
Truffle will call this method whenever an uncaught exception passes through the
execute()
method of RootNode
:
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.RootNode;
public final class StmtBlockRootNode extends RootNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptStmtNode blockStmt;
private final String name;
public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage,
FrameDescriptor frameDescriptor, BlockStmtNode blockStmt, String name) {
this(truffleLanguage, frameDescriptor, (EasyScriptStmtNode) blockStmt, name);
}
public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage,
FrameDescriptor frameDescriptor, UserFuncBodyStmtNode blockStmt, String name) {
this(truffleLanguage, frameDescriptor, (EasyScriptStmtNode) blockStmt, name);
}
private StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage,
FrameDescriptor frameDescriptor, EasyScriptStmtNode blockStmt, String name) {
super(truffleLanguage, frameDescriptor);
this.blockStmt = blockStmt;
this.name = name;
}
@Override
public Object execute(VirtualFrame frame) {
return this.blockStmt.executeStatement(frame);
}
@Override
public String getName() {
return this.name;
}
}
For user-defined functions or methods, we will pass their name as the value of the name
parameter,
and for the top-level code inside a file,
we use the :program
string, same as the GraalVM JavaScript implementation.
This makes the stack trace look as follows:
org.graalvm.polyglot.PolyglotException
at <ezs> f3(Unknown)
at <ezs> f2(Unknown)
at <ezs> f1(Unknown)
at <ezs> main(Unknown)
at <ezs> :program(Unknown)
at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:399)
...
This is better, but we are still missing the source information in parentheses.
You can specify that by overriding the
getSourceSection()
method of Node
to point to the place in the text of the source code that this Node corresponds to.
Thankfully, the parsing technology we use,
ANTLR, preserves this information in the
start
and stop
fields of the
ParserRuleContext
class,
which all classes generated from the grammar extend.
So, in our parser, we can create a helper method that creates a
SourceSection
instance from a given parse element
(note that we need to add 1
to the ending position,
as ANTLR uses 0-based indexing for it,
while Truffle needs 1-based indexes):
import com.oracle.truffle.api.source.SourceSection;
import org.antlr.v4.runtime.ParserRuleContext;
public final class EasyScriptTruffleParser {
// ...
private SourceSection createSourceSection(ParserRuleContext parseElement) {
return this.source.createSection(
parseElement.start.getLine(), parseElement.start.getCharPositionInLine() + 1,
parseElement.stop.getLine(), parseElement.stop.getCharPositionInLine() + 1);
}
}
Since we now need to hold on to the original Source
to implement this createSourceSection()
helper method,
we change our parser API slightly to pass it to our instance when constructing it:
import com.oracle.truffle.api.source.Source;
public final class EasyScriptTruffleParser {
private final Source source;
private EasyScriptTruffleParser(Source source, ShapesAndPrototypes shapesAndPrototypes) {
this.source = source;
// ...
}
// ...
}
With the createSourceSection()
helper method in place, we can create a SourceSection
when parsing a given language construct,
and pass it to the Truffle Node that will be created for it.
In theory, every Node can override getSourceSection()
; in our implementation,
we’ll only do it for the few statement Nodes that can result in exceptions being thrown
(ExprStmtNode
, ReturnStmtNode
, and ThrowStmtNode
),
to save on some repeated code
(of course, feel free to expand that list in your own language’s implementation).
Here’s an example for ThrowStmtNode
from above:
public final class EasyScriptTruffleParser {
// ...
private ThrowStmtNode parseThrowStmt(EasyScriptParser.ThrowStmtContext throwStmt) {
return new ThrowStmtNode(this.parseExpr1(throwStmt.expr1()),
this.createSourceSection(throwStmt));
}
}
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
public final class ThrowStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptExprNode exceptionExpr;
private final SourceSection sourceSection;
public ThrowStmtNode(EasyScriptExprNode exceptionExpr, SourceSection sourceSection) {
this.exceptionExpr = exceptionExpr;
this.sourceSection = sourceSection;
}
@Override
public Object executeStatement(VirtualFrame frame) {
Object value = this.exceptionExpr.executeGeneric(frame);
throw new EasyScriptException(value);
}
@Override
public SourceSection getSourceSection() {
return this.sourceSection;
}
}
This makes the stack trace look like:
org.graalvm.polyglot.PolyglotException
at <ezs> f3(Unknown)
at <ezs> f2(exceptions-nested.js:9:100-111)
at <ezs> f1(exceptions-nested.js:5:50-62)
at <ezs> main(exceptions-nested.js:2:22-26)
at <ezs> :program(exceptions-nested.js:14:164-170)
at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:399)
...
In order to fix that last element,
we need to pass the throw
Node into the AbstractTruffleException
that we raise in ThrowStmtNode
, alongside the value, so that the message is filled correctly:
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
public final class ThrowStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptExprNode exceptionExpr;
private final SourceSection sourceSection;
public ThrowStmtNode(EasyScriptExprNode exceptionExpr, SourceSection sourceSection) {
this.exceptionExpr = exceptionExpr;
this.sourceSection = sourceSection;
}
@Override
public Object executeStatement(VirtualFrame frame) {
Object value = this.exceptionExpr.executeGeneric(frame);
throw new EasyScriptException(value, this);
}
@Override
public SourceSection getSourceSection() {
return this.sourceSection;
}
}
Since AbstractTruffleException
expects a String for the message,
we use the EasyScriptTruffleStrings.toString()
method seen in the
previous parts
to convert the provided value to a String:
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.nodes.Node;
public final class EasyScriptException extends AbstractTruffleException {
public final Object value;
public EasyScriptException(Object value, Node node) {
super(EasyScriptTruffleStrings.toString(value), node);
this.value = value;
}
// ...
}
This finally fills the entire stack trace:
Exception in f3()
at <ezs> f3(exceptions-nested.js:12:135-160)
at <ezs> f2(exceptions-nested.js:9:100-111)
at <ezs> f1(exceptions-nested.js:5:50-62)
at <ezs> main(exceptions-nested.js:2:22-26)
at <ezs> :program(exceptions-nested.js:14:164-170)
at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:399)
...
The try
statement
But, raising exceptions is only half of the story –
the other half is handling them.
For that, we use the try
statement.
The try
statement has three parts: the (required) block for the try
,
the (optional) catch
block that is executed when an exception is caught,
and the (optional) finally
block that executes,
regardless whether an exception was thrown from the try
block,
or not.
The interesting part is the catch
statement,
since, if it executes, it needs to assign the thrown value that has been caught
to the local variable with the name included in the catch
statement.
The way we handle that is by creating a new local variable during parsing of the catch
statement:
public final class EasyScriptTruffleParser {
// ...
private TryStmtNode parseTryCatchStmt(EasyScriptParser.TryCatchStmtContext tryCatchStmt) {
// parse the 'try' statement block
BlockStmtNode tryBlockStmt = this.parseStmtBlock(tryCatchStmt.t);
BlockStmtNode finallyBlockStmt = tryCatchStmt.f == null
? null
: this.parseStmtBlock(tryCatchStmt.f);
// add the 'catch' identifier as a local variable
String exceptionVar = tryCatchStmt.ID().getText();
var frameSlotId = new LocalVariableFrameSlotId(exceptionVar, ++this.localVariablesCounter);
int frameSlot = this.frameDescriptor.addSlot(FrameSlotKind.Object, frameSlotId, DeclarationKind.LET);
if (this.localScopes.peek().putIfAbsent(exceptionVar, new LocalVariable(frameSlot, DeclarationKind.LET)) != null) {
throw new EasyScriptException("Identifier '" + exceptionVar + "' has already been declared");
}
// parse the 'catch' statement block
BlockStmtNode catchBlockStmt = this.parseStmtBlock(tryCatchStmt.c);
return new TryStmtNode(tryBlockStmt, frameSlot, catchBlockStmt, finallyBlockStmt);
}
}
We pass the integer slot that was assigned to the local variable from the catch
statement into TryStmtNode
,
and then we use it when we catch the exception.
We assign that local variable the contents of the value
field of the caught EasyScriptException
that we populated in the throw
statement.
Since Java has the same type of exception handling as JavaScript,
the code looks very natural;
we just have to check whether we have the try
-catch
form
(with the optional finally
), or the try
-finally
form
(without catch
):
import com.oracle.truffle.api.frame.VirtualFrame;
public final class TryStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private BlockStmtNode tryStatements;
private final Integer exceptionVarFrameSlot;
@SuppressWarnings("FieldMayBeFinal")
@Child
private BlockStmtNode catchStatements;
@SuppressWarnings("FieldMayBeFinal")
@Child
private BlockStmtNode finallyStatements;
public TryStmtNode(BlockStmtNode tryStatements, BlockStmtNode finallyStatements) {
this(tryStatements, null, null, finallyStatements);
}
public TryStmtNode(BlockStmtNode tryStatements, Integer exceptionVarFrameSlot,
BlockStmtNode catchStatements, BlockStmtNode finallyStatements) {
this.tryStatements = tryStatements;
this.exceptionVarFrameSlot = exceptionVarFrameSlot;
this.catchStatements = catchStatements;
this.finallyStatements = finallyStatements;
}
@Override
public Object executeStatement(VirtualFrame frame) {
if (this.exceptionVarFrameSlot == null) {
try {
return this.tryStatements.executeStatement(frame);
} finally {
// we now that the 'finally' block is not null if 'catch' block is null
this.finallyStatements.executeStatement(frame);
}
} else {
try {
return this.tryStatements.executeStatement(frame);
} catch (EasyScriptException e) {
frame.setObject(this.exceptionVarFrameSlot, e.value);
return this.catchStatements.executeStatement(frame);
} finally {
if (this.finallyStatements != null) {
this.finallyStatements.executeStatement(frame);
}
}
}
}
}
Since the condition determining whether the given try
statement has a catch
block or not is compilation-final,
the entire if
will actually be eliminated when this Node gets JIT-compiled,
and only the relevant branch will be left in native code.
Built-in error types
While JavaScript allows throwing any value,
it also comes with an error hierarchy:
the Error
class,
and its subclasses.
We will implement only the TypeError
subclass
as a representative example.
In order for the parser and runtime to know about these new classes,
we create a new class, ErrorPrototypes
:
public final class ErrorPrototypes {
public final ClassPrototypeObject errorPrototype, typeErrorPrototype;
public final Map<String, ClassPrototypeObject> allBuiltInErrorClasses;
public ErrorPrototypes(
ClassPrototypeObject errorPrototype,
ClassPrototypeObject typeErrorPrototype) {
this.errorPrototype = errorPrototype;
this.typeErrorPrototype = typeErrorPrototype;
this.allBuiltInErrorClasses = Map.of(
"Error", errorPrototype,
"TypeError", typeErrorPrototype
);
}
}
As we need to add all built-in classes as global variables with the same name as the class,
we create a Map
of their names as keys, and their prototypes as values,
which we surface through the slightly modified ShapesAndPrototypes
class from
part 13:
import com.oracle.truffle.api.object.Shape;
public final class ShapesAndPrototypes {
public final Shape rootShape, arrayShape;
public final ObjectPrototype objectPrototype;
public final ClassPrototypeObject functionPrototype, arrayPrototype, stringPrototype;
public final ErrorPrototypes errorPrototypes;
public final Map<String, ClassPrototypeObject> allBuiltInClasses;
public ShapesAndPrototypes(Shape rootShape, Shape arrayShape,
ObjectPrototype objectPrototype, ClassPrototypeObject functionPrototype,
ClassPrototypeObject arrayPrototype, ClassPrototypeObject stringPrototype,
ErrorPrototypes errorPrototypes) {
this.rootShape = rootShape;
this.arrayShape = arrayShape;
this.objectPrototype = objectPrototype;
this.functionPrototype = functionPrototype;
this.arrayPrototype = arrayPrototype;
this.stringPrototype = stringPrototype;
this.errorPrototypes = errorPrototypes;
Map<String, ClassPrototypeObject> allBuiltInClasses = new HashMap<>();
allBuiltInClasses.put("Object", objectPrototype);
allBuiltInClasses.putAll(errorPrototypes.allBuiltInErrorClasses);
this.allBuiltInClasses = Collections.unmodifiableMap(allBuiltInClasses);
}
}
We change the parser to accept an instance of ShapesAndPrototypes
instead of the root Shape
and Object
prototype separately, like in the previous parts,
and use the allBuiltInClasses
field to register all built-in classes,
so that they can be extended by user-defined classes:
import com.oracle.truffle.api.source.Source;
public final class EasyScriptTruffleParser {
private EasyScriptTruffleParser(Source source, ShapesAndPrototypes shapesAndPrototypes) throws IOException {
// we add a global scope, in which we store the class prototypes
Map<String, FrameMember> classPrototypes = new HashMap<>();
for (Map.Entry<String, ClassPrototypeObject> builtInClassEntry :
shapesAndPrototypes.allBuiltInClasses.entrySet()) {
classPrototypes.put(builtInClassEntry.getKey(),
new ClassPrototypeMember(builtInClassEntry.getValue()));
}
// ...
}
// ...
}
We create the prototypes for these classes in the Truffle Language implementation for this part.
All Error
types have a constructor that takes a string message
as the first argument, which gets assigned to a message
property of the object,
and also a name
property, equal to the name of the class.
In JavaScript code, it looks something like this:
class Error {
constructor(message) {
this.message = message;
this.name = 'Error';
}
}
class TypeError extends Error {
constructor(message) {
super(message);
this.name = 'TypeError';
}
}
We need to initialize the error prototypes with such a constructor. We basically re-create the above JavaScript code by creating the appropriate Truffle Nodes “by hand”, since this happens before we can parse any JavaScript code:
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.TruffleLanguage;
@TruffleLanguage.Registration(id = "ezs", name = "EasyScript")
public final class EasyScriptTruffleLanguage extends
TruffleLanguage<EasyScriptLanguageContext> {
private DynamicObject createGlobalScopeObject(DynamicObjectLibrary objectLibrary) {
// add a constructor to all Error types
for (Map.Entry<String, ClassPrototypeObject> entry :
this.shapesAndPrototypes.errorPrototypes.allBuiltInErrorClasses.entrySet()) {
objectLibrary.putConstant(
entry.getValue(),
"constructor",
// error subtype constructor
new FunctionObject(
this.rootShape,
this.functionPrototype,
new StmtBlockRootNode(
this,
FrameDescriptor.newBuilder().build(),
new BlockStmtNode(List.of(
// this.message = args[1];
new ExprStmtNode(PropertyWriteExprNodeGen.create(
new ThisExprNode(),
new ReadFunctionArgExprNode(1),
"message"
), null),
// this.name = <name>;
new ExprStmtNode(PropertyWriteExprNodeGen.create(
new ThisExprNode(),
new StringLiteralExprNode(entry.getKey()),
"name"
), null)
)),
"constructor").getCallTarget(),
1),
0);
}
// ...
}
// ...
}
Reading properties of the thrown object
When an object is used in the throw
statement
(for example, one of those built-in errors we saw above),
the exception being thrown should construct its message from the name
and message
properties of the object.
However, this shouldn’t apply to throwing non-object values,
so we need to switch our implementation of the throw
statement to use specializations:
import com.oracle.truffle.api.dsl.Executed;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.source.SourceSection;
public abstract class ThrowStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
@Executed
protected EasyScriptExprNode exceptionExpr;
private final SourceSection sourceSection;
protected ThrowStmtNode(EasyScriptExprNode exceptionExpr, SourceSection sourceSection) {
this.exceptionExpr = exceptionExpr;
this.sourceSection = sourceSection;
}
@Specialization(limit = "2")
protected Object throwJavaScriptObject(JavaScriptObject value,
@CachedLibrary("value") DynamicObjectLibrary nameObjectLibrary,
@CachedLibrary("value") DynamicObjectLibrary messageObjectLibrary) {
Object name = nameObjectLibrary.getOrDefault(value, "name", null);
Object message = messageObjectLibrary.getOrDefault(value, "message", null);
throw new EasyScriptException(name, message, value, this);
}
@Specialization
protected Object throwNonJavaScriptObject(Object value) {
throw new EasyScriptException(value, this);
}
@Override
public SourceSection getSourceSection() {
return this.sourceSection;
}
}
In accordance with Truffle recommendations,
we use two different cached DynamicObjectLibrary
instances:
one for the name
property, and another for the message
property.
We need a new EasyScriptException
constructor in order to handle this case:
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.nodes.Node;
public final class EasyScriptException extends AbstractTruffleException {
public final Object value;
// ...
public EasyScriptException(Object name, Object message, JavaScriptObject javaScriptObject, Node node) {
super(EasyScriptTruffleStrings.toString(name) + ": " + EasyScriptTruffleStrings.toString(message), node);
this.value = javaScriptObject;
}
}
Changing built-in errors
In JavaScript, many built-in error conditions,
like accessing a property of undefined
,
assigning a negative length to an array, etc.,
result in a specific instance of a subclass of Error
being thrown – for example,
accessing a property of undefined
raises TypeError
.
The tricky part of implementing this is correctly creating an instance of a subtype of Error
,
since, like we saw above, those classes have a specific constructor in EasyScript.
But, we don’t have an easy way to invoke that constructor from Java code inside a Node specialization!
So, we use a small trick: we create a subclass of JavaScriptObject
that basically re-implements that constructor in Java:
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.object.Shape;
public final class ErrorJavaScriptObject extends JavaScriptObject {
public final String name, message;
public ErrorJavaScriptObject(String name, String message,
DynamicObjectLibrary dynamicObjectLibrary,
Shape shape, ClassPrototypeObject prototype) {
super(shape, prototype);
this.name = name;
this.message = message;
dynamicObjectLibrary.put(this, "name", EasyScriptTruffleStrings.fromJavaString(name));
dynamicObjectLibrary.put(this, "message", EasyScriptTruffleStrings.fromJavaString(message));
}
}
And then we use this subclass in the implementation of the Node for reading properties,
in the specialization that handles attempting to read a property of undefined
:
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
public abstract class CommonReadPropertyNode extends EasyScriptNode {
// ...
@Specialization(guards = "interopLibrary.isNull(target)", limit = "2")
protected Object readPropertyOfUndefined(
Object target, Object property,
@CachedLibrary("target") InteropLibrary interopLibrary,
@CachedLibrary(limit = "2") DynamicObjectLibrary dynamicObjectLibrary,
@Cached("currentLanguageContext().shapesAndPrototypes") ShapesAndPrototypes shapesAndPrototypes) {
var typeError = new ErrorJavaScriptObject(
"TypeError",
"Cannot read properties of undefined (reading '" + property + "')",
dynamicObjectLibrary,
shapesAndPrototypes.rootShape,
shapesAndPrototypes.errorPrototypes.typeErrorPrototype);
throw new EasyScriptException(typeError, this);
}
// ...
}
In order to correctly populate the exception message,
we need to add one more constructor to EasyScriptException
:
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.nodes.Node;
public final class EasyScriptException extends AbstractTruffleException {
public final Object value;
public EasyScriptException(ErrorJavaScriptObject errorJavaScriptObject, Node node) {
super(errorJavaScriptObject.name + ": " + errorJavaScriptObject.message, node);
this.value = errorJavaScriptObject;
}
// ...
}
Filling guest stack traces
Previously, we saw how the Truffle runtime fills the stacktrace of an exception in the polyglot context.
However, it’s also often useful to access the stack trace of an exception inside the guest language itself
(for example, to log it when handling an exception, when we don’t plan on re-throwing it).
In order to do that in JavaScript, we can use the non-standard (but widely supported)
stack
property
that gets filled when an object is thrown.
Note: in JavaScript, that property is filled when an instance of Error
or one of its subclasses is created. However, that makes the Truffle code a little more complex,
so in EasyScript, we’ll do it when the object is thrown instead.
In order to get access to the guest stack trace,
we can use the
TruffleStackTrace.getStackTrace()
method,
passing it the JavaScriptException
we have created.
That gives us a List
of
TruffleStackTraceElement
instances
which we can render as a TruffleString
:
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringBuilder;
import com.oracle.truffle.api.TruffleStackTrace;
import com.oracle.truffle.api.TruffleStackTraceElement;
public abstract class ThrowStmtNode extends EasyScriptStmtNode {
// ...
@Specialization(limit = "2")
protected Object throwJavaScriptObject(
JavaScriptObject value,
@CachedLibrary("value") DynamicObjectLibrary nameObjectLibrary,
@CachedLibrary("value") DynamicObjectLibrary messageObjectLibrary,
@CachedLibrary("value") DynamicObjectLibrary stackObjectLibrary) {
Object name = nameObjectLibrary.getOrDefault(value, "name", null);
Object message = messageObjectLibrary.getOrDefault(value, "message", null);
var easyScriptException = new EasyScriptException(name, message, value, this);
stackObjectLibrary.put(value, "stack", this.formStackTrace(name, message, easyScriptException));
throw easyScriptException;
}
@TruffleBoundary
private TruffleString formStackTrace(Object name, Object message, EasyScriptException easyScriptException) {
TruffleStringBuilder sb = EasyScriptTruffleStrings.builder();
sb.appendJavaStringUTF16Uncached(String.valueOf(name));
if (message != Undefined.INSTANCE) {
sb.appendJavaStringUTF16Uncached(": ");
sb.appendJavaStringUTF16Uncached(String.valueOf(message));
}
List<TruffleStackTraceElement> truffleStackTraceEls = TruffleStackTrace.getStackTrace(easyScriptException);
for (TruffleStackTraceElement truffleStackTracEl : truffleStackTraceEls) {
sb.appendJavaStringUTF16Uncached("\n\tat ");
Node location = truffleStackTracEl.getLocation();
RootNode rootNode = location.getRootNode();
String funcName = rootNode.getName();
// we want to ignore the top-level program RootNode name in this stack trace
boolean isFunc = !":program".equals(funcName);
if (isFunc) {
sb.appendJavaStringUTF16Uncached(funcName);
sb.appendJavaStringUTF16Uncached(" (");
}
SourceSection sourceSection = location.getEncapsulatingSourceSection();
sb.appendJavaStringUTF16Uncached(sourceSection.getSource().getName());
sb.appendJavaStringUTF16Uncached(":");
sb.appendJavaStringUTF16Uncached(String.valueOf(sourceSection.getStartLine()));
sb.appendJavaStringUTF16Uncached(":");
sb.appendJavaStringUTF16Uncached(String.valueOf(sourceSection.getStartColumn()));
if (isFunc) {
sb.appendJavaStringUTF16Uncached(")");
}
}
return sb.toStringUncached();
}
}
Since our stack trace inside EasyScript needs to be a TruffleString
,
we use the TruffleStringBuilder
class
instead of a regular StringBuilder
.
As constructing an instance of TruffleStringBuilder
requires providing an encoding for it,
we encapsulate creating one inside a new method in our utility class,
EasyScriptTruffleStrings
, introduced in part 11:
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringBuilder;
public final class EasyScriptTruffleStrings {
private static final TruffleString.Encoding JAVA_SCRIPT_STRING_ENCODING = TruffleString.Encoding.UTF_16;
// ...
public static TruffleStringBuilder builder() {
return TruffleStringBuilder.create(JAVA_SCRIPT_STRING_ENCODING);
}
}
Following the GraalVM JavaScript implementation,
we don’t use the :program
name for the top-level script in the stack trace inside EasyScript,
but simply omit the function name in that case.
Since we only overrode the getSourceSection()
method
in a few Node subclasses,
we instead use the Node.getEncapsulatingSourceSection()
method
which traverses up to the parent Node until it finds one with a non-null
SourceSection
.
This results in the stack traces inside EasyScript looking very similar to the polyglot ones, for example:
Error: Exception in f3()
at f3 (exceptions-nested.js:12:5)
at f2 (exceptions-nested.js:9:5)
at f1 (exceptions-nested.js:5:5)
at main (exceptions-nested.js:2:5)
at exceptions-nested.js:14:7
Benchmark
While exceptions are inherently slow
(operations like gathering the stack trace are slow,
the method for formatting the stack trace into a string requires a @TruffleBoundary
annotation, etc.),
we can still write a simple benchmark using them –
count until receiving an exception:
class Countdown {
constructor(start) {
this.count = start;
}
decrement() {
if (this.count <= 0) {
throw new Error('countdown has completed');
}
this.count = this.count - 1;
}
}
function countdown(n) {
const countdown = new Countdown(n);
let ret = 0;
for (;;) {
try {
countdown.decrement();
ret = ret + 1;
} catch (e) {
break;
}
}
return ret;
}
Running countdown
with n
equal to 1 million in both JavaScript and EasyScript results in the following numbers on my laptop:
Benchmark Mode Cnt Score Error Units
CountdownBenchmark.count_down_with_exception_ezs avgt 5 921.898 ± 32.561 us/op
CountdownBenchmark.count_down_with_exception_js avgt 5 928.523 ± 8.294 us/op
As we can see, both EasyScript and the GraalVM JavaScript implementation have basically identical performance, which means we at least didn’t introduce some obvious inefficiency to EasyScript.
Summary
So, this is how exceptions work in Truffle.
As usual, all the code from the article is available on GitHub.
In the next part of the series, we will talk about adding support for debugging your language.
This article is part of a tutorial on GraalVM's Truffle language implementation framework.
- Part 0 – what is Truffle
- Part 1 – setup, Nodes, CallTarget
- Part 2 – introduction to specializations
- Part 3 – specializations with Truffle DSL, TypeSystem
- Part 4 – parsing, and the TruffleLanguage class
- Part 5 – global variables
- Part 6 – static function calls
- Part 7 – function definitions
- Part 8 – conditionals, loops, control flow
- Part 9 – performance benchmarking
- Part 10 – arrays, read-only properties
- Part 11 – strings, static method calls
- Part 12 – classes 1: methods, new
- Part 13 – classes 2: fields, this, constructors
- Part 14 – classes 3: inheritance, super
- Part 15 – exceptions