Graal Truffle tutorial part 7 – function definitions
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
In the previous article of the series, EasyScript started supporting functions by allowing calling a small set of built-in ones. The natural next step in our implementation is allowing the users of the language to define their own functions.
Function definitions look as follows in JavaScript:
function add(a, b) {
let sum = a + b;
return sum;
}
We have the “function” keyword, followed by the name of the function, and then its arguments, in parentheses. The body of the function is a block of statements, between brackets; the function can include their own local variables, which live only for the duration of the function call.
Challenges
Now, allowing function definitions introduces a lot of complexity into our implementation.
In the language version from the previous article,
when we saw the usage of a variable like a
(regardless whether it was in a reference, or in assignment),
we were certain that it meant a global variable called a
.
However, with function definitions,
now seeing a variable like a
might mean three different things:
- A reference to a global variable
a
. - A reference to an argument of the function called
a
. - A reference to a local variable of the function with the name
a
.
The problem with these different types of references is that they are implemented differently in Truffle interpreters.
Global variables are stored in a separate GlobalScopeObject
,
while function arguments are kept in the Frame
object,
in the arguments
array.
Local variables are also stored in the Frame
object,
as we’ll see below, albeit in a different place than the arguments
array.
Naive solution
So, what would be the simplest solution to this problem?
Well, since we know all 3 places a given reference can be stored in,
we can simply search through all of them at runtime.
We can start with trying to find a local variable with the name a
;
if there is no local variable with that name,
we can check the function arguments,
and if there is no argument with that name,
we know that the variable is global
(or doesn’t exist at all).
And while that would certainly work, the problem is that it would be an inefficient solution. Every reference to a global variable from inside a function definition, for example, would always have to go through two unsuccessful reads at runtime before it was eventually found.
Optimal solution – static analysis
The key insight here that allows us to eliminate this overhead is that we can decide what each reference to a variable is just by analyzing the structure of the program – we don’t have to wait until runtime to do it.
For example, for this code:
const two = 2;
function addTwo(a) {
return two + a;
}
Just by looking at that program,
we can say with certainty that the reference to two
inside addTwo()
is a reference to a global variable,
while the reference to a
is a function argument.
There is no reason to check whether two
is a local variable,
or a function argument, at runtime.
This sort of examination of the program is called static analysis in the domain of implementing programming languages. While it’s most important for statically-typed languages, as this is where type checking happens, it’s also important for dynamically-typed languages, as we can see from the above example.
Usually, this phase is implemented separately in the compiler, but, to keep our interpreter simple, we will do it in the parsing step.
Frame descriptors and slots
We mentioned above that local variables are stored in the VirtualFrame
object,
but not in the same place as the function’s arguments.
If not there, then where?
They are stored in something called indexed slots;
basically a map
inside the VirtualFrame
.
The keys of that map historically were instances of a class called FrameSlot
.
However, that class has been removed in GraalVM version 22
,
and now the map is keyed by integers,
the same way function arguments are.
These integer keys are related to another important class, FrameDescriptor
.
The frame descriptor can be thought of as containing the static analysis information about a frame. For example, in the following JavaScript function:
function hypotenuse(a, b) {
var aa = a * a;
var bb = b * b;
return Math.sqrt(aa + bb);
}
While we can’t statically know what the values of the local variables are,
since they depend on the arguments passed to the function at runtime,
we do know at compile time that every invocation of hypotenuse()
will need room in its frame for two local variables, aa
and bb
.
Since JavaScript is a dynamically-typed language,
we don’t know in advance what types those slots should have –
depending on the arguments hypotenuse()
is called with,
they could be either integers, or double
s.
We will use specializations to allow Graal and Truffle to speculate on their types in order to squeeze out the maximum performance out of this code,
like we do for expressions.
To create these indexed slots in the frame,
we will use a Builder class
for FrameDescriptor
s, the
FrameDescriptor.Builder
class.
You create instances of it by calling the
newBuilder()
static factory method
of FrameDescriptor
.
You create slots in the frame descriptor by calling the
addSlot()
method of FrameDescriptor.Builder
,
and you get back the integer number reserved for that slot.
The important thing to know is that while retrieving and storing values inside the VirtualFrame
is a fast operation that gets JIT-compiled into efficient machine code,
actually creating and finding the slots in the descriptor builder is slow code that never gets JIT compiled.
For those reasons, it’s important to use the descriptor builder before execution starts,
during static analysis, but only use the descriptor itself at runtime.
Once the frame slots have been created in the FrameDescriptor.Builder
instance,
we call its
build()
method,
which returns a FrameDescriptor
.
That FrameDescriptor
instance is then passed to the RootNode
Truffle class.
When the CallTarget
that wraps that RootNode
is invoked,
it will create a VirtualFrame
instance with the appropriate number of slots
(and of the correct types, if we’ve provided that information to the slots),
using the information it got from the root node’s FrameDescriptor
instance.
Simplifications
To make this already long article at least somewhat manageable in size, we’ll make three simplifications in this part, which we will eliminate later in the series:
- We won’t implement a
return
statement yet – the function will return the result of evaluating the last statement of its body. Since this is the same behavior as for the entire program, this will allow us to re-use one class,BlockStmtNode
(see below), to implement both. We will add thereturn
statement when we handle control flow in the next article of the series. We will not allow nested functions (that is, functions defined inside another function). Their presence complicates the implementation quite a bit; for an example, take this code:
function makeAdder(add) { function adder(arg) { return arg + add; } return adder; } const add3 = makeAdder(3); console.log(add3(2));
This program will print out
5
. The difficulty of implementing it is that the3
argument passed tomakeAdder()
survives after the invocation ofmakeAdder()
finishes – it gets “captured” in theadder()
function returned bymakeAdder()
. This sort of function is called a closure, and we will devote an entire article to implementing them later in the series.- We will not support the
“magical”
arguments
variable in function definitions, as we don’t support arrays in EasyScript yet.
Grammar
Our language’s grammar will only need a small change: adding a new type of statement, the function declaration statement:
stmt : kind=('var' | 'let' | 'const') binding (',' binding)* ';'? #VarDeclStmt
| expr1 ';'? #ExprStmt
| 'function' name=ID '(' args=func_args ')' '{' stmt* '}' ';'? #FuncDeclStmt // new
;
func_args : (ID (',' ID)* )? ; // new
binding : ID ('=' expr1)? ;
Parsing
The entrypoint to our parser will still be a static factory method. However, the parser itself will now need to be stateful, because we need to track what local variables and arguments we’ve seen so far in a function definition (we don’t have to track global variables – we’ll just assume anything that is not local is global):
import com.oracle.truffle.api.frame.FrameDescriptor;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
public final class EasyScriptTruffleParser {
public static List<EasyScriptStmtNode> parse(Reader program) throws IOException {
var lexer = new EasyScriptLexer(CharStreams.fromReader(program));
// remove the default console error listener
lexer.removeErrorListeners();
var parser = new EasyScriptParser(new CommonTokenStream(lexer));
// remove the default console error listener
parser.removeErrorListeners();
// throw an exception when a parsing error is encountered
parser.setErrorHandler(new BailErrorStrategy());
return new EasyScriptTruffleParser().parseStmtsList(parser.start().stmt());
}
private static abstract class FrameMember {}
private static final class FunctionArgument extends FrameMember {
public final int argumentIndex;
FunctionArgument(int argumentIndex) {
this.argumentIndex = argumentIndex;
}
}
private static final class LocalVariable extends FrameMember {
public final int variableIndex;
public final DeclarationKind declarationKind;
LocalVariable(int variableIndex, DeclarationKind declarationKind) {
this.variableIndex = variableIndex;
this.declarationKind = declarationKind;
}
}
private final Map<String, FrameMember> functionLocals;
private FrameDescriptor.Builder frameDescriptor;
private EasyScriptTruffleParser() {
this.functionLocals = new HashMap<>();
}
// ...
}
We store the variables local to a function in the functionLocals
field.
Since both function arguments and local variables are indexed with integer numbers,
we introduce a private class, FrameMember
,
with two subclasses, FunctionArgument
and LocalVariable
,
that allow us to distinguish between the two
(for local variables, we also save the type of the declaration,
to detect assignments to const
variables).
We also save FrameDescriptor.Builder
in a field,
which will be used to create slots in the function’s frame for storing the local variables.
Parsing a block of statements
In JavaScript, like in many other languages, it’s legal to call a function before it’s defined. In order to support that in EasyScript, we need to do two loops through the list of statements on the top level. In the first loop, we only handle function declarations; in the second one, we handle the remaining statement types:
import com.oracle.truffle.api.frame.FrameSlotKind;
public final class EasyScriptTruffleParser {
// ...
private List<EasyScriptStmtNode> parseStmtsList(List<EasyScriptParser.StmtContext> stmts) {
var funcDecls = new ArrayList<FuncDeclStmtNode>();
for (EasyScriptParser.StmtContext stmt : stmts) {
if (stmt instanceof EasyScriptParser.FuncDeclStmtContext) {
funcDecls.add(this.parseFuncDeclStmt((EasyScriptParser.FuncDeclStmtContext) stmt));
}
}
var nonFuncDeclStmts = new ArrayList<EasyScriptStmtNode>();
for (EasyScriptParser.StmtContext stmt : stmts) {
if (stmt instanceof EasyScriptParser.ExprStmtContext) {
nonFuncDeclStmts.add(this.parseExprStmt((EasyScriptParser.ExprStmtContext) stmt));
} else if (stmt instanceof EasyScriptParser.VarDeclStmtContext) {
EasyScriptParser.VarDeclStmtContext varDeclStmt = (EasyScriptParser.VarDeclStmtContext) stmt;
DeclarationKind declarationKind = DeclarationKind.fromToken(varDeclStmt.kind.getText());
List<EasyScriptParser.BindingContext> varDeclBindings = varDeclStmt.binding();
for (EasyScriptParser.BindingContext varBinding : varDeclBindings) {
String variableId = varBinding.ID().getText();
var bindingExpr = varBinding.expr1();
EasyScriptExprNode initializerExpr;
if (bindingExpr == null) {
if (declarationKind == DeclarationKind.CONST) {
throw new EasyScriptException("Missing initializer in const declaration '" + variableId + "'");
}
// if a 'let' or 'var' declaration is missing an initializer,
// it means it will be initialized with 'undefined'
initializerExpr = new UndefinedLiteralExprNode();
} else {
initializerExpr = this.parseExpr1(bindingExpr);
}
if (this.frameDescriptor == null) {
// this is a global variable
nonFuncDeclStmts.add(GlobalVarDeclStmtNodeGen.create(initializerExpr, variableId, declarationKind));
} else {
// this is a function-local variable,
// which we turn into an assignment expression
int frameSlot = this.frameDescriptor.addSlot(FrameSlotKind.Illegal, variableId, declarationKind);
if (this.functionLocals.putIfAbsent(variableId, new LocalVariable(frameSlot, declarationKind)) != null) {
throw new EasyScriptException("Identifier '" + variableId + "' has already been declared");
}
LocalVarAssignmentExprNode assignmentExpr = LocalVarAssignmentExprNodeGen.create(initializerExpr, frameSlot);
nonFuncDeclStmts.add(new ExprStmtNode(assignmentExpr, /* discardExpressionValue */ true));
}
}
}
}
// return the function declarations first, and then the remaining statements
var result = new ArrayList<EasyScriptStmtNode>(funcDecls.size() + nonFuncDeclStmts.size());
result.addAll(funcDecls);
result.addAll(nonFuncDeclStmts);
return result;
}
}
Here, we can see the frame slot being created from the FrameDescriptor.Builder
when we encounter a variable declaration inside a function definition
(we have to handle errors with duplicate variables too,
including a local variable having the same name as a function argument,
which is not allowed in JavaScript).
We store the kind of the declaration (var
, const
or let
)
in the slot when creating it –
the slot has an info
field, of type Object
, that allows storing extra information within it –
because we need to make sure we disallow re-assigning local const
variables.
We also save the name of the local variable in the functionLocals
map,
which we will use below to determine whether a given reference is to a local,
or global, variable.
Since a frame slot is created at parse time, not at runtime,
we don’t need a local equivalent of GlobalVarDeclStmtNode
(which is unchanged from when it was first introduced in
part 5) –
so, we transform a local variable declaration into a local variable assignment:
import com.oracle.truffle.api.dsl.ImportStatic;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.NodeField;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameSlotKind;
import com.oracle.truffle.api.frame.VirtualFrame;
@NodeChild("initializerExpr")
@NodeField(name = "frameSlot", type = int.class)
@ImportStatic(FrameSlotKind.class)
public abstract class LocalVarAssignmentExprNode extends EasyScriptExprNode {
protected abstract int getFrameSlot();
@Specialization(guards = "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Illegal || " +
"frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Int")
protected int intAssignment(VirtualFrame frame, int value) {
var frameSlot = this.getFrameSlot();
frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Int);
frame.setInt(frameSlot, value);
return value;
}
@Specialization(replaces = "intAssignment",
guards = "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Illegal || " +
"frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Double")
protected double doubleAssignment(VirtualFrame frame, double value) {
var frameSlot = this.getFrameSlot();
frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Double);
frame.setDouble(frameSlot, value);
return value;
}
@Specialization(replaces = {"intAssignment", "doubleAssignment"})
protected Object objectAssignment(VirtualFrame frame, Object value) {
var frameSlot = this.getFrameSlot();
frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Object);
frame.setObject(frameSlot, value);
return value;
}
}
We use specializations if the local variables happen to have an int
or double
type.
We use the guards
attribute of @Specialization
to make sure they are only activated if the frame slot
has the correct type (or has not yet been initialized,
which is represented by the Illegal
frame slot kind –
that’s why each specialization sets the appropriate kind,
even though it might be redundant).
If an object is assigned to a given local variable at any time,
we stop further specializations, and work on the boxed object exclusively.
Note that in order to write that guards
expression,
we have to statically import the constants from the FrameSlotKind
Truffle enum.
We do that with the @ImportStatic
annotation.
There’s a small edge case here –
a declaration of a local variable should return undefined
when executed,
while an assignment should return the value assigned.
To distinguish a regular assignment from an assignment created from a declaration,
we add a second constructor parameter to ExprStmtNode
that allows discarding the value of the expression,
and returning undefined
always:
import com.oracle.truffle.api.frame.VirtualFrame;
public final class ExprStmtNode extends EasyScriptStmtNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptExprNode expr;
private final boolean discardExpressionValue;
public ExprStmtNode(EasyScriptExprNode expr) {
this(expr, false);
}
public ExprStmtNode(EasyScriptExprNode expr, boolean discardExpressionValue) {
this.expr = expr;
this.discardExpressionValue = discardExpressionValue;
}
@Override
public Object executeStatement(VirtualFrame frame) {
Object exprResult = this.expr.executeGeneric(frame);
return this.discardExpressionValue ? Undefined.INSTANCE : exprResult;
}
}
Parsing a function declaration
import com.oracle.truffle.api.frame.FrameDescriptor;
import org.antlr.v4.runtime.tree.TerminalNode;
public final class EasyScriptTruffleParser {
// ...
private FuncDeclStmtNode parseFuncDeclStmt(EasyScriptParser.FuncDeclStmtContext funcDeclStmt) {
if (this.frameDescriptor != null) {
throw new EasyScriptException("nested functions are not supported in EasyScript yet");
}
List<TerminalNode> funcArgs = funcDeclStmt.args.ID();
int argumentCount = funcArgs.size();
for (int i = 0; i < argumentCount; i++) {
this.functionLocals.put(funcArgs.get(i).getText(), new FunctionArgument(i));
}
this.frameDescriptor = FrameDescriptor.newBuilder();
List<EasyScriptStmtNode> funcStmts = this.parseStmtsList(funcDeclStmt.stmt());
FrameDescriptor frameDescriptor = this.frameDescriptor.build();
this.functionLocals.clear();
this.frameDescriptor = null;
return new FuncDeclStmtNode(funcDeclStmt.name.getText(),
frameDescriptor, new BlockStmtNode(funcStmts), argumentCount);
}
}
First, like I mentioned above, we check whether this is a function inside a function,
which we don’t allow in this article.
Then, we put all function arguments in the functionLocals
map,
mapping their names to the index in the frame they will be found under.
Note that, duplicate function arguments shadow each other –
this is consistent with how it works in JavaScript,
for example the following code:
function f(a, a) {
return a;
}
f(1, 23);
Will return 23
, not 1
(although note that
JavaScript strict mode
turns duplicate arguments into a syntax error instead).
Then, we create a new FrameDescriptor.Builder
and save it in the frameDescriptor
field.
That means, when we call parseStmtsList()
,
we will now parse variable declarations as local variables,
instead of global ones.
Finally, we save the FrameDescriptor
created from the frameDescriptor
field in a local variable,
reset our fields after parsing the body of the function
(by setting frameDescriptor
to null
,
and clearing the functionLocals
map),
and return a Node that implements a function declaration,
passing it the list of statements that comprise the function’s body,
wrapped as a single EasyScriptStatement
:
import com.oracle.truffle.api.frame.VirtualFrame;
public final class BlockStmtNode extends EasyScriptStmtNode {
@Children
private final EasyScriptStmtNode[] stmts;
public BlockStmtNode(List<EasyScriptStmtNode> stmts) {
this.stmts = stmts.toArray(new EasyScriptStmtNode[]{});
}
@Override
@ExplodeLoop
public Object executeStatement(VirtualFrame frame) {
Object ret = Undefined.INSTANCE;
for (EasyScriptStmtNode stmt : this.stmts) {
ret = stmt.executeStatement(frame);
}
return ret;
}
}
FuncDeclStmtNode
looks as follows:
import com.oracle.truffle.api.frame.VirtualFrame;
public final class FuncDeclStmtNode extends EasyScriptStmtNode {
private final String funcName;
private final FrameDescriptor frameDescriptor;
private final int argumentCount;
@SuppressWarnings("FieldMayBeFinal")
@Child
private BlockStmtNode funcBody;
public FuncDeclStmtNode(String funcName, FrameDescriptor frameDescriptor, BlockStmtNode funcBody, int argumentCount) {
this.funcName = funcName;
this.frameDescriptor = frameDescriptor;
this.funcBody = funcBody;
this.argumentCount = argumentCount;
}
@Override
public Object executeStatement(VirtualFrame frame) {
var truffleLanguage = this.currentTruffleLanguage();
var funcRootNode = new StmtBlockRootNode(truffleLanguage, this.frameDescriptor, this.funcBody);
var func = new FunctionObject(funcRootNode.getCallTarget(), this.argumentCount);
var context = this.currentLanguageContext();
context.globalScopeObject.newFunction(this.funcName, func);
return Undefined.INSTANCE;
}
}
We create a new FunctionObject
,
the same class that we used for the built-in functions from the
previous article.
Since we need a new a CallTarget
for FunctionObject
,
we have to create a new RootNode
.
StmtBlockRootNode
is very simple:
import com.oracle.truffle.api.frame.FrameDescriptor;
public final class StmtBlockRootNode extends RootNode {
@SuppressWarnings("FieldMayBeFinal")
@Child
private BlockStmtNode blockStmt;
public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage,
BlockStmtNode blockStmt) {
this(truffleLanguage, null, blockStmt);
}
public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage,
FrameDescriptor frameDescriptor, BlockStmtNode blockStmt) {
super(truffleLanguage, frameDescriptor);
this.blockStmt = blockStmt;
}
@Override
public Object execute(VirtualFrame frame) {
return this.blockStmt.executeStatement(frame);
}
}
It has two constructors, because we also use it as the root Node for the entire program
(in which case we don’t have a FrameDescriptor
).
Since we need a TruffleLanguage
instance to create the RootNode
from FuncDeclStmtNode
,
we employ a similar trick to what we did for retrieving the context from a Node in
part 5,
just now with the
LanguageReference
class
instead of with
ContextReference
.
The newFunction
method in GlobalScopeObject
is extremely simple:
@ExportLibrary(InteropLibrary.class)
public final class GlobalScopeObject implements TruffleObject {
private final Map<String, Object> variables = new HashMap<>();
// ...
public void newFunction(String name, FunctionObject func) {
this.variables.put(name, func);
}
}
We don’t have to do any checking for duplicates, because it’s actually legal in JavaScript to override functions; for example, this program:
function f() { return 1; }
function f() { return 2; }
console.log(f());
Will execute without any errors,
even in strict mode, and print out 2
.
Parsing an assignment expression
When we encounter an assignment, we have to check whether it’s a local (including function arguments), or global, variable:
public final class EasyScriptTruffleParser {
// ...
private EasyScriptExprNode parseAssignmentExpr(EasyScriptParser.AssignmentExpr1Context assignmentExpr) {
String variableId = assignmentExpr.ID().getText();
FrameMember frameMember = this.functionLocals.get(variableId);
EasyScriptExprNode initializerExpr = this.parseExpr1(assignmentExpr.expr1());
if (frameMember == null) {
return GlobalVarAssignmentExprNodeGen.create(initializerExpr, variableId);
} else {
if (frameMember instanceof FunctionArgument) {
return new WriteFunctionArgExprNode(initializerExpr, ((FunctionArgument) frameMember).argumentIndex);
} else {
var localVariable = (LocalVariable) frameMember;
if (localVariable.declarationKind == DeclarationKind.CONST) {
throw new EasyScriptException("Assignment to constant variable '" + variableId + "'");
}
return LocalVarAssignmentExprNodeGen.create(initializerExpr, localVariable.variableIndex);
}
}
}
}
If the assignment is to a global or local variable,
we use either GlobalVarAssignmentExprNode
(which is unchanged from when it was introduced in a
previous part
of the series),
or LocalVarAssignmentExprNode
that we’ve seen above, respectively.
But if the assignment is to a function argument,
we use the WriteFunctionArgExprNode
class:
import com.oracle.truffle.api.frame.VirtualFrame;
public final class WriteFunctionArgExprNode extends EasyScriptExprNode {
private final int index;
@SuppressWarnings("FieldMayBeFinal")
@Child
private EasyScriptExprNode initializerExpr;
public WriteFunctionArgExprNode(EasyScriptExprNode initializerExpr, int index) {
this.index = index;
this.initializerExpr = initializerExpr;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
Object value = this.initializerExpr.executeGeneric(frame);
frame.getArguments()[this.index] = value;
return value;
}
}
But this raises an interesting question.
Like we saw in the previous article,
when calling a function in JavaScript,
you don’t have to provide all declared arguments.
But in those cases, the arguments
array in the VirtualFrame
might not have enough elements to perform the write.
Like in the following code:
function f(a, b) {
b = 3;
return b;
}
f(1);
To handle this case, we need to modify the FunctionDispatchNode
from the
previous article
to make sure we extend the array of arguments before performing the call:
public abstract class FunctionDispatchNode extends Node {
// ...
private static Object[] extendArguments(Object[] arguments, FunctionObject function) {
if (arguments.length >= function.argumentCount) {
return arguments;
}
Object[] ret = new Object[function.argumentCount];
for (int i = 0; i < function.argumentCount; i++) {
ret[i] = i < arguments.length ? arguments[i] : Undefined.INSTANCE;
}
return ret;
}
}
Parsing a reference expression
And finally, we need to handle referencing variables:
public final class EasyScriptTruffleParser {
// ...
private EasyScriptExprNode parseReference(String variableId) {
FrameMember frameMember = this.functionLocals.get(variableId);
if (frameMember == null) {
return GlobalVarReferenceExprNodeGen.create(variableId);
} else {
return frameMember instanceof FunctionArgument
? new ReadFunctionArgExprNode(((FunctionArgument) frameMember).argumentIndex)
: LocalVarReferenceExprNodeGen.create(((LocalVariable) frameMember).variableIndex);
}
}
}
This is where we use the information about the frame slot we saved in LocalVarAssignmentExprNode
:
import com.oracle.truffle.api.dsl.NodeField;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
@NodeField(name = "frameSlot", type = int.class)
public abstract class LocalVarReferenceExprNode extends EasyScriptExprNode {
protected abstract int getFrameSlot();
@Specialization(guards = "frame.isInt(getFrameSlot())")
protected int readInt(VirtualFrame frame) {
return frame.getInt(this.getFrameSlot());
}
@Specialization(guards = "frame.isDouble(getFrameSlot())", replaces = "readInt")
protected double readDouble(VirtualFrame frame) {
return frame.getDouble(this.getFrameSlot());
}
@Specialization(replaces = {"readInt", "readDouble"})
protected Object readObject(VirtualFrame frame) {
return frame.getObject(this.getFrameSlot());
}
}
Summary
So, this is how to implement user-defined functions in Truffle.
As you can see, most of the complexity in the implementations comes from the static analysis needed to determine whether a given reference is to a local,
or global, variable.
The code in the Nodes themselves is relatively straightforward,
and doesn’t really use many features we haven’t seen before –
the new elements in this part are mainly the APIs related to storing different values in the VirtualFrame
instance.
As usual, all code from the article is available on GitHub.
In the next part of the series, we will add loops and control flow to our 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